При просмотре телевизора я постоянно вижу красный. В прямом смысле – подсветка моего 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 и уютный чат