أضف Ambilight إلى المشغل باستخدام مصابيح Xiaomi الذكية





مرحبا!

أعتقد أن العديد من المهتمين بالمنزل الذكي أو ببساطة الترتيب التكنولوجي لمنزلهم ، فكروا في "الغلاف الجوي" ونظام الإضاءة غير القياسي.



طريقة واحدة لإضاءة غرفة بطريقة "غير عادية" أثناء مشاهدة الأفلام تقدمها Philips بتقنية Ambilight المدمجة في أجهزة التلفزيون المتطورة للغاية للعلامة التجارية.



في هذه المقالة ، سوف تكتشف تنفيذ Ambilight مع المصابيح الذكية Xiaomi Yeelight!



حول Ambilight



من لا يعرف - تقنية Ambilight عبارة عن إضاءة خلفية مدمجة في أجهزة التلفزيون ، والتي تحلل الصورة الملونة للإطار على شاشة التلفزيون وتعيد إنتاج الضوء المنتشر حول محيط التلفزيون.







إيجابيات Ambilight:



  • , ;
  • ;
  • , .


بشكل عام ، تعد Ambilight تقنية مثيرة للاهتمام إلى حد ما ، وتأكيد هذه الحقيقة هو وجود عدد كبير من الخيارات المختلفة لتنفيذ "الحرف اليدوية" ، المعروضة على الإنترنت. ومع ذلك ، فهي تعتمد بشكل كبير على استخدام شريط LED قابل للعنونة يتم لصقه على الجزء الخلفي من غطاء التلفزيون / الشاشة / الكمبيوتر المحمول. لمثل هذا التنفيذ ، من الضروري أن يكون لديك على الأقل وحدة تحكم خارجية مادية مسؤولة عن التحكم في المصابيح. هذا يتطلب معرفة محددة من الشخص الذي يريد تثبيت مثل هذا النظام. لذلك ، كبديل ، أقترح الإصدار الأكثر "بروجر" والأكثر بساطة من مثل هذه الإضاءة الخلفية باستخدام المصابيح الذكية.



ما هي هذه المصابيح الذكية؟



لإنشاء خيار الإضاءة هذا ، ستحتاج إلى أي جهاز إضاءة ماركة Yeelight (شركة تابعة لشركة Xiaomi) أو Xiaomi (ولكن فقط تلك التي تذكر Yeelight في الاسم). هذا يعني أن الجهاز مضمن في النظام البيئي للمنزل الذكي Xiaomi ويتم التحكم فيه من خلال تطبيق Yeelight.







في رأيي ، الإضاءة الخلفية التكيفية ليست هي الشيء الذي سيركض شخص ما من أجله لشراء مصباح Xiaomi الذكي (مقابل مبلغ كبير من المال ، بالمناسبة). ومع ذلك ، بالنسبة لي ، فهذه فرصة جيدة لتوسيع وظائف المصباح الموجود في المنزل. على أي حال ، بصفتي مالكًا لمصابيح XIAOMI ، يمكنني القول أنه بعد شهرين من استخدامها ، لدي فقط انطباعات ممتعة.



يلعب تطبيق Yeelight دورًا مهمًا في تنفيذ هذا المشروع ، لأنه يحتوي على معلمة واحدة مفيدة - وضع المطور .





في آخر التحديثات ، تمت إعادة تسميته إلى "LAN Control"



يعتمد النظام البيئي للمنزل الذكي الحديث على تبادل البيانات بين الأجهزة عبر بروتوكول wi-fi. يحتوي كل جهاز ذكي على وحدة واي فاي مدمجة تتيح لك الاتصال بشبكة لاسلكية محلية. بفضل هذا ، يتم التحكم في الجهاز من خلال الخدمة السحابية للمنزل الذكي. ومع ذلك ، يسمح لك وضع Developer بالتواصل مع الجهاز مباشرة عن طريق إرسال الطلبات إلى عنوان IP المخصص للجهاز (يمكن العثور على عنوان الجهاز في تطبيق Yeelight في معلومات الجهاز). يضمن هذا الوضع استقبال البيانات من الأجهزة الموجودة في نفس الشبكة المحلية مثل المصباح الذكي. يحتوي موقع Yeelight على عرض توضيحي صغير لوظيفة وضع المطور.



بفضل هذا الخيار ، من الممكن تنفيذ وظيفة الإضاءة التكيفية وتضمينها في مشغل المصدر المفتوح.



التعريف الوظيفي



سيتم تخصيص وظيفة أخرى للصعوبات (وطرق حلها) التي قد يواجهها المهندس عندما يفكر في تصميم شيء من هذا القبيل ، بالإضافة إلى التقدم العام في تنفيذ الخطة.



إذا كنت مهتمًا فقط ببرنامج جاهز ، فيمكنك الانتقال مباشرةً إلى العنصر "لأولئك الذين يريدون فقط استخدام لاعب جاهز".



بادئ ذي بدء ، دعنا نقرر المهام التي يجب أن يحلها المشروع قيد التطوير. النقاط الرئيسية لاختصاصات هذا المشروع:



  • من الضروري تطوير وظائف تسمح لك بتغيير المعلمات ديناميكيًا (لون أو سطوع / درجة حرارة الضوء في حالة استخدام جهاز بدون مصابيح rgb LED) للمصباح الذكي ، اعتمادًا على الصورة الحالية في نافذة مشغل الوسائط.
  • .
  • , «» .
  • .
  • .




,



, jar Before you start README .









ستكون المرحلة الأولى من تطوير المشروع هي تحديد المشغل لتضمين وظيفة ومكتبة للتواصل مع مصباح ذكي.



وقع خياري على مشغل vlcj ومكتبة يابي المكتوبة بلغة جافا . تم استخدام Maven كأداة بناء .



Vlcj هو إطار يسمح لك بتضمين مشغل VLC أصلي في تطبيق Java ، وكذلك إدارة دورة حياة اللاعب من خلال كود java. يمتلك مؤلف الإطار أيضًا إصدارًا تجريبيًا من المشغل ، والذي يكرر بشكل شبه كامل واجهة ووظائف مشغل VLC. الإصدار الأكثر استقرارًا من المشغل حاليًا هو الإصدار 3. وسيتم استخدامه في المشروع.





واجهة مشغل Vlcj مع نوافذ إضافية مفتوحة



مزايا مشغل vlcj:



  • عدد كبير من تنسيقات الفيديو المدعومة ، وهي ميزة طويلة الأمد لمشغل VLC ؛
  • Java كـ PL ، والذي يسمح لك بفتح المشغل على عدد كبير من أنظمة التشغيل (في هذه الحالة ، نحن مقيدون فقط بتنفيذ مشغل VLC ، والذي يرتبط ارتباطًا وثيقًا بتطبيق جافا).


سلبيات:



  • تصميم قديم للاعب ، يتم حله من خلال تطبيقه الخاص للواجهة ؛
  • قبل استخدام البرنامج ، تحتاج إلى تثبيت مشغل VLC و Java الإصدار 8 أو أعلى ، وهذا بالتأكيد عيب.


يمكن تبرير استخدام Yapi كمكتبة للاتصال بأدوات Yeelight الذكية في المقام الأول بالبساطة ، وثانيًا ، من خلال ندرة الحلول الجاهزة. في الوقت الحالي ، لا توجد العديد من أدوات الطرف الثالث للتحكم في المصابيح الذكية ، خاصة في لغة Java.



العيب الرئيسي لمكتبة Yapi هو عدم وجود أي من إصداراتها في مستودع Maven ، لذلك قبل تجميع كود المشروع ، تحتاج إلى تثبيت Yapi يدويًا في المستودع المحلي (تم وصف التثبيت بالكامل في ملف README في المستودع).



خوارزمية تحليل الصورة



يعتمد مبدأ الإضاءة الديناميكية على تحليل الألوان الدوري للإطار الحالي.



نتيجة لمرحلة التجربة والخطأ ، تم تطوير المبدأ التالي لتحليل الصور:



عند التردد المحدد ، يأخذ البرنامج لقطة شاشة لمشغل الوسائط ويستقبل كائنًا من فئة BufferedImage. بعد ذلك ، باستخدام أسرع خوارزمية مضمنة ، يتم تغيير حجم الصورة الأصلية إلى 20 × 20 بكسل.



هذا ضروري لسرعة الخوارزمية ، من أجلها يمكننا التضحية ببعض الدقة في تحديد اللون. من الضروري أيضًا تقليل اعتماد وقت معالجة الصورة على دقة ملف الوسائط الحالي.



بعد ذلك ، تقسم الخوارزمية الصورة الناتجة إلى أربع مناطق "أساسية" (أعلى اليسار ، أسفل اليسار ، إلخ) بحجم 10x10 بكسل.





مناطق "أساسية"



يتم تنفيذ هذه الآلية لتوفير تحليل مستقل لمناطق الصورة المختلفة ، مما يسمح لك بوضع جهاز الإضاءة في مكان معين في الغرفة في المستقبل وتحديد منطقة الصورة التي يحتاج "تتبعها". عند استخدامها مع برنامج متعدد المصابيح ، فإن هذه الوظيفة تجعل الإضاءة الديناميكية أكثر جاذبية.



بعد ذلك ، لكل مساحة من الصورة ، يتم حساب متوسط ​​اللون عن طريق حساب المتوسط ​​الحسابي بشكل منفصل لمكونات الألوان الثلاثة (الأحمر والأخضر والأزرق) لكل بكسل وترتيب البيانات الناتجة في قيمة لون واحدة.



بفضل القيم الأربع الناتجة ، يمكننا:



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

    (ص0.2126+ز0.7152+ب0.0722)/255100

    r, g, b – //
  • :

    {0،صب،(ص-ب)/255100،ص>ب

    r, b – /


للحصول على آليات فعالة وقابلة للتطوير لحساب معلمات الصورة ، يتم حساب جميع البيانات الإضافية (وليس المناطق "الأساسية" ودرجة الحرارة وسطوع الألوان) "كسول" ، أي كما هو مطلوب.



كل كود معالجة الصور يلائم فئة واحدة 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 بالمائة من السابق.



مقارنة مع طريقة تحليل أخرى



قد تسأل ، "لماذا لا يتم تصغير الصورة إلى 2 × 2 بكسل فقط وتحسب القيم الناتجة؟" ...

ستكون الإجابة على النحو التالي: "بناءً على تجاربي ، أثبتت خوارزمية تحديد متوسط ​​اللون عن طريق تقليل حجم الصورة (أو مناطقها) أنها أقل ثباتًا وأقل موثوقية (خاصة عند تحليل المناطق المظلمة من الصورة) من الخوارزمية القائمة على تحديد المتوسط ​​الحسابي لجميع وحدات البكسل " .



تم تجربة عدة طرق لتغيير حجم الصور. كان من الممكن استخدام مكتبة OpenCV للقيام بعمل أكثر جدية مع الصورة ، لكنني اعتبرت أن هذا يمثل هندسة مفرطة لهذه المهمة. للمقارنة ، يوجد أدناه مثال لتعريف لون باستخدام القياس السريع المدمج لفئة BufferedImage وحساب المتوسط ​​الحسابي. أعتقد أن التعليقات لا لزوم لها.







التكوين



في الوقت الحالي ، يتم تكوين البرنامج باستخدام ملف json. تم استخدام JSON.simple كمكتبة لتحليل ملف التكوين .



يجب تسمية ملف Json باسم "config.json" ووضعه في نفس المجلد مع البرنامج لاكتشاف التكوين التلقائي ، وإلا ، عند تمكين وظيفة السطوع التكيفي ، سيطالبك البرنامج بتحديد ملف التكوين بنفسك عن طريق فتح نافذة اختيار الملف. في الملف ، يجب تحديد عناوين IP لأجهزة الإضاءة ، ومناطق الصور "المراقبة" لكل جهاز ، ومعاملات السطوع ودرجة حرارة اللون ، أو فترة التثبيت التلقائي (والتي سيتم وصفها في الفقرة التالية). تم توضيح قواعد ملء ملف json في ملف README الخاص بالمشروع.





كل التغييرات في الواجهة (زر الضوء). عند الضغط على الزر ، سيتم تطبيق ملف التكوين المتاح أو سيتم فتح نافذة



لتحديده.المعامِلات ضرورية لإعداد أكثر دقة لتحليل الصورة ، على سبيل المثال ، لجعل المصباح أغمق قليلاً أو ، على العكس ، أفتح. كل هذه المعلمات اختيارية. المعلمة الوحيدة المطلوبة هنا هي قيم عناوين IP لتركيبات الإضاءة.



الإعداد التلقائي للاحتمالات



يقوم البرنامج أيضًا بتنفيذ وظيفة الضبط التلقائي للمعاملات اعتمادًا على الإضاءة الحالية للغرفة. يحدث هذا على النحو التالي: تلتقط كاميرا الويب لجهاز الكمبيوتر المحمول الخاص بك بتردد محدد صورة للبيئة ، وتحلل سطوعها وفقًا للخوارزميات الموصوفة بالفعل ، ثم تحدد المعامل وفقًا للصيغة:

ل=1+x/100

حيث x هو سطوع الغرفة الحالي كنسبة مئوية.



يتم تمكين هذه الوظيفة عن طريق كتابة علامة خاصة في ملف التكوين.



مثال على كيفية عمل الوظيفة





خاتمة



نتيجة لحل المشكلة ، تم تطوير وظيفة تسمح لك باستخدام مصابيح Yeelight الذكية كإضاءة خلفية تكيفية لملفات الوسائط. بالإضافة إلى ذلك ، تم تنفيذ وظيفة تحليل الإضاءة الحالية للغرفة. جميع التعليمات البرمجية المصدر متاحة من رابط في مستودع جيثب الخاص بي .



شكرا لكم جميعا على اهتمامكم!



ملاحظة: سأكون سعيدًا بأي إضافات أو ملاحظات أو دلائل على وجود أخطاء.



All Articles