Tambahkan Ambilight ke pemain dengan lampu pintar Xiaomi





Halo!

Saya rasa banyak orang yang tertarik dengan rumah pintar atau hanya penataan teknologi rumah mereka, memikirkan tentang sistem pencahayaan "atmosfer" dan non-standar.



Salah satu cara menerangi ruangan dengan cara yang "tidak biasa" sambil menonton film ditawarkan oleh Philips dengan teknologi Ambilight yang terpasang pada TV merek yang sangat canggih .



Dalam artikel ini, Anda akan mengetahui implementasi Ambilight dengan bohlam pintar Xiaomi Yeelight!



Tentang Ambilight



Siapa yang tidak tahu - Teknologi Ambilight adalah lampu latar yang terpasang pada TV, yang menganalisis gambar berwarna dari bingkai di layar TV dan mereproduksi cahaya yang tersebar di sekeliling TV.







Kelebihan Ambilight:



  • , ;
  • ;
  • , .


Secara umum, Ambilight adalah teknologi yang cukup menarik, dan konfirmasi dari fakta ini adalah adanya berbagai macam opsi untuk penerapan "kerajinan tangan" -nya, yang disajikan di Internet. Namun, mereka sebagian besar didasarkan pada penggunaan strip LED beralamat yang direkatkan ke bagian belakang penutup TV / monitor / laptop. Untuk implementasi seperti itu, diperlukan setidaknya pengontrol eksternal fisik yang bertanggung jawab untuk mengontrol LED. Ini membutuhkan pengetahuan khusus dari seseorang yang ingin memasang sistem seperti itu. Oleh karena itu, sebagai alternatif, saya mengusulkan versi lampu latar yang paling "sederhana" dan agak sederhana dengan menggunakan lampu pintar.



Apa sajakah lampu pintar ini?



Untuk membuat opsi iluminasi ini, Anda memerlukan perangkat penerangan merek Yeelight (anak perusahaan Xiaomi) atau Xiaomi (tetapi hanya yang menyebutkan Yeelight pada namanya). Artinya perangkat tersebut tertanam dalam ekosistem rumah pintar Xiaomi dan dikontrol melalui aplikasi Yeelight.







Menurut pendapat saya, lampu latar adaptif bukanlah hal yang membuat seseorang berlari untuk membeli lampu Xiaomi pintar (omong-omong, dengan jumlah uang yang besar). Namun, bagi saya, ini adalah kesempatan bagus untuk memperluas fungsionalitas lampu yang ada di rumah. Bagaimanapun, sebagai pemilik dua lampu Xiaomi, saya dapat mengatakan bahwa setelah dua bulan menggunakannya, saya hanya mendapat kesan yang menyenangkan.



Aplikasi Yeelight memainkan peran penting dalam implementasi proyek ini, karena memiliki satu parameter yang berguna - Mode pengembang .





Di update terbaru, namanya diubah menjadi "Kontrol LAN"



Ekosistem rumah pintar modern didasarkan pada pertukaran data antar perangkat menggunakan protokol wi-fi. Setiap perangkat pintar memiliki modul wi-fi built-in yang memungkinkan Anda untuk terhubung ke jaringan nirkabel lokal. Berkat ini, perangkat dikontrol melalui layanan cloud rumah pintar. Namun, mode Pengembang memungkinkan Anda untuk berkomunikasi dengan perangkat secara langsung dengan mengirimkan permintaan ke alamat IP yang dialokasikan ke perangkat (alamat perangkat dapat ditemukan di aplikasi Yeelight di informasi perangkat). Mode ini menjamin penerimaan data dari perangkat yang berada di jaringan lokal yang sama dengan lampu pintar. Situs web Yeelight memiliki demo kecil dari fungsionalitas mode pengembang.



Berkat opsi ini, dimungkinkan untuk mengimplementasikan fungsi pencahayaan adaptif dan menyematkannya di pemutar open source.



Definisi fungsional



Posting selanjutnya akan dikhususkan untuk kesulitan apa (dan cara untuk menyelesaikannya) yang mungkin dihadapi seorang insinyur ketika dia berpikir untuk merancang hal seperti itu, serta kemajuan umum dalam implementasi rencana.



Jika Anda tertarik secara eksklusif pada program yang sudah jadi, maka Anda bisa langsung ke item "Bagi mereka yang hanya ingin menggunakan pemain yang sudah jadi."



Pertama-tama, mari kita putuskan tugas yang harus diselesaikan oleh proyek yang sedang dikembangkan. Poin utama TOR untuk proyek ini:



  • Anda perlu mengembangkan fungsionalitas yang memungkinkan Anda mengubah parameter secara dinamis (warna atau kecerahan / suhu cahaya jika menggunakan perangkat tanpa LED rgb) dari lampu pintar, tergantung pada gambar saat ini di jendela pemutar media.
  • .
  • , «» .
  • .
  • .




,



, jar Before you start README .









Tahap awal pengembangan proyek akan menjadi definisi pemain untuk menanamkan fungsi dan perpustakaan untuk komunikasi dengan lampu pintar.



Pilihan saya jatuh pada pemutar vlcj dan perpustakaan Yapi , yang ditulis dalam bahasa Jawa . Maven digunakan sebagai alat membangun .



Vlcj adalah kerangka kerja yang memungkinkan Anda menyematkan pemutar VLC asli ke dalam aplikasi Java, serta mengelola siklus hidup pemutar melalui kode java. Penulis kerangka juga memiliki versi demo pemutar , yang hampir sepenuhnya mengulangi antarmuka dan fungsionalitas pemutar VLC. Versi paling stabil dari pemain saat ini adalah versi 3. Ini akan digunakan dalam proyek.





Antarmuka pemutar VLCJ dengan jendela tambahan yang terbuka



Keuntungan dari pemutar vlcj:



  • sejumlah besar format video yang didukung, yang merupakan fitur lama dari pemutar VLC;
  • Java sebagai PL, yang memungkinkan Anda untuk membuka pemutar di sejumlah besar sistem operasi (dalam hal ini, kami hanya dibatasi oleh penerapan pemutar VLC, yang terkait erat dengan aplikasi java).


Kekurangan:



  • desain pemain yang ketinggalan zaman, yang diselesaikan dengan implementasi antarmuka sendiri;
  • Sebelum menggunakan program ini, Anda perlu menginstal pemutar VLC dan Java versi 8 atau lebih tinggi, yang jelas merupakan kekurangan.


Penggunaan Yapi sebagai perpustakaan untuk menghubungkan dengan gadget pintar Yeelight dapat dibenarkan terutama oleh kesederhanaan, dan kedua, oleh kelangkaan solusi yang sudah jadi. Saat ini belum banyak alat pihak ketiga untuk pengendalian lampu pintar, terutama yang berbahasa Jawa.



Kerugian utama dari pustaka Yapi adalah tidak ada versinya yang ada di repositori Maven, jadi sebelum mengkompilasi kode proyek, Anda perlu menginstal Yapi secara manual ke dalam repositori lokal (seluruh penginstalan dijelaskan dalam file README di repositori).



Algoritma analisis gambar



Prinsip pencahayaan dinamis akan didasarkan pada analisis warna periodik dari bingkai saat ini.



Sebagai hasil dari tahap coba-coba, prinsip analisis citra berikut dikembangkan:



Pada frekuensi yang ditentukan, program mengambil tangkapan layar dari pemutar media dan menerima objek dari kelas BufferedImage. Selanjutnya, dengan algoritme bawaan tercepat, gambar asli diubah ukurannya menjadi 20x20 piksel.



Ini diperlukan untuk kecepatan algoritme, sehingga kami dapat mengorbankan beberapa akurasi dalam menentukan warna. Ini juga diperlukan untuk meminimalkan ketergantungan waktu pemrosesan gambar pada resolusi file media saat ini.



Selanjutnya, algoritme membagi gambar yang dihasilkan menjadi empat zona "dasar" (kiri atas, kiri bawah, dll.) Berukuran 10x10 piksel.





Zona "Dasar"



Mekanisme ini diterapkan untuk memastikan analisis independen dari zona gambar yang berbeda, yang memungkinkan Anda untuk meletakkan perangkat pencahayaan di tempat tertentu dalam ruangan di masa mendatang dan menunjukkan zona gambar mana yang perlu "dilacak". Saat digunakan dengan program multi-lampu, fungsi ini membuat pencahayaan dinamis jauh lebih atmosferik.



Kemudian, untuk setiap area gambar, warna rata-rata dihitung dengan menghitung rata-rata aritmatika secara terpisah untuk tiga komponen warna (merah, hijau, biru) dari setiap piksel dan menyusun data yang dihasilkan menjadi satu nilai warna.



Berkat empat nilai yang dihasilkan, kami dapat:



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

    (r0,2126+g0.7152+b0,0722)/255100

    r, g, b – //
  • :

    {0,rb,(r-b)/255100,r>b

    r, b – /


Untuk mekanisme penghitungan parameter gambar yang efisien dan dapat diskalakan, semua data tambahan (bukan zona "dasar", suhu, dan kecerahan warna) dihitung "dengan malas", yaitu sesuai kebutuhan.



Semua kode pemrosesan gambar cocok dengan satu kelas 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;
        }
    }
}




Untuk mencegah kerlipan lampu yang sering mengiritasi mata, diperkenalkan ambang batas untuk mengubah parameter. Misalnya, lampu akan mengubah nilai kecerahan hanya jika adegan saat ini dalam film lebih dari 10 persen lebih terang dari yang sebelumnya.



Perbandingan dengan metode analisis lain



Anda mungkin bertanya, "Mengapa tidak memperkecil gambar menjadi 2x2 piksel dan menghitung nilai yang dihasilkan?" ...

Jawabannya adalah sebagai berikut: “Berdasarkan eksperimen saya, algoritme untuk menentukan warna rata-rata dengan mengurangi ukuran gambar (atau zonanya) terbukti kurang stabil dan kurang dapat diandalkan (terutama saat menganalisis area gelap gambar) dibandingkan algoritme yang didasarkan pada penentuan mean aritmatika semua piksel " .



Beberapa metode telah dicoba untuk mengubah ukuran gambar. Dimungkinkan untuk menggunakan pustaka openCV untuk pekerjaan yang lebih serius dengan gambar, tetapi saya menganggap ini terlalu rekayasa untuk tugas ini. Sebagai perbandingan, di bawah ini adalah contoh menentukan warna menggunakan penskalaan cepat bawaan dari kelas BufferedImage dan menghitung rata-rata aritmatika. Saya pikir komentar tidak berguna.







Konfigurasi



Saat ini, program tersebut dikonfigurasi menggunakan file json. JSON.simple digunakan sebagai pustaka untuk mengurai file konfigurasi .



File Json harus diberi nama "config.json" dan ditempatkan di folder yang sama dengan program untuk deteksi konfigurasi otomatis, jika tidak, ketika fungsi kecerahan adaptif diaktifkan, program akan meminta Anda untuk menentukan sendiri file konfigurasi dengan membuka jendela pemilihan file. Dalam file tersebut, Anda harus menentukan alamat ip perangkat pencahayaan, zona gambar yang "dipantau" untuk setiap perangkat, koefisien kecerahan dan suhu warna, atau periode pemasangan otomatisnya (yang akan dijelaskan di paragraf berikutnya). Aturan untuk mengisi file json dijelaskan di file README proyek.





Semua perubahan di antarmuka (tombol lampu). Saat tombol ditekan, file konfigurasi yang tersedia akan diterapkan atau jendela untuk pemilihannya akan terbuka



Koefisien diperlukan untuk pengaturan analisis gambar yang lebih tepat, misalnya, untuk membuat lampu sedikit lebih gelap atau, sebaliknya, lebih terang. Semua parameter ini opsional. Satu-satunya parameter yang diperlukan di sini adalah nilai alamat ip dari perlengkapan pencahayaan.



Pengaturan peluang otomatis



Selain itu, program ini mengimplementasikan fungsi penyesuaian otomatis koefisien tergantung pada penerangan ruangan saat ini. Ini terjadi seperti ini: webcam laptop Anda mengambil snapshot lingkungan pada frekuensi yang dipilih, menganalisis kecerahannya menggunakan algoritme yang telah dijelaskan, dan kemudian menyetel koefisien sesuai dengan rumus:

l=1+x/100

di mana x adalah kecerahan ruangan saat ini sebagai persentase.



Fungsi ini diaktifkan dengan menulis tag khusus di file konfigurasi.



Contoh cara kerja fungsionalitas





Kesimpulan



Sebagai hasil dari pemecahan masalah, fungsionalitas dikembangkan yang memungkinkan Anda menggunakan lampu pintar Yeelight sebagai lampu latar adaptif file media. Selain itu, fungsi analisis iluminasi ruangan saat ini telah diterapkan. Semua kode sumber tersedia dari tautan di repositori github saya .



Terima kasih atas perhatiannya!



PS Saya akan senang dengan penambahan, pernyataan dan indikasi kesalahan.



All Articles