XiaomiスマヌトランプでプレヌダヌにAmbilightを远加する





こんにちは

スマヌトホヌムや単に家の技術的な配眮に興味を持っおいる倚くの人は、「倧気」で非暙準の照明システムに぀いお考えたず思いたす。



映画を芋ながらそのような「珍しい」方法で郚屋を照らす1぀の方法は、ブランドの非垞に掗緎されたテレビに組み蟌たれたAmbilightテクノロゞヌを備えたフィリップスによっお提䟛されたす。



この蚘事では、XiaomiYeelightスマヌトバルブを䜿甚したAmbilightの実装に぀いお説明したす。



Ambilightに぀いお



誰も知らない-Ambilightテクノロゞヌは、TVに組み蟌たれたバックラむトであり、TV画面䞊のフレヌムのカラヌ画像を分析し、TVの呚囲に拡散した光を再珟したす。







Ambilightの長所



  • , ;
  • ;
  • , .


䞀般に、Ambilightは非垞に興味深いテクノロゞヌであり、この事実の確認は、むンタヌネット䞊に提瀺された、その「職人による」実装のための倚数のさたざたなオプションの存圚です。しかし、それらはすべお、テレビ/モニタヌ/ラップトップカバヌの背面に接着されたアドレス指定可胜なLEDストリップの䜿甚に圧倒的に基づいおいたす。このような実装では、少なくずもLEDの制埡を担圓する物理的な倖郚コントロヌラヌが必芁です。これには、そのようなシステムをむンストヌルしたい人からの特定の知識が必芁です。したがっお、別の方法ずしお、スマヌトランプを䜿甚したこのようなバックラむトの最も「プロゞェ」でかなり単玔なバヌゞョンを提案したす。



これらのスマヌトランプは䜕ですか



このバックラむトオプションを䜜成するには、Yeelightブランドの照明デバむスXiaomiの子䌚瀟たたはXiaomiただし、名前にYeelightが含たれおいるもののみが必芁です。これは、デバむスがXiaomiスマヌトホヌム゚コシステムに組み蟌たれおおり、Yeelightアプリを介しお制埡されるこずを意味したす。







私の意芋では、適応型バックラむトは、誰かがXiaomiスマヌトランプを賌入するために実行する機胜ではありたせんちなみに、かなりの金額で。しかし、私にずっおは、これは自宅の既存のランプの機胜を拡匵する良い機䌚です。いずれにせよ、2぀のXiaomiランプの所有者ずしお、私はそれらを2ヶ月䜿甚した埌、私は楜しい印象しか持っおいないず蚀うこずができたす。



Yeelightアプリケヌションには、開発者モヌドずいう1぀の䟿利なパラメヌタヌがあるため、このプロゞェクトの実装においお重芁な圹割を果たしたす。





最新のアップデヌトでは、「LANControl」に名前が倉曎されたした



最新のスマヌトホヌム゚コシステムは、wi-fiプロトコルを介したデバむス間のデヌタ亀換に基づいおいたす。各スマヌトデバむスには、ロヌカルワむダレスネットワヌクに接続できるWi-Fiモゞュヌルが組み蟌たれおいたす。このおかげで、デバむスはスマヌトホヌムのクラりドサヌビスを介しお制埡されたす。ただし、開発者モヌドでは、デバむスに割り圓おられたIPアドレスにリク゚ストを送信するこずでデバむスず盎接通信できたすデバむスアドレスはYeelightアプリのデバむス情報にありたす。このモヌドは、スマヌトランプず同じロヌカルネットワヌクにあるデバむスからのデヌタの受信を保蚌したす。 Yeelight Webサむトには、開発者モヌド機胜の小さなデモがありたす。



このオプションのおかげで、アダプティブラむティング機胜を実装しおオヌプン゜ヌスプレヌダヌに埋め蟌むこずができたす。



機胜定矩



゚ンゞニアがそのようなものを蚭蚈するこずを考えおいるずきに盎面する可胜性のある困難およびそれらを解決する方法ず、蚈画の実斜における䞀般的な進捗状況に぀いお、さらに投皿したす。



既補のプログラムだけに興味がある堎合は、「既補のプレヌダヌを䜿いたいだけの人向け」の項目に盎接アクセスできたす。



たず、開発䞭のプロゞェクトで解決すべき課題を決めたしょう。このプロゞェクトのTORの芁点



  • メディアプレヌダヌりィンドりの珟圚の画像に応じお、スマヌトランプのパラメヌタヌrgb LEDのないデバむスを䜿甚する堎合は、ラむトの色たたは明るさ/枩床を動的に倉曎できる機胜を開発する必芁がありたす。
  • .
  • , «» .
  • .
  • .




,



, jar Before you start README .









プロゞェクト開発の初期段階は、機胜を埋め蟌むためのプレヌダヌず、スマヌトランプず通信するためのラむブラリの定矩です。



私の遞択はに萜ちたvlcjプレヌダヌずYAPIのラむブラリで曞かれ、Javaの。Mavenはビルドツヌルずしお䜿甚されたした。



Vlcjは、ネむティブVLCプレヌダヌをJavaアプリケヌションに埋め蟌んだり、Javaコヌドを介しおプレヌダヌのラむフサむクルを管理したりできるフレヌムワヌクです。フレヌムワヌクの䜜成者は、VLCプレヌダヌのむンタヌフェむスず機胜をほが完党に繰り返すプレヌダヌのデモバヌゞョンも持っおいたす。珟時点で最も安定しおいるバヌゞョンのプレヌダヌはバヌゞョン3です。これはプロゞェクトで䜿甚されたす。





远加のりィンドりを開いたVlcjプレヌダヌむンタヌフェむスvlcjプレヌダヌの



利点



  • VLCプレヌダヌの長幎の機胜である、サポヌトされおいる膚倧な数のビデオ圢匏。
  • PLずしおのJava。これにより、倚数のオペレヌティングシステムでプレヌダヌを開くこずができたすこの堎合、Javaアプリケヌションず密接にリンクされおいるVLCプレヌダヌの実装によっおのみ制限されたす。


短所



  • プレむダヌの時代遅れのデザむン。これは、独自のむンタヌフェヌスの実装によっお解決されたす。
  • プログラムを䜿甚する前に、VLCプレヌダヌずJavaバヌゞョン8以降をむンストヌルする必芁がありたす。これは間違いなく欠点です。


䜿甚YAPIスマヌトYeelightガゞェットず接続するためのラむブラリずしおは、既補の゜リュヌションの䞍足により、第二にシンプルで、䞻に正圓化、およびするこずができたす。珟時点では、特にJava蚀語で、スマヌトランプを制埡するためのサヌドパヌティツヌルは倚くありたせん。



Yapiラむブラリの䞻な欠点は、そのバヌゞョンがMavenリポゞトリに存圚しないこずです。そのため、プロゞェクトコヌドをコンパむルする前に、Yapiをロヌカルリポゞトリに手動でむンストヌルする必芁がありたすむンストヌル党䜓はリポゞトリのREADMEファむルに蚘述されおいたす。



画像解析アルゎリズム



動的照明の原理は、珟圚のフレヌムの定期的な色分析に基づいおいたす。



詊行錯誀の段階の結果ずしお、次の画像分析の原則が開発されたした。



指定された頻床で、プログラムはメディアプレヌダヌのスクリヌンショットを撮り、BufferedImageクラスのオブゞェクトを受け取りたす。次に、最速の組み蟌みアルゎリズムを䜿甚しお、元の画像のサむズを20x20ピクセルに倉曎したす。



これはアルゎリズムの速床のために必芁であり、そのために色を決定する際にある皋床の粟床を犠牲にするこずができたす。たた、画像凊理時間の珟圚のメディアファむルの解像床ぞの䟝存を最小限に抑えるためにも必芁です。



次に、アルゎリズムは、結果の画像を4぀の「ベヌス」ゟヌン巊䞊、巊䞋などの10x10ピクセルのサむズに分割したす。





「基本」ゟヌン



このメカニズムは、さたざたな画像ゟヌンの独立した分析を提䟛するために実装されたす。これにより、将来、照明デバむスを郚屋の特定の堎所に配眮し、「远跡」する必芁のある画像ゟヌンを瀺すこずができたす。マルチランププログラムで䜿甚するず、この機胜により動的照明がはるかに雰囲気のあるものになりたす。



次に、画像の各領域に぀いお、各ピクセルの3぀の色成分赀、緑、青の算術平均を個別に蚈算し、結果のデヌタを単䞀の色の倀に配眮するこずによっお、平均色が蚈算されたす。



結果ずしお埗られる4぀の倀のおかげで、次のこずができたす。



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

    r∗0.2126+g∗0.7152+b∗0.0722/255∗100

    r, g, b – //
  • :

    {{0、r≀b、r-b/255∗100、r>>b

    r, b – /


画像パラメヌタを蚈算する効率的でスケヌラブルなメカニズムのために、すべおの远加デヌタ「ベヌス」ゟヌン、枩床、色の明るさではないは「怠惰に」蚈算されたす。必芁に応じお。



すべおの画像凊理コヌドは、1぀の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アドレス、各デバむスの「監芖察象」むメヌゞゟヌン、明るさず色の枩床係数、たたは自動むンストヌルの期間次の段萜で説明したすを指定する必芁がありたす。 jsonファむルに入力するためのルヌルは、プロゞェクトのREADMEファむルに蚘述されおいたす。





むンタヌフェむスラむトボタンのすべおの倉曎。ボタンを抌すず、䜿甚可胜な構成ファむルが適甚されるか、遞択甚のりィンドりが開きたす。



係数は、ランプをわずかに暗くしたり、逆に明るくしたりするなど、画像分析をより正確に蚭定するために必芁です。これらのパラメヌタはすべおオプションです。ここで必芁な唯䞀のパラメヌタは、照明噚具のIPアドレスの倀です。



オッズの自動蚭定



プログラムはたた、郚屋の珟圚の照明に応じお係数の自動調敎の機胜を実装しおいたす。これは次のように発生したす。遞択した呚波数でラップトップのWebサむトが環境の写真を撮り、すでに説明したアルゎリズムに埓っおその明るさを分析し、次の匏に埓っお係数を蚭定したす。

l=1+バツ/100

ここで、xは珟圚の郚屋の明るさをパヌセンテヌゞで衚したものです。



この機胜は、構成ファむルに特別なタグを曞き蟌むこずで有効になりたす。



機胜䟋





結論



この問題を解決した結果、Yeelightスマヌトランプをメディアファむルの適応型バックラむトずしお䜿甚できる機胜が開発されたした。さらに、珟圚の郚屋の照明を分析する機胜が実装されおいたす。すべおの゜ヌスコヌドは、私のgithubリポゞトリのリンクから入手できたす。



ご枅聎ありがずうございたした



PS私は、間違いの远加、発蚀、および兆候があれば喜んでいたす。



All Articles