
Olá!
Acho que muitos que estão interessados em uma casa inteligente ou simplesmente em um arranjo tecnológico de sua casa, pensaram no sistema de iluminação "atmosférico" e não padrão.
Uma maneira de iluminar uma sala de uma forma tão "incomum" enquanto assiste a filmes é oferecida pela Philips com a tecnologia Ambilight integrada nas TVs
Neste artigo, você descobrirá a implementação do Ambilight com lâmpadas inteligentes Xiaomi Yeelight!
Sobre Ambilight
Quem não sabe - A tecnologia Ambilight é uma luz de fundo embutida nas TVs, que analisa a imagem colorida do quadro na tela da TV e reproduz a luz difusa em todo o perímetro da TV.

Prós do Ambilight:
- , ;
- ;
- , .
Em geral, o Ambilight é uma tecnologia bastante interessante, e a confirmação desse fato é a presença de um grande número de várias opções para sua implementação "artesanal", apresentadas na Internet. No entanto, eles são amplamente baseados no uso de uma faixa de LED endereçável colada na parte traseira da tampa da TV / monitor / laptop. Para tal, é necessário que haja pelo menos um controlador físico externo responsável pelo controle dos LEDs. Isso requer conhecimento específico de uma pessoa que deseja instalar tal sistema. Portanto, como alternativa, proponho a versão mais "proger" e bastante simples de tal luz de fundo usando lâmpadas inteligentes.
O que são essas lâmpadas inteligentes?
Para criar esta opção de iluminação, você precisará de qualquer dispositivo de iluminação da marca Yeelight (uma subsidiária da Xiaomi) ou Xiaomi (mas apenas aqueles que mencionam Yeelight no nome). Isso significa que o dispositivo está integrado ao ecossistema de casa inteligente Xiaomi e é controlado por meio do aplicativo Yeelight.

Na minha opinião, a luz de fundo adaptável não é um recurso pelo qual alguém correrá para comprar uma lâmpada inteligente Xiaomi (por uma quantia substancial de dinheiro, a propósito). No entanto, para mim, esta é uma boa oportunidade para expandir a funcionalidade de uma lâmpada existente em casa. Em todo caso, como dono de duas lâmpadas Xiaomi, posso dizer que depois de dois meses de uso, tenho apenas impressões agradáveis.
A aplicação Yeelight desempenha um papel importante na implementação deste projeto, pois possui um parâmetro útil - o modo Desenvolvedor .

Nas últimas atualizações, ele foi renomeado para "Controle de LAN"
O moderno ecossistema de casa inteligente é baseado na troca de dados entre dispositivos por meio do protocolo wi-fi. Cada dispositivo inteligente possui um módulo wi-fi integrado que permite a conexão a uma rede sem fio local. Graças a isso, o dispositivo é controlado por meio do serviço de nuvem da casa inteligente. No entanto, o modo Desenvolvedor permite que você se comunique com o dispositivo diretamente, enviando solicitações ao endereço IP alocado para o dispositivo (o endereço do dispositivo pode ser encontrado no aplicativo Yeelight nas informações do dispositivo). Este modo garante a recepção de dados de dispositivos que estão na mesma rede local que a lâmpada inteligente. O site Yeelight tem uma pequena demonstração da funcionalidade do modo de desenvolvedor.
Graças a esta opção, é possível implementar a função de iluminação adaptativa e incorporá-la no reprodutor de código aberto.
Definição funcional
Um outro post será dedicado a quais dificuldades (e maneiras de resolvê-las) um engenheiro pode enfrentar quando está pensando em projetar tal coisa, bem como o progresso geral na implementação do plano.
Se você está interessado apenas em um programa pronto, pode ir diretamente para o item "Para quem deseja usar apenas um reprodutor pronto".
Em primeiro lugar, vamos decidir sobre as tarefas que o projeto em desenvolvimento deve resolver. Os principais pontos do TOR para este projeto:
- É necessário desenvolver funcionalidade que permita alterar dinamicamente os parâmetros (cor ou brilho / temperatura da luz no caso de usar um dispositivo sem LEDs rgb) da lâmpada inteligente, dependendo da imagem atual na janela do media player.
- .
- , «» .
- .
- .
,
A etapa inicial de desenvolvimento do projeto será a definição de um player para embutir uma função e uma biblioteca para comunicação com uma lâmpada inteligente.
Minha escolha recaiu sobre o vlcj player e a biblioteca Yapi , escrita em Java . Maven foi usado como uma ferramenta de construção .
Vlcj é uma estrutura que permite incorporar um player VLC nativo em um aplicativo Java, bem como gerenciar o ciclo de vida do player por meio de código java. O autor do framework também tem uma versão demo do player , que repete quase completamente a interface e funcionalidade do player VLC. A versão mais estável do player no momento é a versão 3. Ela será usada no projeto.

Interface do player Vlcj com janelas adicionais abertas
Vantagens do player Vlcj:
- um grande número de formatos de vídeo suportados, que é um recurso de longa data do player VLC;
- Java como um PL, que permite abrir o player em um grande número de sistemas operacionais (neste caso, estamos limitados apenas pela implementação do player VLC, que está intimamente ligado a um aplicativo java).
Desvantagens:
- design desatualizado do player, o que é resolvido por sua própria implementação da interface;
- Antes de usar o programa, você precisa instalar um VLC player e Java versão 8 ou superior, o que é definitivamente uma desvantagem.
O uso de Yapi como uma biblioteca para conexão com dispositivos inteligentes Yeelight pode ser justificado principalmente pela simplicidade e, em segundo lugar, pela escassez de soluções prontas. No momento, não existem muitas ferramentas de terceiros para controlar lâmpadas inteligentes, especialmente na linguagem Java.
A principal desvantagem da biblioteca Yapi é que nenhuma de suas versões está presente no repositório Maven, portanto, antes de compilar o código do projeto, você precisa instalar manualmente o Yapi no repositório local (toda a instalação é descrita no arquivo README do repositório).
Algoritmo de análise de imagem
O princípio da iluminação dinâmica será baseado na análise periódica da cor do quadro atual.
Como resultado da etapa de tentativa e erro, o seguinte princípio de análise de imagem foi desenvolvido:
Na frequência especificada, o programa faz uma captura de tela do reprodutor de mídia e recebe um objeto da classe BufferedImage. Em seguida, com o algoritmo integrado mais rápido, a imagem original é redimensionada para 20x20 pixels.
Isso é necessário para a velocidade do algoritmo, por causa do qual podemos sacrificar alguma precisão na determinação da cor. Também é necessário para minimizar a dependência do tempo de processamento da imagem na resolução do arquivo de mídia atual.
A seguir, o algoritmo divide a imagem resultante em quatro zonas "básicas" (superior esquerdo, inferior esquerdo, etc.) de 10x10 pixels de tamanho.

Zonas "básicas"
Este mecanismo é implementado para fornecer uma análise independente de diferentes zonas de imagem, o que permite colocar o dispositivo de iluminação em um determinado local da sala no futuro e indicar qual zona de imagem ele precisa "rastrear". Quando usado com um programa de lâmpadas múltiplas, esta funcionalidade torna a iluminação dinâmica muito mais atmosférica.
Em seguida, para cada área da imagem, uma cor média é calculada calculando a média aritmética separadamente para os três componentes de cor (vermelho, verde, azul) de cada pixel e organizando os dados resultantes em um único valor de cor.
Graças aos quatro valores resultantes, podemos:
- 5 : , , , ( «» );
- :
r, g, b – // -
Para uma mecânica eficiente e escalável de cálculo de parâmetros de imagem, todos os dados adicionais (não zonas de "base", temperatura e brilho de cor) são calculados "preguiçosamente", ou seja, como necessário.
Todo o código de processamento de imagem se encaixa em uma classe 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 a oscilação frequente da lâmpada irrite os olhos, foi introduzido um limite para alterar os parâmetros. Por exemplo, a lâmpada mudará o valor do brilho apenas se a cena atual do filme for mais de 10 por cento mais brilhante do que a anterior.
Comparação com outro método de análise
Você pode perguntar: "Por que não diminuir a imagem para 2x2 pixels e contar os valores resultantes?" ...
A resposta será a seguinte: “Com base em meus experimentos, o algoritmo para determinar a cor média reduzindo o tamanho da imagem (ou suas zonas) provou ser menos estável e menos confiável (especialmente ao analisar áreas escuras da imagem) do que o algoritmo baseado na determinação da média aritmética de todos os pixels " .
Vários métodos foram tentados para redimensionar imagens. Foi possível usar a biblioteca openCV para um trabalho mais sério com a imagem, mas considerei isso uma engenharia exagerada para essa tarefa. Para comparação, abaixo está um exemplo de definição de uma cor usando o escalonamento rápido embutido da classe BufferedImage e calculando a média aritmética. Eu acho que comentários são desnecessários.

Configurando
No momento, o programa está configurado usando um arquivo json. JSON.simple foi usado como uma biblioteca para analisar o arquivo de configuração .
O arquivo json deve ser nomeado "config.json" e colocado na mesma pasta com o programa para detecção automática de configuração, caso contrário, quando a função de brilho adaptável estiver habilitada, o programa solicitará que você especifique o arquivo de configuração abrindo a janela de seleção de arquivo. No arquivo, você deve especificar os endereços IP dos dispositivos de iluminação, as zonas de imagem "monitoradas" para cada dispositivo, os coeficientes de brilho e temperatura de cor, ou o período de sua instalação automática (que será descrito no próximo parágrafo). As regras para preenchimento do arquivo json estão descritas no arquivo README do projeto.

Todas as mudanças na interface (botão de luz). Quando o botão é pressionado, o arquivo de configuração disponível será aplicado ou uma janela para sua seleção será aberta
Os coeficientes são necessários para uma configuração mais precisa da análise da imagem, por exemplo, para tornar a lâmpada um pouco mais escura ou, ao contrário, mais clara. Todos esses parâmetros são opcionais. Os únicos parâmetros necessários aqui são os valores dos endereços IP das luminárias.
Configuração automática de probabilidades
Além disso, o programa implementa a função de ajuste automático dos coeficientes dependendo da iluminação atual da sala. Acontece assim: a webcam do seu laptop tira um instantâneo do ambiente em uma frequência selecionada, analisa seu brilho usando os algoritmos já descritos e, em seguida, define o coeficiente de acordo com a fórmula:
Esta função é habilitada escrevendo uma tag especial no arquivo de configuração.
Um exemplo de como a funcionalidade funciona
Conclusão
Como resultado da resolução do problema, foi desenvolvida uma funcionalidade que permite o uso de lâmpadas inteligentes Yeelight como retroiluminação adaptativa de arquivos de mídia. Além disso, foi implementada a função de analisar a iluminação atual da sala. Todo o código-fonte está disponível em um link no meu repositório github .
Obrigado a todos pela atenção!
PS Terei todo o prazer em quaisquer acréscimos, comentários e indicações de erros.