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)

Почему это важно сделать правильно

Отображение — это рычаг, который делает усиление содержательно‑зависимым. Оно смещает базовый слой к светлым или тёмным областям до добавления «высоких частот». Если на этапе композиции вы выбрасываете отображённую базу, рычаг нейтрализуется, и всё сводится к общему подчёркиванию контуров. Именно поэтому результаты не повторяли ожидаемое усиление, зависящее от интенсивности.

Выводы

Реализуя квадратичные фильтры Вольтерры с нелинейным входным отображением, проверьте, какой сигнал берёте за базу при композиции. Компонента, похожая на оператор Тигера, вносит в основном «высокие частоты»; отображённое изображение должно донести до финального результата запланированную форму по интенсивностям. Держите файловый ввод‑вывод вне функций обработки для удобства тестирования и до смешивания отдельно посмотрите на отображённую базу и выход оператора Тигера. С корректной композиции улучшение работает как задумано.