понедельник, 23 декабря 2013 г.

DIY: Атмосвет 2.0, Часть 2: Софт.

Провозился с обработкой фреймов все выходные и получил приятный результат.
Пока не успел все обернуть в win service с web UI - это будет задачей на следующие, о чем писать я уже не буду, ибо и с это все просто.

К предыдущей части дополню 2 нюанса:
1. Ардуино не рабоает корректно с буфером HardwareSerial в 512, выставил в 410(размер моих пакетов), все заработало корреткно, не стал разбираться почему 512 не подошло.
2. Как оказалась аналоговая картинка на выходе из конвертера шла с черными полосами сверху/снизу и слева, притом снизу это около 18 пикселей, исправил это я кропом кадра. В opencv это делается через ROI. Черные полосы идут в ущерб изображению, но для подсветки это не заметно.

Итак, про обработку фреймов...

Тестовая тулза у меня выглядилт следующим образом (дергаем ползунки и смотрим на изменение подсветки в риалтайме), фактически она и убдет разбита на сервис + WebUI:

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

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мс она захлебывается, таким образом выходит, что мы не зависим напрямую от частоты кадров захвата и не мешаем тому процессу(а ведь отправка пакета тоже жрет время).

Эпилог

Что я еще хотел реализовать в обработке, но не вышло в первом прототипе:
1. Не удалось нагуглить методику предсказания изображения за его границами(а я так надеялся, что кто-то придумал подобное). Типа построения вектора распрсотранения цветов по картинке, определение шаблона картинки и на её основе дорисовка 5-10% изображения за его пределами.
Потом какнибудь подумаю сам на эту тему.
2. Не придумал как бы можно было бы вкорячить предсказание движения объектов за пределами изображения, хорошо бы смешать это с п.1. походу это лишнее.
3. Я бы хотел соорудить автоматическую подстройку цвета и коррекцию расположения пикселей, используя внешнюю камеру типа кинекта, которая может корректно воспринимать цвета.

Комментариев нет:

Отправить комментарий