2026, Jan 11 12:02
Исправляем композицию в квадратичных фильтрах Вольтерры: эффект отображения и оператор Тигера
Почему квадратичные фильтры Вольтерры с оператором Тигера не усиливают изображение? Разбираем ошибку композиции, даём исправление и готовый код на Python.
Воспроизвести настраиваемые квадратичные фильтры для улучшения изображений кажется обманчиво простым: нормализовать, переназначить уровни яркости, запустить квадратичный оператор Вольтерры в стиле Тигера, смешать и денормализовать. Но одна маленькая ошибка на этапе композиции способна полностью обнулить эффект нелинейного отображения и сделать результат похожим на слабый unsharp mask. Ниже — короткое объяснение, где кроется ловушка, как её исправить и готовый код для проверки.
Проблема
Конвейер улучшения нормализует оттенки серого в диапазон [0,1], применяет входное отображение вроде f_map_2 (x^2) или f_map_5 (кусочно-квадратичная функция), фильтрует отображённое изображение двумерным квадратичным оператором Вольтерры, подобным оператору Тигера (формула (53) из указанной работы), а затем собирает итог как сумму базового слоя и масштабированной высокочастотной компоненты. Ожидается, что выход будет подчёркивать светлые и тёмные области в зависимости от выбранного отображения, а не просто утолщать границы. Однако результат почти не отличался от исходного, с минимальным усилением, зависящим от интенсивности.
Код, воспроизводящий проблему
Ниже показан фрагмент, где на этапе композиции по ошибке используется нормализованное изображение вместо отображённого. Логика в остальном верная, но из‑за одной строки влияние отображения на финальный результат пропадает.
import cv2
import numpy as np
def to_unit_range(img):
return img.astype(np.float32) / 255.0
def to_byte(img):
return (img * 255).clip(0, 255).astype(np.uint8)
def map_input(arr, map_kind='none'):
if map_kind == 'none':
return arr
elif map_kind == 'map2':
return arr ** 2
elif map_kind == 'map5':
out = np.zeros_like(arr)
m = arr > 0.5
out[m] = 1 - 2 * (1 - arr[m]) ** 2
out[~m] = 2 * (arr[~m] ** 2)
return out
else:
raise ValueError("bad map")
def teager2d(src):
pad = np.pad(src, 1, mode='reflect')
dst = np.zeros_like(src)
for i in range(1, pad.shape[0] - 1):
for j in range(1, pad.shape[1] - 1):
c = pad[i, j]
t1 = 3 * (c ** 2)
t2 = -0.5 * pad[i + 1, j + 1] * pad[i - 1, j - 1]
t3 = -0.5 * pad[i + 1, j - 1] * pad[i - 1, j + 1]
t4 = -1.0 * pad[i + 1, j] * pad[i - 1, j]
t5 = -1.0 * pad[i, j + 1] * pad[i, j - 1]
dst[i - 1, j - 1] = t1 + t2 + t3 + t4 + t5
return dst
def run_enhance(path, gain_k, map_kind='none'):
im = cv2.imread(path, 0)
if im is None:
raise FileNotFoundError("No image found!")
x_unit = to_unit_range(im)
remapped = map_input(x_unit, map_kind)
tq_out = teager2d(remapped)
# Проблема: при композиции с x_unit эффект переназначения пропадает
result_unit = np.clip(x_unit + gain_k * tq_out, 0, 1)
return to_byte(result_unit)
Почему улучшение перестаёт работать
Сердце метода — нелинейное отображение. Отображённое изображение — это f(x), а нормализованное — всего лишь x. Если складывать x с высокочастотной частью, вы фактически игнорируете отображение в финальном результате. Поэтому выход и выглядит как лёгкое подчёркивание контуров почти без усиления, зависящего от яркости.
Следующее наблюдение хорошо описывает связь фильтра и базового слоя:
Заметьте, фильтр Тигера усиливает только высокочастотные компоненты изображения. Для его выхода принципиальной разницы почти не будет, подадите ли вы на вход отображённое изображение или нормализованное. Поэтому при сложении низкочастотной и высокочастотной составляющих необходимо использовать именно отображённое изображение, чтобы сохранить применённое отображение.
Иными словами, оператор, похожий на Тигера, отвечает за «высокие частоты». А базовый слой, к которому вы его добавляете, должен быть именно отображённым сигналом; иначе задуманное отображение до зрителя не дойдёт.
Исправление и корректный код
Правка в одну строку: составлять результат, используя отображённое изображение как базу. Полезно также отделить ввод‑вывод от обработки — так проще тестировать.
import cv2
import numpy as np
def to_unit_range(img):
return img.astype(np.float32) / 255.0
def to_byte(img):
return (img * 255).clip(0, 255).astype(np.uint8)
def map_input(arr, map_kind='none'):
if map_kind == 'none':
return arr
elif map_kind == 'map2':
return arr ** 2
elif map_kind == 'map5':
out = np.zeros_like(arr)
m = arr > 0.5
out[m] = 1 - 2 * (1 - arr[m]) ** 2
out[~m] = 2 * (arr[~m] ** 2)
return out
else:
raise ValueError("bad map")
def teager2d(src):
pad = np.pad(src, 1, mode='reflect')
dst = np.zeros_like(src)
for i in range(1, pad.shape[0] - 1):
for j in range(1, pad.shape[1] - 1):
c = pad[i, j]
t1 = 3 * (c ** 2)
t2 = -0.5 * pad[i + 1, j + 1] * pad[i - 1, j - 1]
t3 = -0.5 * pad[i + 1, j - 1] * pad[i - 1, j + 1]
t4 = -1.0 * pad[i + 1, j] * pad[i - 1, j]
t5 = -1.0 * pad[i, j + 1] * pad[i, j - 1]
dst[i - 1, j - 1] = t1 + t2 + t3 + t4 + t5
return dst
def enhance_from_array(img_gray, gain_k, map_kind='none'):
x_unit = to_unit_range(img_gray)
remapped = map_input(x_unit, map_kind)
tq_out = teager2d(remapped)
# Исправление: составлять результат, используя отображённую базу
result_unit = np.clip(remapped + gain_k * tq_out, 0, 1)
return to_byte(result_unit)
# Пример использования
# img = cv2.imread("path/to/image.png", 0)
# out = enhance_from_array(img, gain_k=0.1, map_kind='map5')
# cv2.imwrite("out.png", out)
Почему это важно сделать правильно
Отображение — это рычаг, который делает усиление содержательно‑зависимым. Оно смещает базовый слой к светлым или тёмным областям до добавления «высоких частот». Если на этапе композиции вы выбрасываете отображённую базу, рычаг нейтрализуется, и всё сводится к общему подчёркиванию контуров. Именно поэтому результаты не повторяли ожидаемое усиление, зависящее от интенсивности.
Выводы
Реализуя квадратичные фильтры Вольтерры с нелинейным входным отображением, проверьте, какой сигнал берёте за базу при композиции. Компонента, похожая на оператор Тигера, вносит в основном «высокие частоты»; отображённое изображение должно донести до финального результата запланированную форму по интенсивностям. Держите файловый ввод‑вывод вне функций обработки для удобства тестирования и до смешивания отдельно посмотрите на отображённую базу и выход оператора Тигера. С корректной композиции улучшение работает как задумано.