При просмотре телевизора я постоянно вижу красный. В прямом смысле – подсветка моего Panasonic частично не работает, что вызывает неравномерное розовое свечение там, где должен быть белый цвет.

Мне этот старый хлам достался даром, поэтому я особо не жалуюсь, но пару недель назад все же решил как-то отображение цвета наладить.

▍ Проблема


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


Красноватые пятна при просмотре фильма “Береги свою косынку, Татьяна»

Предположим, дано изображение x, которое телевизор искажает в y = f(x). Нам нужно найти обратную функцию f-1(x), которая обратит искажение цвета, на выходе дав y = f-1(f(x)). Затем можно использовать ее для компенсации красного цвета перед отправкой изображения на экран.


Схема предполагаемой конфигурации. Скорректированное изображение f-1(x) передается с компьютера через HDMI, искажается функцией f(x) телевизора, после чего итоговый цвет воспринимается зрителем

К сожалению, я знаю, что такой f-1(x) не существует. Как уже говорилось, частично рабочая подсветка недостаточно ярко излучает свет на определенных частотах. Компенсировать это с помощью какой-либо предварительной обработки не выйдет.

Вы можете подумать: «А почему бы просто не приглушить каналы зеленого и синего?» Верная мысль – именно так я и собираюсь поступить. Здесь нам потребуется решить уравнение:


Где с – это некая константа, предположим с = 0.9, а y – это реальное изображение, которое зритель хочет видеть. Мы принимаем тот факт, что достичь удастся лишь 90% от максимальной яркости. Теперь пора начать поиск f-1. Как вы видели в начале, в разных частях экрана красный проявляется с разной интенсивностью, поэтому постоянная цветокоррекция изображения не сработает. Значит, есть смысл попробовать перехватить искажение, чтобы его отменить.

▍ Чрезмерное усложнение решения


Я настроил ТВ на показ белого экрана и сделал несколько фото с телефона, чтобы получить изображение неравномерного паттерна распределения пятен. После некоторой коррекции перспективы и размытия в GIMP получилось вот что:


Это изображение паттерна распределения пятен получено усреднением трех разных картинок с целью устранения муарового узора. Помимо этого, здесь применено гауссово размытие.

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

Пусть исходное изображение будет x, а изображение с пятнами z. Утвердим допущение, что искомая функция f-1(x) имеет вид f-1(x) = x * (gain * z + offset) и в коде выглядит так:

z = load_image("blobs.png")

def finv(x):
    return x * (gain * z + offset)

Здесь gain и offset являются скалярами. В этом коде мы сначала подстраиваем цвета изображения z, после чего модулируем с его помощью исходный входной сигнал. Теперь проблема сводится к поиску удачных значений для gain и offset.

▍ Нахождение обратной функции


На этом этапе все начало сходить с рельс. Вместо того, чтобы просто попытаться вычислить функцию от руки, я пошел по пути «оптимизации», подключив через USB камеру и направив ее на экран ТВ. При этом я также написал скрипт Python, перебирающий случайные значения для gain и offset до тех пор, пока не будет получено хорошее изображение. В результате конфигурация стала уже такой:


В схему добавилась камера. Она видит показываемые телевизором цвета, повторно искажает их собственным откликом g(x) и отправляет кадр на компьютер для анализа.

Но откуда компьютер знает, как выглядит хорошая картинка? Какой будет функция пригодности? Нельзя ли просто попиксельно вычислить разницу с помощью error = |x - camera_img|? Проблема этого подхода в том, что у камеры есть собственное искажение цветов, которое на схеме представлено как g(x). Это усложняет составление грамотной функции пригодности.

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


Исходное изображение с камеры (слева) и отредактированная картинка (справа), которая будет считаться эталоном

Теперь можно минимизировать среднюю разницу в пикселях между эталонным изображением и любым другим, поступающим с камеры. Получается следующий цикл:

import numpy as np

z = load_image('blobs.png')
gt = load_image('ground_truth.png')

# Здесь мы применяем ранее упомянутую константу "c".
gt *= 0.9

# Индексы в массиве "params".
GAIN = 0
OFFSET = 1

params = np.zeros(2)
params[GAIN] = 1.0
params[OFFSET] = 0.0

# Наша функция f^-1, описанная ранее.
def finv(x):
    return x * (z * params[GAIN] + params[OFFSET])

best_fitness = 0
best_params = np.copy(params)

while True:
    # "frame" по факту получается с выдержкой в одну секунду для исключения шума.
    frame = capture_camera_img()

    # Предположим, что интенсивность изображения находится в диапазоне [0, 1].
    # Функция пригодности равна один минус норма L1 разницы в пикселях.
    fitness = 1 - np.mean(np.abs(frame - gt))

    if fitness > best_fitness:
        best_fitness = fitness
        best_params = np.copy(params)
        print(best_fitness, best_params)

    # Рандомизация параметров.

    params[GAIN] = np.random.random() * -1. - 0.1   # диапазон [-1.1, -0.1]
    params[OFFSET] = np.random.random() * 2         # диапазон [0, 2]

    # Обновление изображения, показываемого на ТВ.
    # Мы показываем скорректированную пустую белую картинку, оценивая относительно нее очередной кадр.
    white_img = np.ones_like(frame)
    show_on_tv(finv(white_img))

Итак, по итогу у нас получилась следующая функция:


Здесь z по-прежнему является тем же изображением с пятнами.

Вот, что получается при применении этой функции к видеокадру:



Результаты обратной функции. Входные изображения (левый столбец) и соответствующие изображения на ТВ (правый столбец). Скорректированная картинка показана в левом нижнем углу и имеет зеленоватый оттенок, однако ее результат справа получился по цветам уже более сбалансированным. Заметьте, что сюда также входит глобальная коррекция цвета конечного шейдера. Полноразмерная картинка

Оглядываясь назад, можно заметить, что она очень похожа на f-1(x) = x * (-z + 1.5), к чему можно было прийти и без какого-либо автоматизированного поиска. Кроме того, остальные решения, возвращенные в его результате, оказались совсем безобразными. Но сейчас это уже не важно, поскольку у нас есть обратная функция, которую самое время использовать.

▍ Добавление в MPC-BE собственного шейдера


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


Редактор шейдеров в MPC-BE

Основная проблема заключалась в необходимости передать в шейдер изображение blobs.png. Это заняло немало времени, но в конечном итоге мне удалось взломать поддержку внешних текстур и немного подстроить яркость и цвета. Получилось превосходно:


Цветокоррекция с помощью f-1(x) сильно помогла

Работает! По краям все еще наблюдается эффект виньетирования, но самый неприятный частотный разнобой устранен. Можете также посмотреть полноценные снимки до и после.

▍ Заключение


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

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

Дополнение: конечный шейдер colorfix.hlsl
Предполагается, что sampler s0 содержит входное видео, а sampler s1 изображение с пятнами.

// $MinimumShaderProfile: ps_2_0
sampler s0 : register(s0);
sampler s1 : register(s1);

float4 main(float2 tex : TEXCOORD0) : COLOR {
    float4 c0 = tex2D(s0, tex);
    float4 c1 = tex2D(s1, tex);
    float3 c1b = float3(-0.9417, -0.9417, -0.9417) * c1.rgb + float3(1.48125, 1.48125, 1.48125);

    float4 c2 = c0 * float4(c1b.rgb, 1.);
    c2 *= float4(1.05, 1., 1.15, 1.);
    c2 *= 0.95;

    return c2 ;
}


Дополнение: патч для MPC BE
Index: src/filters/renderer/VideoRenderers/SyncRenderer.cpp
===================================================================
--- src/filters/renderer/VideoRenderers/SyncRenderer.cpp    (revision 5052)
+++ src/filters/renderer/VideoRenderers/SyncRenderer.cpp    (working copy)
@@ -45,6 +45,16 @@
 #include "../DSUtil/DXVAState.h"
 #include "../../../apps/mplayerc/resource.h"
 
+#define NOMINMAX
+#include <algorithm>
+namespace Gdiplus
+{
+   using std::min;
+   using std::max;
+}
+#include "../../../apps/mplayerc/PngImage.h"
+
+
 using namespace GothSync;
 using namespace D3D9Helper;
 
@@ -654,6 +664,45 @@
    }
 
    hr = m_pD3DDevEx->ColorFill(m_pVideoSurfaces[m_iCurSurface], nullptr, 0);
+   
+
+   CMPCPngImage pattern;
+   DLog(L"Loading pattern texture");
+   // TODO use relative path
+   if (pattern.Load(L"C:\\dev\\opensource\\mpcbe-code\\pattern2.png") == E_FAIL) {
+       DLog(L"Loading failed");
+   } 
+   else {
+       DLog(L"Pattern: %dx%d @ %d bpp, IsDIB: %d", pattern.GetWidth(), pattern.GetHeight(), pattern.GetBPP(), pattern.IsDIBSection());
+       if (FAILED(hr = m_pD3DDevEx->CreateTexture(
+           pattern.GetWidth(), pattern.GetHeight(), 1, D3DUSAGE_DYNAMIC, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &m_pPatternTexture, nullptr))) {
+
+           DLog(L"Texture creation failed");
+           return hr;
+       }
+       else {
+           DLog(L"Pattern texture OK");
+           unsigned char* data = (unsigned char*)pattern.GetBits();
+           int pitch = pattern.GetPitch();
+           DLog("Data: %p, pitch: %d bytes\n", data, pitch);
+
+           D3DLOCKED_RECT rect = {};
+           m_pPatternTexture->LockRect(0, &rect, NULL, D3DLOCK_DISCARD);
+           DLog("Rect pBits: %p, rect.pitch: %d bytes\n", rect.pBits, rect.Pitch);
+           for (int y = 0; y < pattern.GetHeight(); y++) {
+               for (int x = 0; x < pattern.GetWidth(); x++) {
+                   unsigned char* pix = (unsigned char*)rect.pBits + (y * rect.Pitch + 4 * x);
+                   pix[0] = data[y * pitch + 4 * x + 0];
+                   pix[1] = data[y * pitch + 4 * x + 1];
+                   pix[2] = data[y * pitch + 4 * x + 2];
+                   pix[3] = data[y * pitch + 4 * x + 3];
+               }
+           }
+
+           m_pPatternTexture->UnlockRect(0);
+       }
+   }
+   
    return S_OK;
 }
 
@@ -669,6 +718,7 @@
    m_pRotateTexture = nullptr;
    m_pRotateSurface = nullptr;
    m_pResizeTexture = nullptr;
+   m_pPatternTexture = nullptr;
 }
 
 // ISubPicAllocatorPresenter3
@@ -1483,6 +1533,8 @@
                            Shader.Compile(m_pPSC);
                        }
                        hr = m_pD3DDevEx->SetPixelShader(Shader.m_pPixelShader);
+
+                       hr = m_pD3DDevEx->SetTexture(1, m_pPatternTexture);
                        TextureCopy(m_pScreenSizeTextures[src]);
 
                        std::swap(src, dst);
Index: src/filters/renderer/VideoRenderers/SyncRenderer.h
===================================================================
--- src/filters/renderer/VideoRenderers/SyncRenderer.h  (revision 5052)
+++ src/filters/renderer/VideoRenderers/SyncRenderer.h  (working copy)
@@ -153,6 +153,7 @@
        CComPtr<IDirect3DSurface9>  m_pOSDSurface;
        CComPtr<IDirect3DTexture9>  m_pScreenSizeTextures[2];
        CComPtr<IDirect3DTexture9>  m_pResizeTexture;
+       CComPtr<IDirect3DTexture9>  m_pPatternTexture;
        CComPtr<ID3DXLine>          m_pLine;
        CComPtr<ID3DXFont>          m_pFont;
        CComPtr<ID3DXSprite>        m_pSprite;


RUVDS | Community в telegram и уютный чат

Комментарии (22)


  1. koresh_builder
    16.10.2022 14:33
    +6

    Статья интересная, но не проще ли было поменять подсветку? На али их куча лежит.


    1. flass
      16.10.2022 14:46
      +20

      Ну что вы, конечно же проще, но это ведь неспортивно! Казалось бы, чтобы поймать рыбу, к чему мучиться с удочкой, если человечество уже изобрело сеть?


      1. 104u
        16.10.2022 14:53
        +1

        Ну да, у меня так в авто было — кто-то подкрутил тросик газа вместо регулировки дроссельной заслонки (ЭБУ не видел, что педаль газа отпущена). Пофиг, что иногда на холостом глохнет

        Автор ещё и дорабатывать коррекцию будет, наверное, по мере догорания подсветки

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


      1. Didimus
        16.10.2022 19:25

        Хм, а как бы найти годную подсветку, чтобы была не хуже родной и не мерцала? А то CFL стали мерцать по краю экрана


    1. DistortNeo
      16.10.2022 16:19
      +1

      Не проще. Почти все экраны с большой диагональю от этого страдают.
      А OLED-телевизоры пока ещё дороги.


      1. EvilFox
        16.10.2022 17:15
        +1

        А OLED-телевизоры пока ещё дороги.

        Словно у них нет этих проблем.


    1. novoselov
      17.10.2022 14:16

      Вообще было бы неплохо иметь подобный калибратор под рукой.

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


  1. defecator
    16.10.2022 15:36
    -13

    А есть ли ещё какие-то области, в которые не проник этот современный бейсик - питон ?

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


    1. DartfoL
      16.10.2022 18:25
      +10

      Слышали про такое понятие, как «проблемно-ориентированное программирование»? Это когда программирование не ради программирования, не ради щупанья и самостоятельной реализации уже давно реализованных алгоритмов, а когда нужно решать конкретные задачи.

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


      1. firehacker
        17.10.2022 10:54
        -2

        Все правильно, Си же не проблемно-ориентированный язык. Его же придумали не для написания ОС, его придумывали чтобы сидеть и аутировать, программируя ради программирования.


      1. defecator
        17.10.2022 21:01
        -3

        проблемно-ориентированное программирование хорошо, когда надо решать задачи в ERP - хренак-хренак, и пусть там пользователь умирает.

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

        Я подозреваю, что банальный алгоритм сортировки пузырьком данный товарищ не осилит


        1. iig
          18.10.2022 09:58
          +3

          Я подозреваю, что банальный алгоритм сортировки пузырьком данный товарищ не осилит

          Ну вызовите его на батл. Или покажите свой идеальный код, где все кубики вырезаны самостоятельно :D


    1. rezedent12
      17.10.2022 11:29
      +1

      Что ты имеешь против BASIC и его диалектов?


  1. SnakeSolid
    16.10.2022 15:37

    Раз уж z не изменяется можно коэффициенты -0.94 и 1.48 "запечь" в нее, тогда весь код упростится до color = color * z.


  1. YegorP
    16.10.2022 17:23
    +30

    Я как-то давно на работе выцепил списанный монитор с "дыркой". Потом набросал прогу на Windows Forms, которая при наведении туда указателя выводила изображение с этой дырки рядом.

    Не знаю как это языком математики выразить. Афинное преобразование?)) Саму дырень заклеил чтобы меньше контрастировала (y = C + x * 0 ?).

    В этом даже некоторая целесообразность была, ибо контора государственная, и лишние моники - а этот я поставил вторым - нам не полагались, даже за свои деньги (попробуй пронеси).


  1. nixtonixto
    16.10.2022 18:45
    +11

    Рабочее время специалиста, способного решить такую задачу — стоит гораздо дороже нового телевизора. Даже в СНГ. Но для прокачки скиллов сойдёт. А потом телик сразу на помойку и в магазин за новым.


    1. Didimus
      16.10.2022 19:27
      -3

      Вы давно цены на телевизоры смотрели?


      1. nixtonixto
        16.10.2022 20:41
        +2

        За такую работу исполнитель запросит порядка 1000 долларов. За эти деньги можно купить 4К 55" Сяоми или 50" Самсунг и ещё останется.


        1. himch
          16.10.2022 22:04
          +3

          О, моя месячная зарплата ????


        1. vasilykomyagin
          17.10.2022 09:46

          Отличный пет-проект :)


    1. firehacker
      17.10.2022 18:09

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


  1. Rober
    17.10.2022 13:23

    Интересно, можно подобное на уровне системы сделать? Хотя бы в Linux.