Por que funciona: Unidade 1.2: Captura da imagem, Unidade 1.3: A imagem como dado

Correção gama no OpenCV com uma tabela de consulta

Monte uma tabela de consulta de 256 entradas e aplique com cv2.LUT para clarear mais os tons escuros que os claros. Com dois stops de falta, multiplicar vence (9,1 contra 10,7); com três, a gama vence (13,9 contra 15,1).

Luis Condados · · 4 min de leitura Iniciante

Tradução assistida por máquina do original em inglês. Números, código e referências foram conferidos com ele; o texto não foi revisado por um falante nativo. Ler o original. Os labs interativos e o código estão em inglês.

A foto tirada em 1/2 segundo, passada por uma gama de 0,40.
A foto tirada em 1/2 segundo, passada por uma gama de 0,40.

A correção gama clareia mais as partes escuras de uma foto do que as claras, então as sombras se abrem sem que as altas luzes estourem.

A foto muito escura do abajur ao lado de uma versão corrigida com gama 0,40, com as distâncias 62,3 e 13,9.
À esquerda: a foto tirada em 1/2 segundo, mais de três stops escura. À direita: passada por uma gama de 0,40. Quadro do HDR Photographic Survey [3].

Onde você já viu isso: a opção Gama dos jogos e dos menus de monitor; o controle do meio da ferramenta Níveis no GIMP e no Photoshop, que é um controle de gama.

TL;DR. Uma curva gama levanta as sombras mais que as altas luzes e nunca empurra um valor além do branco, o que faz dela a melhor correção quando a foto está muito escura. Com dois stops de falta, multiplicar ainda vence (9,1 contra 10,7); com três stops, a gama vence (13,9 contra 15,1).

O problema

A lição 3 corrigiu a foto dois stops escura multiplicando todos os valores por 2,4. O projeto tem também uma mais escura, tirada em 1/2 segundo, mais de três stops abaixo do alvo de 5 segundos. Multiplicar essa foto por 4,2 é o melhor que um ganho consegue, e tudo o que começou acima de 60 termina em 255: o ganho que salva as sombras destrói todo o resto. A célula mais abaixo faz essa conta.

Uma tabela de consulta

A correção gama passa cada valor por uma curva de potência: divide por 255 para que vá de 0 a 1, eleva a γ e multiplica por 255 de novo [1]. Um γ abaixo de 1 clareia, acima de 1 escurece, e 0 continua 0 enquanto 255 continua 255 para qualquer γ [2].

Como só existem 256 valores de entrada possíveis, a curva é calculada uma vez numa tabela de 256 respostas, e cv2.LUT troca cada pixel pela sua entrada. É assim que qualquer mapeamento fixo por valor (gama, curvas de contraste, gradação de cor) é aplicado rápido.

Python · OpenCV
import cv2
import numpy as np

ref = cv2.imread("reference.webp")

def distance(a, b):
    return np.abs(a.astype(int) - b.astype(int)).mean()

def gamma(img, g):
    table = (np.arange(256) / 255) ** g * 255
    table = table.round().astype(np.uint8)
    return cv2.LUT(img, table)

for name, g, a in [("dark.webp", 0.55, 2.4),
                   ("darker.webp", 0.40, 4.2)]:
    img = cv2.imread(name)
    by_gamma = distance(gamma(img, g), ref)
    by_gain = distance(cv2.convertScaleAbs(img, alpha=a), ref)
    print(name, "gamma", g, round(by_gamma, 1),
          "| times", a, round(by_gain, 1))
Saída
dark.webp gamma 0.55 10.7 | times 2.4 9.1
darker.webp gamma 0.4 13.9 | times 4.2 15.1
FunçãoArgumentosO que faz
np.arange(256)—as 256 entradas possíveis, de 0 a 255
cv2.LUT(img, table)uma imagem de 8 bits, uma tabela de 256 entradastroca cada valor v por table[v], canal por canal

Na foto dois stops escura, o multiplicador ainda fica um pouco à frente. Na que está três stops escura, a gama fica à frente. Quanto mais longe a foto está do alvo, mais pesa o fato de a gama nunca estourar.

Python · OpenCV
darker = cv2.imread("darker.webp")
times = cv2.convertScaleAbs(darker, alpha=4.2)
for name, x in [("as shot", darker), ("times 4.2", times),
                ("gamma 0.40", gamma(darker, 0.40))]:
    share = (x.max(axis=2) == 255).mean()
    print(name, "- at 255:", round(100 * share, 1), "%")
def half(x):
    return cv2.resize(x, None, fx=0.5, fy=0.5)

small = [half(x) for x in (times, gamma(darker, 0.40), ref)]
cv2.imshow("times 4.2 | gamma 0.40 | reference", np.hstack(small))
Saída
as shot - at 255: 6.2 %
times 4.2 - at 255: 14.4 %
gamma 0.40 - at 255: 6.4 %
times 4.2 | gamma 0.40 | reference
times 4.2 | gamma 0.40 | reference

Multiplicar por 4,2 leva a fatia de pixels em 255 de 6,2% para 14,4%, enquanto a gama a leva só a 6,4%: a versão multiplicada (à esquerda) perdeu o abajur, a carta e a poça de luz para um branco chapado. A versão com gama mantém as bordas da carta e o aro do abajur, e suas sombras são mais claras e mais planas que as do alvo, que é de onde vem a distância que sobra.

A lição 5 testa a alternativa automática, que escolhe a curva a partir do histograma em vez de a partir de um alvo.

Quebre

A fórmula só funciona com valores entre 0 e 1. Eleve valores de 0–255 à potência direto e o pixel mais claro, 255, vira 2550.40, cerca de 9.

Quebre
# Elevando valores de 0-255 a uma potência sem escalar para 0-1 antes
wrong = darker.astype(float) ** 0.40
wrong = np.clip(wrong, 0, 255).astype(np.uint8)
print("largest value after the wrong gamma:", wrong.max())
cv2.imshow("gamma on 0-255 instead of 0-1", wrong)
Saída
largest value after the wrong gamma: 9
gamma on 0-255 instead of 0-1
gamma on 0-255 instead of 0-1

A imagem inteira desaba para quase preto.

Sua vez

Um γ acima de 1 escurece. Escolha um γ que deixe a superexposta bright.webp a menos de 36 do alvo.

Sua vez
# escolha uma gama que escureça bright.webp em direção ao alvo
g = 1.0
print(round(distance(gamma(cv2.imread("bright.webp"), g), ref), 1))

Por que funciona

A imagem revelada por uma câmera já é a luz bruta passada por uma curva com esse formato, e The response curve (em inglês) mede essa curva nesta mesma cena do abajur. Aplicar outra curva de potência dobra os tons ainda mais no mesmo sentido, e é por isso que um único γ consegue desfazer boa parte de um erro de exposição que um multiplicador simples não desfaz. O custo aparece em Quantization and bit depth (em inglês): esticar os poucos níveis mais escuros de uma foto de 8 bits sobre uma faixa maior espalha os 256 níveis, e degradês suaves podem formar faixas.

Erros comuns

  • Esquecer o / 255. É a célula Quebre.
  • Montar a tabela em float. cv2.LUT quer uma tabela do mesmo tipo da imagem; monte em uint8, como faz a célula.
  • Confundir o sentido de γ. Nesta fórmula, γ abaixo de 1 clareia. Algumas ferramentas definem a gama ao contrário (como 1/γ), então confira o que um controle significa antes de copiar o número dele.
  • Esperar que a gama recupere altas luzes estouradas. 255 vira 255 para qualquer γ.

Referências

[1] OpenCV. Changing the contrast and brightness of an image!, section “Gamma correction” (tutorial, doc/tutorials/core/basic_linear_transform/basic_linear_transform.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv

[2] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 3.2 “Some Basic Intensity Transformation Functions”, Power-Law (Gamma) Transformations, p. 139. Pearson.

[3] Fairchild, M. D. (2007). The HDR Photographic Survey. IS&T 15th Color Imaging Conference, pp. 233–238. doi:10.2352/CIC.2007.15.1.art00044