Agregue Ambilight al reproductor con lámparas inteligentes Xiaomi





¡Hola!

Creo que muchos de los que están interesados ​​en una casa inteligente o simplemente en la disposición tecnológica de su hogar, pensaron en el sistema de iluminación "atmosférico" y no estándar.



Philips ofrece una forma de iluminar una habitación de una manera tan "inusual" mientras mira películas con la tecnología Ambilight incorporada en los televisores altamente sofisticados de la marca.



¡En este artículo, descubrirás la implementación de Ambilight con las bombillas inteligentes Xiaomi Yeelight!



Sobre Ambilight



Quién no lo sabe: la tecnología Ambilight es una luz de fondo incorporada en los televisores, que analiza la imagen en color del marco en la pantalla del televisor y reproduce la luz difusa alrededor del perímetro del televisor.







Ventajas de Ambilight:



  • , ;
  • ;
  • , .


En general, Ambilight es una tecnología bastante interesante, y la confirmación de este hecho es la presencia de una gran cantidad de diversas opciones para su implementación "artesanal", presentadas en Internet. Sin embargo, se basan mayoritariamente en el uso de una tira de LED direccionable pegada a la parte posterior de la cubierta del televisor / monitor / computadora portátil. Para tal implementación, es necesario tener al menos un controlador externo físico responsable de controlar los LED. Esto requiere conocimientos específicos de una persona que quiera instalar dicho sistema. Por lo tanto, como alternativa, propongo la versión más "progresiva" y bastante simple de dicha luz de fondo utilizando lámparas inteligentes.



¿Qué son estas lámparas inteligentes?



Para crear esta opción de iluminación, necesitará cualquier dispositivo de iluminación de la marca Yeelight (una subsidiaria de Xiaomi) o Xiaomi (pero solo aquellos que mencionen Yeelight en el nombre). Esto significa que el dispositivo está integrado en el ecosistema del hogar inteligente de Xiaomi y se controla a través de la aplicación Yeelight.







En mi opinión, la retroiluminación adaptativa no es una característica por la que alguien corra a comprar una lámpara inteligente Xiaomi (por cierto, por una cantidad sustancial de dinero). Sin embargo, para mí, esta es una buena oportunidad para ampliar la funcionalidad de una lámpara existente en casa. En cualquier caso, como propietario de dos lámparas Xiaomi, puedo decir que después de dos meses de usarlas, solo tengo impresiones agradables.



La aplicación Yeelight juega un papel importante en la implementación de este proyecto, ya que tiene un parámetro útil: el modo Desarrollador .





En las últimas actualizaciones, se le cambió el nombre a "Control LAN".



El ecosistema del hogar inteligente moderno se basa en el intercambio de datos entre dispositivos a través del protocolo wi-fi. Cada dispositivo inteligente tiene un módulo wi-fi incorporado que le permite conectarse a una red inalámbrica local. Gracias a esto, el dispositivo se controla a través del servicio en la nube de la casa inteligente. Sin embargo, el modo Desarrollador le permite comunicarse con el dispositivo directamente enviando solicitudes a la dirección IP asignada al dispositivo (la dirección del dispositivo se puede encontrar en la aplicación Yeelight en la información del dispositivo). Este modo garantiza la recepción de datos de dispositivos que están en la misma red local que la lámpara inteligente. El sitio web de Yeelight tiene una pequeña demostración de la funcionalidad del modo de desarrollador.



Gracias a esta opción, es posible implementar la función de iluminación adaptativa e incrustarla en el reproductor de código abierto.



Definición funcional



Se dedicará un post adicional a las dificultades (y formas de resolverlas) que puede enfrentar un ingeniero cuando esté pensando en diseñar tal cosa, así como el progreso general en la implementación del plan.



Si solo está interesado en un programa listo para usar, puede ir directamente al elemento "Para aquellos que solo quieren usar un reproductor listo para usar".



En primer lugar, decidamos las tareas que debe resolver el proyecto que se está desarrollando. Los puntos principales de los TOR para este proyecto:



  • Es necesario desarrollar una funcionalidad que le permita cambiar dinámicamente los parámetros (color o brillo / temperatura de la luz en el caso de usar un dispositivo sin LED rgb) de la lámpara inteligente, dependiendo de la imagen actual en la ventana del reproductor multimedia.
  • .
  • , «» .
  • .
  • .




,



Si no desea comprender la implementación de la iluminación adaptable y solo desea utilizar un reproductor listo para usar, puede descargar el archivo jar ya ensamblado del repositorio y luego asegúrese de leer la sección Antes de comenzar en el archivo README del repositorio.









La etapa inicial del desarrollo del proyecto será la definición de un jugador para incorporar una función y una biblioteca para la comunicación con una lámpara inteligente.



Mi elección recayó en el reproductor vlcj y la biblioteca Yapi , escrita en Java . Maven se utilizó como herramienta de construcción .



Vlcj es un marco que le permite incrustar un reproductor VLC nativo en una aplicación Java, así como administrar el ciclo de vida del reproductor a través del código Java. El autor del marco también tiene una versión de demostración del reproductor , que repite casi por completo la interfaz y la funcionalidad del reproductor VLC. La versión más estable del reproductor en este momento es la versión 3. Se utilizará en el proyecto.





Interfaz del reproductor vlcj con ventanas adicionales abiertas



Ventajas del reproductor vlcj:



  • una gran cantidad de formatos de video compatibles, que es una característica de larga data del reproductor VLC;
  • Java como PL, que le permite abrir el reproductor en una gran cantidad de sistemas operativos (en este caso, estamos limitados solo por la implementación del reproductor VLC, que está indisolublemente vinculado con una aplicación Java).


Desventajas:



  • diseño desactualizado del reproductor, que se resuelve mediante su propia implementación de la interfaz;
  • Antes de usar el programa, debe instalar un reproductor VLC y Java versión 8 o superior, lo que definitivamente es un inconveniente.


El uso de Yapi como biblioteca para conectarse con dispositivos inteligentes Yeelight puede justificarse principalmente por la simplicidad y, en segundo lugar, por la escasez de soluciones listas para usar . Por el momento, no hay muchas herramientas de terceros para controlar las lámparas inteligentes, especialmente en el lenguaje Java.



La principal desventaja de la biblioteca Yapi es que ninguna de sus versiones está presente en el repositorio de Maven, por lo que antes de compilar el código del proyecto, debe instalar manualmente Yapi en el repositorio local (la instalación completa se describe en el archivo README en el repositorio).



Algoritmo de análisis de imágenes



El principio de iluminación dinámica se basará en un análisis de color periódico del cuadro actual.



Como resultado de la etapa de prueba y error, se desarrolló el siguiente principio de análisis de imágenes:



Con la frecuencia especificada, el programa toma una captura de pantalla del reproductor multimedia y recibe un objeto de la clase BufferedImage. A continuación, con el algoritmo integrado más rápido, la imagen original cambia de tamaño a 20x20 píxeles.



Esto es necesario para la velocidad del algoritmo, por lo que podemos sacrificar cierta precisión en la determinación del color. También es necesario para minimizar la dependencia del tiempo de procesamiento de la imagen en la resolución del archivo multimedia actual.



A continuación, el algoritmo divide la imagen resultante en cuatro zonas "base" (arriba a la izquierda, abajo a la izquierda, etc.) de 10x10 píxeles de tamaño.





Zonas "básicas"



Este mecanismo se implementa para proporcionar un análisis independiente de diferentes zonas de imagen, lo que le permite colocar el dispositivo de iluminación en un lugar determinado de la habitación en el futuro e indicar qué zona de imagen necesita "rastrear". Cuando se utiliza con un programa de varias lámparas, esta funcionalidad hace que la iluminación dinámica sea mucho más atmosférica.



Luego, para cada área de la imagen, se calcula un color promedio calculando la media aritmética por separado para los tres componentes de color (rojo, verde, azul) de cada píxel y organizando los datos resultantes en un solo valor de color.



Gracias a los cuatro valores resultantes, podemos:



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

    (r0.2126+gramo0,7152+segundo0.0722)/255100

    r, g, b – //
  • :

    {0,rsegundo,(r-segundo)/255100,r>segundo

    donde r , b - componentes de color rojo / azul


Para una mecánica eficiente y escalable de calcular los parámetros de la imagen, todos los datos adicionales (no las zonas "base", la temperatura y el brillo del color) se calculan "de forma perezosa", es decir, según sea necesario.



Todo el código de procesamiento de imágenes encaja en una clase 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;
        }
    }
}




Para evitar que el parpadeo frecuente de la lámpara irritara el ojo, se introdujo un umbral para cambiar los parámetros. Por ejemplo, la lámpara cambiará el valor de brillo solo si la escena actual de la película es más de un 10 por ciento más brillante que la anterior.



Comparación con otro método de análisis



Podría preguntar: "¿Por qué no reducir la imagen a 2x2 píxeles y contar los valores resultantes?" ...

La respuesta será: “Según mis experimentos, el algoritmo para determinar el color promedio mediante la reducción del tamaño de la imagen (o sus zonas) resultó ser menos estable y menos confiable (especialmente al analizar las áreas oscuras de la imagen) que el algoritmo basado en determinar la media aritmética de todos los píxeles. " .



Se han probado varios métodos para cambiar el tamaño de las imágenes. Era posible usar la biblioteca openCV para un trabajo más serio con la imagen, pero consideré que esto era un exceso de ingeniería para esta tarea. A modo de comparación, a continuación se muestra un ejemplo de cómo definir un color utilizando la escala rápida incorporada de la clase BufferedImage y calculando la media aritmética. Creo que los comentarios son superfluos.







Configurando



Por el momento, el programa está configurado usando un archivo json. JSON.simple se utilizó como biblioteca para analizar el archivo de configuración .



El archivo Json debe llamarse "config.json" y colocarse en la misma carpeta con el programa para la detección automática de la configuración; de lo contrario, cuando la función de brillo adaptativo está habilitada, el programa le pedirá que especifique el archivo de configuración usted mismo abriendo la ventana de selección de archivos. En el archivo, debe especificar las direcciones IP de los dispositivos de iluminación, las zonas de imagen "monitoreadas" para cada dispositivo, los coeficientes de brillo y temperatura de color o el período de su instalación automática (que se describirá en el siguiente párrafo). Las reglas para llenar el archivo json se describen en el archivo README del proyecto.





Todos los cambios en la interfaz (botón de luz). Al presionar el botón se aplicará el archivo de configuración disponible o se abrirá una ventana para su selección.Los



coeficientes son necesarios para un ajuste más preciso del análisis de la imagen, por ejemplo, para hacer la lámpara un poco más oscura o, por el contrario, más clara. Todos estos parámetros son opcionales. El único parámetro requerido aquí son los valores de las direcciones IP de los dispositivos de iluminación.



Configuración automática de probabilidades



El programa también implementa la función de ajuste automático de los coeficientes en función de la iluminación actual de la habitación. Sucede así: la cámara web de su computadora portátil toma una instantánea del entorno a una frecuencia seleccionada, analiza su brillo usando los algoritmos ya descritos y luego establece el coeficiente de acuerdo con la fórmula:

l=1+X/100

donde x es el brillo actual de la habitación como porcentaje.



Esta función se habilita escribiendo una etiqueta especial en el archivo de configuración.



Un ejemplo de cómo funciona la funcionalidad





Conclusión



Como resultado de la solución del problema, se desarrolló una funcionalidad que le permite usar lámparas inteligentes Yeelight como retroiluminación adaptativa de archivos multimedia. Adicionalmente, se ha implementado la función de analizar la iluminación actual de la habitación. Todo el código fuente está disponible en un enlace en mi repositorio de github .



¡Gracias a todos por su atención!



PD: Estaré encantado de recibir adiciones, comentarios e indicaciones de errores.



All Articles