使用小米智能灯将流光溢彩添加到播放器





你好!

我认为许多对智能家居或仅仅对自己的住宅进行技术配置感兴趣的人都在考虑“大气”和非标准照明系统。



在看电影时照亮房间的一种方法是,飞利浦在其高度复杂的电视中内置了流光溢彩技术



在本文中,您将发现使用小米Yeelight智能灯泡实现流光溢彩!



关于流光溢彩



谁不知道-流光溢彩技术是电视中内置的背光灯,它可以分析电视屏幕上画面的彩色图像,并在电视周边重现散射光。







流光溢彩的优点:



  • , ;
  • ;
  • , .


通常,流光溢彩是一种相当有趣的技术,这一事实的确认是在Internet上提供了用于其“手工艺品”实现的大量各种选项。但是,它们绝大多数是基于粘贴在电视/显示器/笔记本电脑盖背面的可寻址LED灯条的使用。对于这样的实施方式,必须至少具有负责控制LED的物理外部控制器。这需要想要安装这种系统的人的特定知识。因此,作为替代方案,我提出了使用智能灯的这种背光的“最先进”和相当简单的版本。



这些智能灯是什么?



要创建此照明选项,您将需要任何Yeelight品牌照明设备(小米的子公司)或小米(但仅名称中提及Yeelight的设备)。这意味着该设备已嵌入到小米智能家居生态系统中,并通过Yeelight应用程序进行控制。







我认为,自适应背光不是某人会购买小米智能灯的功能(顺便说一句,要花很多钱)。但是,对我而言,这是扩展家用现有灯功能的好机会。无论如何,作为两个小米灯的拥有者,我可以说,在使用它们两个月之后,我只有令人愉快的印象。



Yeelight应用程序在该项目的实施中起着重要作用,因为它具有一个有用的参数-Developer mode





在最新更新中,它已重命名为“ LAN Control”



现代的智能家居生态系统基于使用Wi-Fi协议的设备之间的数据交换。每个智能设备都有一个内置的wi-fi模块,可让您连接到本地无线网络。因此,可以通过智能家居的云服务来控制设备。但是,开发人员模式允许您通过向分配给设备的IP地址发送请求来直接与设备通信(可以在Yeelight应用程序的设备信息中找到设备地址)。此模式可确保从与智能灯位于同一局域网中的设备接收数据。 Yeelight网站上有一个有关开发人员模式功能的小演示



借助此选项,可以实现自适应照明功能并将其嵌入到开源播放器中。



功能定义



另一篇文章将专门讨论工程师在考虑设计此类事物时可能面临的困难(以及解决方法)以及计划实施的总体进展。



如果您只对现成的程序感兴趣,则可以直接转到“对于那些只想使用现成的播放器的人”项目。



首先,让我们决定要开发的项目应解决的任务。该项目的职责范围要点:



  • 有必要开发功能,使您可以根据媒体播放器窗口中的当前图像动态更改智能灯的参数(在使用不带rgb LED的设备的情况下,灯光的颜色或亮度/温度)。
  • .
  • , «» .
  • .
  • .




,



如果您不希望了解自适应照明的实现,而只想使用现成的播放器,则可以存储库下载已经组装好的jar文件,然后确保存储库中阅读README文件中的“开始之前”部分









项目开发的初始阶段将是定义用于嵌入功能的播放器以及与智能灯进行通信的库。



我的选择取决于Java编写vlcj播放器YapiMaven被用作构建工具Vlcj是一个框架,允许您将本机VLC播放器嵌入Java应用程序,并通过Java代码管理播放器的生命周期。该框架的作者还具有播放器演示版,该演示版几乎完全重复了VLC播放器的界面和功能。目前,播放器最稳定的版本是版本3。它将在项目中使用。









具有打开其他窗口



的Vlcj播放器界面vlcj播放器的优点:



  • 支持多种视频格式,这是VLC播放器的一项长期功能;
  • Java作为PL,它使您可以在许多操作系统上打开播放器(在这种情况下,我们仅受VLC播放器的实现限制,该播放器与Java应用程序有着千丝万缕的联系)。


缺点:



  • 播放器的过时设计,可通过其自己的界面实现解决;
  • 在使用该程序之前,您需要安装VLC播放器和Java 8或更高版本,这绝对是一个缺点。


使用Yapi作为连接智能Yeelight小工具的库的使用,首先可以通过简单性来证明其合理性,其次,可以通过缺乏现成的解决方案来证明。目前,没有很多用于控制智能灯的第三方工具,尤其是Java语言。



Yapi库的主要缺点是Maven存储库中没有其任何版本,因此在编译项目代码之前,您需要手动将Yapi安装到本地存储库(整个安装在存储库的README文件中进行了描述)。



图像分析算法



动态照明的原理将基于当前帧的定期颜色分析。



经过反复试验,开发出了以下图像分析原理:



在指定频率下,程序获取媒体播放器的屏幕截图并接收BufferedImage类的对象。接下来,使用最快的内置算法,将原始图像调整为20x20像素。



这对于算法的速度是必需的,因此,我们可以牺牲确定颜色的准确性。为了使图像处理时间对当前媒体文件的分辨率的依赖性最小,也是必要的。



接下来,该算法将结果图像划分为大小为10x10像素的四个“基本”区域(左上方,左下方等)。





“基本”区域



此机制的实现是为了对不同的图像区域提供独立的分析,从而使您将来可以将照明设备放置在房间的特定位置,并指示需要“跟踪”哪个图像区域。当与多灯程序一起使用时,此功能使动态照明更加大气。



然后,对于图像的每个区域,通过分别计算每个像素的三个颜色分量(红色,绿色,蓝色)的算术平均值并将结果数据排列为单个颜色值来计算平均颜色。



由于有四个结果值,我们可以:



  • 5 : , , , ( «» );
  • :

    [R0.2126+G0.7152+b0.0722/255100

    r, g, b – //
  • :

    {0[Rb[R--b/255100[R>b

    其中rb-红色/蓝色分量


对于计算图像参数的有效且可扩展的机制,所有其他数据(不是“基本”区域,温度和颜色亮度)都是“延迟”计算的,即 如所须。



所有图像处理代码都适合一个ImageHandler类:



public class ImageHandler {
    private static List<ScreenArea> mainAreas = Arrays.asList(ScreenArea.TOP_LEFT, ScreenArea.TOP_RIGHT, ScreenArea.BOTTOM_LEFT, ScreenArea.BOTTOM_RIGHT);
    private static int scaledWidth = 20;
    private static int scaledHeight = 20;
    private static int scaledWidthCenter = scaledWidth / 2;
    private static int scaledHeightCenter = scaledHeight / 2;
    private Map<ScreenArea, Integer> screenData;
    private LightConfig config;

    //        
    private int[] getDimensions(ScreenArea area) {
        int[] dimensions = new int[4];
        if (!mainAreas.contains(area)) {
            return dimensions;
        }
        String name = area.name().toLowerCase();
        dimensions[0] = (name.contains("left")) ? 0 : scaledWidthCenter;
        dimensions[1] = (name.contains("top")) ? 0 : scaledHeightCenter;
        dimensions[2] = scaledWidthCenter;
        dimensions[3] = scaledHeightCenter;
        return dimensions;
    }

    //    
    private BufferedImage getScaledImage(BufferedImage image, int width, int height) {
        Image tmp = image.getScaledInstance(width, height, Image.SCALE_FAST);
        BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

        Graphics2D g2d = scaledImage.createGraphics();
        g2d.drawImage(tmp, 0, 0, null);
        g2d.dispose();
        return scaledImage;
    }

    // ,   ,   ,   
    private void proceedImage(BufferedImage image) {
        BufferedImage scaledImage = getScaledImage(image, scaledWidth, scaledHeight);

        screenData = new HashMap<>();
        mainAreas.forEach(area -> {
            int[] dimensions = getDimensions(area);
            BufferedImage subImage = scaledImage.getSubimage(dimensions[0], dimensions[1], dimensions[2], dimensions[3]);

            int average = IntStream.range(0, dimensions[3])
                    .flatMap(row -> IntStream.range(0, dimensions[2]).map(col -> subImage.getRGB(col, row))).boxed()
                    .reduce(new ColorAveragerer(), (t, u) -> {
                        t.accept(u);
                        return t;
                    }, (t, u) -> {
                        t.combine(u);
                        return t;
                    }).average();

            screenData.put(area, average);
        });
    }

    public ImageHandler(BufferedImage image, LightConfig config) {
        this.config = config;
        proceedImage(image);
    }

    //       ,  considerRate   (    )
    public int getValue(ScreenArea area, Feature feature, Boolean considerRate) {
        Integer intValue = screenData.get(area);
        if (intValue != null) {
            Color color = new Color(intValue);
            if (feature == Feature.COLOR) {
                return color.getRGB();
            } else if (feature == Feature.BRIGHTNESS || feature == Feature.TEMPERATURE) {
                int value = (feature == Feature.BRIGHTNESS) ? getBrightness(color) : getTemperature(color);
                double rate = (feature == Feature.BRIGHTNESS) ? config.getBrightnessRate() : config.getTemperatureRate();
                value = (value < 0) ? 0 : value;
                if (considerRate) {
                    value = 10 + (int) (value * rate);
                }
                return (value > 100) ? 100 : value;
            } else {
                return 0;
            }
        } else {
            calculateArea(area);
            return getValue(area, feature, considerRate);
        }
    }
   
    //    
    private int getBrightness(Color color) {
        return (int) ((color.getRed() * 0.2126f + color.getGreen() * 0.7152f + color.getBlue() * 0.0722f) / 255 * 100);
    }

    //    
    private int getTemperature(Color color) {
        return (int) ((float) (color.getRed() - color.getBlue()) / 255 * 100);
    }

    //   "" 
    private void calculateArea(ScreenArea area) {
        int value = 0;
        switch (area) {
            case TOP:
                value = getAverage(ScreenArea.TOP_LEFT, ScreenArea.TOP_RIGHT);
                break;
            case BOTTOM:
                value = getAverage(ScreenArea.BOTTOM_LEFT, ScreenArea.BOTTOM_RIGHT);
                break;
            case LEFT:
                value = getAverage(ScreenArea.BOTTOM_LEFT, ScreenArea.TOP_LEFT);
                break;
            case RIGHT:
                value = getAverage(ScreenArea.BOTTOM_RIGHT, ScreenArea.TOP_RIGHT);
                break;
            case WHOLE_SCREEN:
                value = getAverage(mainAreas.toArray(new ScreenArea[0]));
                break;
        }
        screenData.put(area, value);
    }

    //      
    private int getAverage(ScreenArea... areas) {
        return Arrays.stream(areas).map(color -> screenData.get(color))
                .reduce(new ColorAveragerer(), (t, u) -> {
                    t.accept(u);
                    return t;
                }, (t, u) -> {
                    t.combine(u);
                    return t;
                }).average();
    }

    //  rgb  int-  
    public static int[] getRgbArray(int color) {
        int[] rgb = new int[3];
        rgb[0] = (color >>> 16) & 0xFF;
        rgb[1] = (color >>> 8) & 0xFF;
        rgb[2] = (color >>> 0) & 0xFF;
        return rgb;
    }

    // int-     rgb
    public static int getRgbInt(int[] pixel) {
        int value = ((255 & 0xFF) << 24) |
                ((pixel[0] & 0xFF) << 16) |
                ((pixel[1] & 0xFF) << 8) |
                ((pixel[2] & 0xFF) << 0);
        return value;
    }

   //         stream API
    private class ColorAveragerer {
        private int[] total = new int[]{0, 0, 0};
        private int count = 0;

        private ColorAveragerer() {
        }

        private int average() {
            int[] rgb = new int[3];
            for (int it = 0; it < total.length; it++) {
                rgb[it] = total[it] / count;
            }

            return count > 0 ? getRgbInt(rgb) : 0;
        }

        private void accept(int i) {
            int[] rgb = getRgbArray(i);
            for (int it = 0; it < total.length; it++) {
                total[it] += rgb[it];
            }
            count++;
        }

        private void combine(ColorAveragerer other) {
            for (int it = 0; it < total.length; it++) {
                total[it] += other.total[it];
            }
            count += other.count;
        }
    }
}




为了防止灯的频繁闪烁刺激眼睛,引入了用于改变参数的阈值。例如,仅当电影中的当前场景比前一个场景亮10%以上时,灯才会更改亮度值。



与其他分析方法的比较



您可能会问:“为什么不将图像缩小到2x2像素并计算结果值?” ...

答案将如下:“根据我的实验,通过减少图像(或其区域)的大小来确定平均颜色的算法被证明比基于确定所有像素的算术平均值的算法不稳定(尤其是在分析图像的暗区时)。 “



尝试了几种方法来调整图像大小。可以使用openCV库对图像进行更认真的处理,但是我认为这对这项任务来说是过度设计的。为了进行比较,下面是使用BufferedImage类的内置快速缩放定义颜色并计算算术平均值的示例。我认为评论是不必要的。







配置中



目前,该程序是使用json文件配置的。JSON.simple用作解析配置文件的库



Json文件必须命名为“ config.json”,并与程序位于同一文件夹中以进行自动配置检测,否则,当启用自适应亮度功能时,程序将通过打开文件选择窗口提示您指定配置文件。在文件中,您必须指定照明设备的IP地址,每个设备的“受监视”图像区域,亮度和色温系数或它们的自动安装时间(将在下一段中描述)。在项目的README文件中描述了填充json文件的规则。





界面中的所有更改(指示灯按钮)。按下按钮后,将使用可用的配置文件或打开其选择窗口,



这些系数对于图像分析的更精确设置是必不可少的,例如,使灯更暗或更轻。所有这些参数都是可选的。此处唯一需要的参数是照明设备的IP地址的值。



自动赔率设置



此外,该程序还实现了根据当前房间照明度自动调整系数的功能。发生这种情况:笔记本电脑的网络摄像头以选定的频率拍摄环境快照,使用已经描述的算法分析其亮度,然后根据以下公式设置系数:

=1个+X/100

其中x是当前房间亮度的百分比。



通过在配置文件中写入特殊标签来启用此功能。



功能实例





结论



解决问题的结果是,开发了一种功能,使您可以将Yeelight智能灯用作媒体文件的自适应背光。另外,已经实现了分析当前房间照明的功能。所有源代码都可以从我的github存储库中的链接获得



谢谢大家的关注!



附言:我将为任何补充,说明和错误指示感到高兴。



All Articles