DIY: Универсальный Ambilight для домашней мультимедиа системы — Атмосвет

Добрый день.

Для своей первой статьи я выбрал одну из самых успешных своих поделок: HDMI-passthrough аналог Ambilight от Philips, далее я будут называть эту композицию «Атмосвет».

Введение

В интернетах не очень сложно найти готовые/открытые решения и статьи как сделать Амбилайт для монитора/телевизора, если ты выводишь картинку с ПК. Но в моей мультимедиа системе вывод картинки на телевизор c ПК занимает только 5% времени использования, большее кол-во времени я играю с игровых консолей, а значит нужно было придумать что-то свое.


Исходные данные:

  • 60" Плазменный телевизор
  • HTPC на базе Asrock Vision 3D 137B
  • Xbox 360
  • PS3
  • PS4
  • WiiU

Большинство устройств используют HDCP для воспроизведения контента даже во время игры.
Требование:

Необходимо обеспечить централизованную поддержку Атмосвета для всех устройств подключенных к телевизору.

Реализация

Я не буду рассказывать, как я прикреплял 4.5м светодиодную ленту к телевизору и что нужно сделать с Arduino, в качестве базы можно использовать эту статью.

Единственный нюанс:
Я заметил, что внизу экрана идут странные мерцания, сначала погрешил на сигнал, перековырял дефликер, изменил ресазинг картинки и еще кучу всего перекопал, стало лучше, но от мерцания не помогло. Стал наблюдать. Оказалось, что мерцание было только в конце ленты и то при ярких сценах. Взяв мультиметр, я замерил напряжение на начале, середине и конце ленты и угадал с причиной мерцаний: в начале ленты было 4.9В( да китайский БП дает напряжение с отклонением, это не существенно), в середине 4.5 в конце 4.22 — Падение напряжение слишком существенно, пришлось решить проблему просто — к середине ленты я подвел питание от бп, провод пустил за телевизором. Помогло мгновенно, какие либо мерцания прекратились вообще.

Захватываем картинку вебкамерой

Первая тестовая версия для обкатки идеи и её визуализации была выбрана через захват картинки через вебкамеру) выглядело это как-то так:
image

Низкая цветопередача и высокий latency показал, что эта реализация не может быть никак использована.

Захват картинки через HDMI


В процессе исследования возможных вариантов была выбрана следующая схема, как сама надежная и бюджетная:
  • Сигнал со всех устройств подается на 5in-1out HDMI свитч, который поддерживает HDCP
  • Выходной сигнал подается на 1in-2out HDMI splitter, который мало того, что поддерживает HDCP, так еще отключайте его на выходе(слава китайцам).
  • Один из выходных сигналов идет на телевизор
  • Другой выходной сигнал идет на HDMI to AV конвертер
  • S-Video сигнал идет на коробочку захвата от ICONBIT
  • Коробочка захвата подключается к вечно работающему HTCP по USB, который подключен к Arduino контроллеру ленте на телевизоре.


Изначально выглядит дико и как костыли, но:
  • Это работает.
  • Сумарно все это дело, заказывая из китая, мне обошлось тысяч в 3-4 тыс. рублей.


Почему я не использовал плату для HDMI захвата? Все просто: самый дешевый вариант и доступный — это Blackmagic Intensity Shuttle, но она не может работать с сигналом 1080p/60fps, только с 1080p/30fps — что не приемлемо, т.к. я не хотел понижать фреймрейт, чтобы можно было захватывать картинку. + это дело стоило в районе 10 тыc. рублей. — что не дешево при неизвестном результате.

Потери на конвертации HDMI to S-video несущественны для захвата цвета в разрешении 46х26 светодиодной подсветки.

Изначально для захвата S-video я пробовал использовать EasyCap( у него много китайских вариаций), но суть в том, что используемый там чип крайне убог, и с ним нельзя работать при помощи openCV.

Единственный минус — выходной сигнал S-Video содержал черные полосы по краям срезающий реальный контент(около 2-5%), выходную картинку с платы захвата я обрезал, чтобы удалить эти полосы, сама потеря изображения в тех областях на практике не сказалась на результате.

Софт

Для меня это была самая интересная часть, т.к. с железками я не очень люблю ковыряться.

Для захвата картинки я использовал openCV и в частности его .NET враппер emgu CV.

Я решил также применить несколько разных техник постобработки изображения и его подготовки, прежде чем отдавать список цветов на контроллер.

Процесс обработки фрейма

1. Получение захваченного фрейма

2. Кроп фрейма, для исключения черных полос

Тут все просто:
frame.ROI = new Rectangle(8, 8, frame.Width - 8, frame.Height - 18 - 8); 

Обрезаем 8 пикселей сверху, 8 справа и 18 снизу.(слева полосы нет)

3. Ресайзим фрейм в разрешение подсветки, незачем нам таскать с собой здоровую картинку

Тоже ничего сложного, делаем это средствами openCV:
frame.Resize(LedWidth — 2*LedSideOverEdge,
LedHeight — LedBottomOverEdge — LedTopOverEdge,
INTER.CV_INTER_LINEAR);
Внимательный читатель заметит, обилие переменных. Дело в том, что у меня рамка телевизора достаточно большая, занимая 1 светодиод по бокам, 1 сверху и 3 снизу, поэтому ресайз делается на светодиоды, которые находятся непосредственно напротив дисплея, а углы мы уже дополняем потом. При ресайзинге мы как раз получаем усредненные цвета, которые должны будут иметь пиксели светодиодов.

4. Выполняем мапинг светодиодов с отреcайзенного фрейма

Ну тут тоже все просто, тупо проходим по каждой стороне и последовательно заполняем массив из 136 значений цветом светодиодов. Так вышло, что на текущий момент все остальные операции проще выполнять с массивом светодиодов, чем с фреймом, который тяжелее в обработке. Также на будущее я добавил параметр «глубины» захвата(кол-во пикселей от границы экрана, для усреднения цвета светодиода), но в конечном сетапе, оказалось лучше без неё.

5. Выполняем коррекцию цвета (баланс белого/цветовой баланс)

Стены за телевизором у меня из бруса, брус желтый, поэтому нужно компенсировать желтизну.
var blue = 255.0f/(255.0f + blueLevelFloat)*pixelBuffer[k];
var green = 255.0f/(255.0f + greenLevelFloat)*pixelBuffer[k + 1];
var red = 255.0f/(255.0f + redLevelFloat)*pixelBuffer[k + 2];
Вообще я изначально из исходников какого-то опенсорс редактора взял цветовой баланс, но он не менял белый(белый оставался белым), я поменял формулы немного, опечатался, и получил прям то, что нужно: если level компонента цвета отрицательный(я поинмаю как — этого цвета не хватает), то мы добавляем его интенсивность и наоборот. Для моих стен это получилось: RGB(-30,5,85).

В кореркции цвета я также выполняю выравнивание уровня черного(черный приходит где-то на уровне 13,13,13 по RGB), просто вычитая 13 из каждой компоненты.

6. Выполняем десатурацию (уменьшение насыщенности изображения)

В конечном сетапе, я не использую десатурацию, но может в определенный момент понадобится, фактически это делает цвета более «пастельными», как у Филипсовского амбилайта. Код приводить не буду, мы просто конвертим из RGB -> HSL, уменьшаем компоненту Saturation(насыщенность) и возвращаемся обратно уже в RGB.

7. Дефликер

Так уж выходит, что входное изображение «дрожит» — это следствие конвертации в аналоговый сигнал, как я полагаю. Я сначала пытался решить по своему, потом подсмотрел в исходники Defliker фильтра, используемом в VirtualDub, переписал его на C#(он был на С++), понял, что он не работает, ибо он такое впечталение, что борется с мерцаниями между кадрами, в итоге я совместил свое решение и этот дефликер получив что-то странное, но работающее лучше чем ожидалось. Изначальный дефликер работал только с интенсивностью всего фрейма, мне нужно по каждому светодиоду отдельно. Изначальный дефликер сравнивал изменение интенсивности как суммы, мне больше нравится сравнение длинны вектора цвета, Изначальный дефликер сравнивал дельту изменения интенсивности по сравнению с предыдущим кадром, это не подходит, и я переделал на среднюю величину интенсивности в пределах окна предыдущих кадров. И еще много других мелочей, в результате чего от начального дефликера мало что осталось.
Основная идея: исходя из средней интенсивности предыдущих кадров, выполнять модификацию текущего кадра, если его интенсивность не выше определенного порога (у меня этот порог в конечном сетапе 25), если порог преодолевается, то производится сброс окна, без модификации.
Немного модифицированный (для читаемости вне контекста) код моего дефликера:
Array.Copy(_leds, _ledsOld, _leds.Length);  
       for (var i = 0; i < _leds.Length; i++)  
       {  
         double lumSum = 0;  
         // Calculate the luminance of the current led.  
         lumSum += _leds[i].R*_leds[i].R;  
         lumSum += _leds[i].G*_leds[i].G;  
         lumSum += _leds[i].B*_leds[i].B;  
         lumSum = Math.Sqrt(lumSum);  
         // Do led processing  
         var avgLum = 0.0;  
         for (var j = 0; j < LedLumWindow; j++)  
         {  
           avgLum += _lumData[j, i];  
         }  
         var avg = avgLum/LedLumWindow;  
         var ledChange = false;  
         if (_strengthcutoff < 256 && _lumData[0, i] != 256 &&  
           Math.Abs((int) lumSum - avg) >= _strengthcutoff)  
         {  
           _lumData[0, i] = 256;  
           ledChange = true;  
         }  
         // Calculate the adjustment factor for the current led.  
         var scale = 1.0;  
         int r, g, b;  
         if (ledChange)  
         {  
           for (var j = 0; j < LedLumWindow; j++)  
           {  
             _lumData[j, i] = (int) lumSum;  
           }  
         }  
         else  
         {  
           for (var j = 0; j < LedLumWindow - 1; j++)  
           {  
             _lumData[j, i] = _lumData[j + 1, i];  
           }  
           _lumData[LedLumWindow - 1, i] = (int) lumSum;  
           if (lumSum > 0)  
           {  
             scale = 1.0f/((avg+lumSum)/2);  
             var filt = 0.0f;  
             for (var j = 0; j < LedLumWindow; j++)  
             {  
               filt += (float) _lumData[j, i]/LedLumWindow;  
             }  
             scale *= filt;  
           }  
           // Adjust the current Led.  
           r = _leds[i].R;  
           g = _leds[i].G;  
           b = _leds[i].B;  
           // save source values  
           var sr = r;  
           var sg = g;  
           var sb = b;  
           var max = r;  
           if (g > max) max = g;  
           if (b > max) max = b;  
           double s;  
           if (scale*max > 255) s = 255.0/max;  
           else s = scale;  
           r = (int) (s*r);  
           g = (int) (s*g);  
           b = (int) (s*b);  
           // keep highlight  
           double k;  
           if (sr > _lv)  
           {  
             k = (sr - _lv)/(double) (255 - _lv);  
             r = (int) ((k*sr) + ((1.0 - k)*r));  
           }  
           if (sg > _lv)  
           {  
             k = (sg - _lv)/(double) (255 - _lv);  
             g = (int) ((k*sg) + ((1.0 - k)*g));  
           }  
           if (sb > _lv)  
           {  
             k = (sb - _lv)/(double) (255 - _lv);  
             b = (int) ((k*sb) + ((1.0 - k)*b));  
           }  
           _leds[i] = Color.FromArgb(r, g, b);  
         }  
         /* Temporal softening phase. */  
         if (ledChange || _softening == 0) continue;  
         var diffR = Math.Abs(_leds[i].R - _ledsOld[i].R);  
         var diffG = Math.Abs(_leds[i].G - _ledsOld[i].G);  
         var diffB = Math.Abs(_leds[i].B - _ledsOld[i].B);  
         r = _leds[i].R;  
         g = _leds[i].G;  
         b = _leds[i].B;  
         int sum;  
         if (diffR < _softening)  
         {  
           if (diffR > (_softening >> 1))  
           {  
             sum = _leds[i].R + _leds[i].R + _ledsOld[i].R;  
             r = sum/3;  
           }  
         }  
         if (diffG < _softening)  
         {  
           if (diffG > (_softening >> 1))  
           {  
             sum = _leds[i].G + _leds[i].G + _ledsOld[i].G;  
             g = sum/3;  
           }  
         }  
         if (diffB < _softening)  
         {  
           if (diffB > (_softening >> 1))  
           {  
             sum = _leds[i].B + _leds[i].B + _ledsOld[i].B;  
             b = sum/3;  
           }  
         }  
         _leds[i] = Color.FromArgb(r, g, b);  
       }  

Пусть _leds — массив светодиодов класса Color, _ledsOld — значения кадра до конвертации, LedLumWindow — ширина окна предыдущих кадров, для оценки среднего изменения интенсивности, в конечном сетапе окно у меня было 100, что примерно при 30кад/с равняется 3-секундам. _lumData — массив значения интенсивности предыдущих кадров.

В конечном итоге данный механизм дал еще приятные неожиданные последствия на картинку, сложно описать как это воспринимается визуально, но он делает темнее где надо и ярче где надо, словно динамический контраст. Цель дефликера в итоге получилась широкая, не только устранение мерцаний, но и общее уравновешивание выводимого цвета, как и по компонентам, так и по времени в пределах окна.

8. Сглаживание светодиодов по соседям.

Вообще в конечном сетапе, сглаживание мне не очень понравилось, и я его отключил, но в некоторых случаях может пригодиться. Тут мы просто усредняем цвет каждого светодиода по его соседним.
var smothDiameter = 2*_smoothRadius + 1;  
       Array.Copy(_leds, _ledsOld, _leds.Length);  
       for (var i = 0; i < _ledsOld.Length; i++)  
       {  
         var r = 0;  
         var g = 0;  
         var b = 0;  
         for (var rad = -_smoothRadius; rad <= _smoothRadius; rad++)  
         {  
           var pos = i + rad;  
           if (pos < 0)  
           {  
             pos = _ledsOld.Length + pos;  
           }  
           else if (pos > _ledsOld.Length - 1)  
           {  
             pos = pos - _ledsOld.Length;  
           }  
           r += _ledsOld[pos].R;  
           g += _ledsOld[pos].G;  
           b += _ledsOld[pos].B;  
         }  
         _leds[i] = Color.FromArgb(r/smothDiameter, g/smothDiameter, b/smothDiameter);  
       }  



9. Сохраняем текущий стейт, чтобы тред отправки пакетов схватил и отправил его на контроллер подсветки.

Я умышленно разделил процесс обработки кадров и отправки пакетов на контроллер: пакеты отправляются раз в определенный интервал(у меня это 40мс), чтобы ардуино спела обработать предыдущий, ибо чаще чем 30мс она захлебывается, таким образом выходит, что мы не зависим напрямую от частоты кадров захвата и не мешаем тому процессу(а ведь отправка пакета тоже тратит время).

Немного про ардуино

Нельзя просто так взять и отправить по сериалу здоровенный пакет на ардуино, ибо онв ыйдет за пределы дефолтного буфера HardwareSerial и ты потеряешь его конец.
Решается это довольно просто: выставляем значение размера буфера HardwareSerial достаточного размера, чтобы влезал весь отправляемый пакет с массивом цветов, для меня это 410.

UI

Сам софт был реализован в виде win службы, чтобы настраивать все параметры + включать/отключать я сделал Web UI, который связывался с службой через WebService на службе. Итоговый интерфейс на экране мобильника выглядит так:
image

Сейчас планирую прикрутить голосовое управление через Kinect for Windows подключенном к HTCP.

Результат

В итоге результат оправдал все ожидания, и теперь играя в игры на консолях я получаю еще больше погружения в атмосферу игры.

Как общий результат работы я записал видео с работой атмосвета по моей схеме:

Испытуемый образец 1: Pacific Rim, сцена битвы в Шанхае, этот фильм хорошо подходит для тестирования и демонстрации, много ярких сцен и вспышек, ударов молнии и т.д.:



Испытуемый образец 2: Какой-то ролик из MLP, слитый с ютуба, очень хорошо подходит для теста сцен с яркими цветами(мне понравились полосы), а также быстро сменяющихся сцен(под конец виде можно разглядеть последствия задержки, видных только на видео, при реальном просмотре этого не заметно, пробовал измерить задержку по видео — получилось 10-20мс):



И на последок стоит заметить про потребление ресурсов от HTPC:
HTPC у меня ASRock Vision 3D на i3, служба атмосвета отжирает 5-10% CPU и 32MB RAM.

Спасибо за внимание, очень надеюсь, что кому-нибудь моя статья поможет.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 34

    +2
    Вполне себе так ничего и выглядит, но система слишком сложная.
    Как альтернативу этому, я бы взял сей девайс www.cyborggaming.com/prod/ambx.htm (100у.е. пара).
    Конечно вашему творению он уступает по общей эффектности, но все же.
      +3
      Результат великолепный, наверное самый эффектный, что я видел среди DIY-проектов такого плана. И за подробное описание алгоритма спасибо. Плюс это первая реализация на HDMI, которая мне попадалась. Классный обход HDCP. Жаль, что все равно требуется полноценный компьютер для обработки. Много ресурсов требуется для захвата и обсчета?

      По сравнению с этим монстрячеством (hdmi-свитч — hdmi-хаб — конвертер — захват — комп — ардуина — светодиоды) инженеры Филипса просто читеры — у них-то все нужные данные о картинке прямо внутри телевизора есть.

      Реквестирую ссылку на MLP-ролик из второго видео.
        0
        Если я не путаю, то оригинал ролика вот это видео

        Про ресурсы: на i3-370M отжирает 5-10% CPU
          0
          Было бы забавно взять чуть более мощный МК с видеовходом и сделать всё на нём. Можно было бы здорово уменьшить общую задержку и избавиться от HTPC в цепочке.
            0
            У меня есть желание попробовать внедрить туда Rasberry Pi(вместо HTCP + Arduino), если получится — напишу статью с описанием результата и подводных камней.
              0
              Вот у меня на днях будет Cubietruck, но я слабо представляю себе как на него послать HDMI внутрь.
            +1
            www.keiang.de/Content-pid-32.html — это лучшая реализация из всех что я пока видел (для нее не нужен полноценный комп для обработки сигнала т.к. оно само по сути является компом). Я даже связывался с автором данного девайса и он предложил мне его купить за 200 евро.
              0
              Это очень круто. Я после прочтения этого поста как раз думал, взять бы специально обученный АЦП, данные с него в реальном времени прогонять через DSP и потом окончательно обрабатывать контроллером. Это далеко за пределами моих электронных скилов, так теоретически размышлял как бы избавиться от компьютера. А в этом проекте ровно так и сделано. И вообще уровень очень высокий. Автор одиночка?
                0
                Да автор одиночка. Но думаю если вы с ним свяжетесь то он будет не против собрать для вас один такой девайс и продать вам его.
            0
            Как все сложно!
            Когда уже появится девайс вида 1-ин-1-аут в который просто подключаешь ленту?:)
            Вопрос риторический, но из этого вполне можно сделать хороший стартап, по-моему.
              +1
              Когда Филипс разрешит =)
                0
                Так вот же — store.lightpack.tv/products/lightpack
                На Хабре они ещё в 2011-2012 первые пробные версии описывали.

                Там, правда, «любую ленту» не подключишь, если вам именно это надо (было).
                  0
                  Ну я это и имел ввиду, например хдми мама-папа, который любой источник ловит и ленту подсвечивает.
                    0
                    Был в полной уверенности, что Lightpack тоже картинку «на проводе» захватывает. А ему, оказывается, своя программка нужна на источнике сигнала…
                  0
                  Очень круто. Вы меня вдохновили на следующий DIY проект. :)
                    –2
                    Все хорошо, но толстые рамки телика портят картинку :( Лучше бы смотрелось что-нибудь безрамочное.
                      0
                      Очень здорово!
                      Меня всегда тоже расстраивало ограничение существующих решений, что картинка только с ПК.
                      p.s. Но я предпочитаю просто иметь телевизор филипс с эмбилайт :)
                      p.p.s. Не планируете ли вы выпустить ваше решение в виде готового комплекта или устройства?
                        0
                        Я считаю, что как готовый продукт — это слишком большие костыли, а чтобы спроектировать отдельное устройство моих знаний в электронике пока не хватает.
                        0
                        Кстати есть проект github.com/gkaindl/ambi-tv
                        нужно попробовать iconBIT TV-HUNTER STUDIO подключить к Raspberry PI
                        не факт что будет работать

                        Странно что всего 4 тыс рублей вышло, как считали?

                        Подскажите какой конкретно 1in-2out HDMI splitter использовали и поддерживает ли он 1.3 HDMI 3d
                          +1
                          Сплиттер использовал вот этот. В описании написано, что поддерживает, сам не тестил 3d, ибо телевизор у меян обычный.

                          Про стоимость, покупал я это во времена, когда доллар был меньше(в ноябре):
                          27$ за конвертер HDMI to AV
                          22$ за 1in-2out сплиттер HDMI
                          1 000 руб за ICONBIT Tv-Hunter Studio
                          1 634,30 руб — 5м ленты светодиодной
                          Arduino брал за ~12$ с dx.com

                          И отдельно потом докупил:
                          18$ за свитч 5in-1out HDMI

                          Кроме ICONBIT Tv-Hunter Studio все заказывал с ebay/dx.com, его мне пришлось докупать в московском магазине ближайшем, после того, как мне не удалось поженить OpenCV и EasyCap.

                          Про Rasberry Pi: есть в планах проверить работу с ним (Никогда еще с ним не имел дело и ожидаю, когда он до меня доедет почтой).
                          Но я уже находил в интернетах, что даже для различных чипов EasyCap есть костыли, чтобы на RasberryPi захватывать сигнал.

                          Проект по ссылке очень любопытный, спасибо за ссылку!
                            0
                            Для EasyCap давно есть драйвера в Линуксовом ядре версий 3.чтототам и выше. А на Raspberry просто других не используют =) Так что единственная проблема, которая может возникнуть и которую будет трудно решить — глючные драйвера USB на Raspberry Pi, глюки ещё иногда проскакивают, к сожалению =(
                          0
                          Добавьте, пожалуйста, в пост фотографии итоговой конструкции и ссылки на компоненты.
                            0
                            Ссылки на компоненты добавил в статью, а вот фотографию, к сожалению, сделать не могу — все это дело сейчас погребено за тумбой под телевизором,
                            Собственно на фотографии было бы изображено 5 последовательно подключенных коробочек.
                              0
                              спасибо
                            0
                            от второго ролика чуть не случился эпилептический припадок… неужели дети такое смотрят?

                            Но для демонстрации девайса — самое оно. Впечатляет скорость реакции
                              0
                              Всё здорово, но у вас не правильный ambilight.
                              1. в темных сценах у вас темно — это не правильно, должен быть общий уровень света, чтобы глаза не уставали. В этом один из главных смыслов ambilight — комфортно для глаз.
                              2. динамика у вас слишком высокая, да у оригинального филипского ambilight тоже можно динамику увеличить, только это очень напрягает, по дефолту стоит низкая динамика.

                              по-моему самое универсальное решение именно с камерой, нужно было заменить тормозную камеру, на что-нибудь стоящее.
                              А текущее решение не работает для кабельного телевидения.

                                0
                                A нельзя было без всяких HDMI конвертеров, подключить граббер прямо к аналоговому видеовыходу телевизора (надеюсь, такое на телевизорах еще существует)?
                                  0
                                  У моего телевизора LG 60PK960 такого видео выхода нет, если бы он был, все было бы намного проще.
                                    0
                                    Современные ТВ, в большинстве, не «дублируют» сигнал на композитный выход при входе через HDMI, поскольку для этого необходимо дополнительное преобразование.
                                    А вот разобрать сигнал с композитного вывода AV-конвертера напрямую с помощью Arduino было бы реально круто!
                                    0
                                    Поразительно, но ваш Амбилайт работает мгновенно! Т.е. быстрее, чем оригинальный от Филипс, который, суко, ЖУТКО тормозит и отстает от видеоряда. :(
                                        0
                                        Разве лайтпак не работает с консольками?
                                          0
                                          Лайтпак работает через захват локально выводимого изображения, соответственно на источнике сигнала должен стоять софт лайтпака -> на нормальной консоли (не андройд огрызок) поставить такой софт не получится. Только через схему сквозного захвата видео сигнала можно получить универсальное платформо-независимое решение.
                                            0
                                            Тяжела жизнь консольного геймера, эх

                                        Only users with full accounts can post comments. Log in, please.