albitro's picture
YOLO26s fire/person detection: baseline vs preprocessed (dehaze+CLAHE)
96f110e verified
Raw
History Blame Contribute Delete
12.5 kB
#!/usr/bin/env python3
"""
Dark Channel Prior (DCP) ๊ธฐ๋ฐ˜ ๋””ํ—ค์ด์ฆˆ โ€” He et al., CVPR 2009.
OpenCV์— ์™„์ œํ’ˆ์ด ์—†์–ด ์ง์ ‘ ๊ตฌํ˜„ํ•œ ๋ถ€๋ถ„.
ROS์— ์˜์กดํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ฃผํ”ผํ„ฐ/CLI์—์„œ ๊ทธ๋Œ€๋กœ importํ•ด ํŠœ๋‹ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์‹ค์‹œ๊ฐ„์„ฑ ์ตœ์ ํ™” (๋ฐœํ‘œ์—์„œ์˜ ๊ธฐ์—ฌ ํฌ์ธํŠธ):
- ๋Œ€๊ธฐ๊ด‘/ํˆฌ๊ณผ์œจ ์ถ”์ •์„ ์ถ•์†Œ ํ•ด์ƒ๋„(๊ธฐ๋ณธ 1/4)์—์„œ ์ˆ˜ํ–‰ โ†’ ํ”ฝ์…€ ์ˆ˜ 1/16
- guided filter๋„ ์ถ•์†Œ ํ•ด์ƒ๋„์—์„œ ์ˆ˜ํ–‰ ํ›„ ํˆฌ๊ณผ์œจ๋งŒ ์—…์ƒ˜ํ”Œ
- ๋ณต์›์‹๋งŒ ์›๋ณธ ํ•ด์ƒ๋„์—์„œ ๊ณ„์‚ฐ (๋ฒกํ„ฐ ์—ฐ์‚ฐ 3์ค„)
- min filter๋Š” cv2.erode ๋กœ ๋Œ€์ฒด (์‚ฌ๊ฐ ์ปค๋„ erode == min filter, C ๊ตฌํ˜„)
ํ™”์žฌ ๋„๋ฉ”์ธ ํŠน์œ ์˜ ํ•จ์ •:
- ํ‘œ์ค€ DCP์˜ ๋Œ€๊ธฐ๊ด‘ A ์ถ”์ •์€ "๊ฐ€์žฅ ๋ฐ์€ ํ”ฝ์…€"์„ ๊ณ ๋ฅด๋Š”๋ฐ, ์šฐ๋ฆฌ ์žฅ๋ฉด์—์„œ๋Š”
๊ทธ๊ฒŒ **๋ถˆ์”จ/ํ™”์—ผ**์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. A๊ฐ€ ๊ณผ๋Œ€ ์ถ”์ •๋˜๋ฉด ํ™”๋ฉด ์ „์ฒด๊ฐ€ ์–ด๋‘์›Œ์ง€๊ณ 
์ •์ž‘ ๋ถˆ์”จ ์ฃผ๋ณ€์ด ๋ญ‰๊ฐœ์ง‘๋‹ˆ๋‹ค.
- ๋Œ€์‘: ์ƒ์œ„ ํ›„๋ณด์˜ **ํ‰๊ท **์„ ์‚ฌ์šฉ + a_max๋กœ ์ƒํ•œ ํด๋ฆฌํ•‘ + (์˜ต์…˜) ํ™”๋ฉด ์ƒ๋‹จ
์˜์—ญ๋งŒ ํ›„๋ณด๋กœ ์‚ผ๊ธฐ(sky_ratio). ์•„๋ž˜ estimate_atmospheric_light ์ฐธ๊ณ .
"""
from __future__ import annotations
import cv2
import numpy as np
# ---------------------------------------------------------------- ๊ธฐ๋ณธ ์—ฐ์‚ฐ
def dark_channel(img: np.ndarray, patch: int) -> np.ndarray:
"""๋‹คํฌ ์ฑ„๋„: ์ฑ„๋„ ์ตœ์†Ÿ๊ฐ’ -> patch x patch ์ตœ์†Œ ํ•„ํ„ฐ.
img: float32 (H, W, 3), 0~1 ๋ฒ”์œ„
"""
min_ch = np.min(img, axis=2)
if patch <= 1:
return min_ch
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (patch, patch))
# erode(์‚ฌ๊ฐ ์ปค๋„) == ์ตœ์†Œ ํ•„ํ„ฐ. ์ง์ ‘ ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ ์งœ๋ฉด ์ˆ˜์‹ญ ๋ฐฐ ๋А๋ฆผ.
return cv2.erode(min_ch, kernel)
def estimate_atmospheric_light(
img: np.ndarray,
dark: np.ndarray,
top_ratio: float = 0.001,
a_max: float = 0.92,
sky_ratio: float = 1.0,
) -> np.ndarray:
"""๋Œ€๊ธฐ๊ด‘ A ์ถ”์ • (1, 1, 3).
top_ratio : ๋‹คํฌ ์ฑ„๋„ ์ƒ์œ„ ๋ช‡ %๋ฅผ ํ›„๋ณด๋กœ ๋ณผ์ง€ (์›๋…ผ๋ฌธ 0.1%)
a_max : A ์ƒํ•œ. ํ™”์—ผ ๊ฐ™์€ ํฌํ™” ํ”ฝ์…€์ด ์„ž์˜€์„ ๋•Œ ๊ณผ๋Œ€์ถ”์ • ๋ฐฉ์ง€
sky_ratio : ํ›„๋ณด๋ฅผ ์ด๋ฏธ์ง€ ์ƒ๋‹จ ๋ช‡ ๋น„์œจ๋กœ ์ œํ•œํ• ์ง€ (1.0 = ์ œํ•œ ์—†์Œ).
์ง€ํ•˜์ฃผ์ฐจ์žฅ์ฒ˜๋Ÿผ ํ•˜๋Š˜์ด ์—†์œผ๋ฉด 1.0 ์œ ์ง€, ์—ฐ๊ธฐ๊ฐ€ ์œ„์— ๊น”๋ฆฌ๋ฉด 0.5~0.7.
"""
h, w = dark.shape
limit = h if sky_ratio >= 1.0 else max(1, int(h * sky_ratio))
dark_roi = dark[:limit]
img_roi = img[:limit]
n = max(int(dark_roi.size * top_ratio), 1)
flat_dark = dark_roi.ravel()
idx = np.argpartition(flat_dark, -n)[-n:]
candidates = img_roi.reshape(-1, 3)[idx] # (n, 3)
# ์›๋…ผ๋ฌธ์€ ํ›„๋ณด ์ค‘ "๊ฐ€์žฅ ๋ฐ์€ ํ•œ ํ”ฝ์…€"์„ ์“ฐ์ง€๋งŒ, ์ŠคํŽ˜ํ˜๋Ÿฌ/ํ™”์—ผ ํ•œ ์ ์—
# ํ†ต์งธ๋กœ ๋Œ๋ ค๊ฐ‘๋‹ˆ๋‹ค. ํ‰๊ท ์ด ํ›จ์”ฌ ์•ˆ์ •์ ์ž…๋‹ˆ๋‹ค.
a = candidates.mean(axis=0)
a = np.clip(a, 1e-3, a_max)
return a.reshape(1, 1, 3).astype(np.float32)
def guided_filter(guide: np.ndarray, src: np.ndarray, radius: int, eps: float) -> np.ndarray:
"""๋‹จ์ผ ์ฑ„๋„ guided filter (He et al.).
cv2.ximgproc.guidedFilter ๋Š” opencv-contrib-python ์ด ์žˆ์–ด์•ผ ์“ธ ์ˆ˜ ์žˆ์–ด์„œ
box filter 5๋ฒˆ์œผ๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์˜์กด์„ฑ ์—†์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
"""
guide = guide.astype(np.float32)
src = src.astype(np.float32)
k = (2 * radius + 1, 2 * radius + 1)
mean_i = cv2.blur(guide, k)
mean_p = cv2.blur(src, k)
corr_i = cv2.blur(guide * guide, k)
corr_ip = cv2.blur(guide * src, k)
var_i = corr_i - mean_i * mean_i
cov_ip = corr_ip - mean_i * mean_p
a = cov_ip / (var_i + eps)
b = mean_p - a * mean_i
mean_a = cv2.blur(a, k)
mean_b = cv2.blur(b, k)
return mean_a * guide + mean_b
# ---------------------------------------------------------------- ๋ฉ”์ธ ํด๋ž˜์Šค
class DarkChannelDehazer:
"""DCP ๋””ํ—ค์ด์ €.
process(bgr_uint8) -> bgr_uint8 ํ˜•ํƒœ๋กœ ์”๋‹ˆ๋‹ค.
ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ROS ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๊ทธ๋Œ€๋กœ ๊ฐˆ์•„๋ผ์šธ ์ˆ˜ ์žˆ๊ฒŒ attribute๋กœ ๋…ธ์ถœ.
"""
def __init__(
self,
omega: float = 0.95,
t0: float = 0.1,
patch: int = 15,
scale: float = 0.25,
use_guided: bool = True,
guided_radius: int = 8,
guided_eps: float = 1e-3,
a_top_ratio: float = 0.001,
a_max: float = 0.92,
sky_ratio: float = 1.0,
a_smoothing: float = 0.0,
):
self.omega = omega # 1.0์œผ๋กœ ํ•˜๋ฉด ์›๊ทผ๊ฐ์ด ์‚ฌ๋ผ์ ธ ๋ถ€์ž์—ฐ์Šค๋Ÿฌ์›€ (์›๋…ผ๋ฌธ 0.95)
self.t0 = t0 # ํˆฌ๊ณผ์œจ ํ•˜ํ•œ. ๋‚ฎ์„์ˆ˜๋ก ์ง„ํ•œ ์—ฐ๊ธฐ๊นŒ์ง€ ๋ณต์›ํ•˜์ง€๋งŒ ๋…ธ์ด์ฆˆ ํญ๋ฐœ
self.patch = patch # ์›๋ณธ ํ•ด์ƒ๋„ ๊ธฐ์ค€ ํŒจ์น˜ ํฌ๊ธฐ
self.scale = scale # ์ถ”์ • ๋‹จ๊ณ„ ์ถ•์†Œ ๋ฐฐ์œจ (โ˜… ์‹ค์‹œ๊ฐ„์„ฑ์˜ ํ•ต์‹ฌ)
self.use_guided = use_guided
self.guided_radius = guided_radius # ์ถ•์†Œ ํ•ด์ƒ๋„ ๊ธฐ์ค€ ๋ฐ˜๊ฒฝ
self.guided_eps = guided_eps
self.a_top_ratio = a_top_ratio
self.a_max = a_max
self.sky_ratio = sky_ratio
# ๋Œ€๊ธฐ๊ด‘ A์˜ ํ”„๋ ˆ์ž„ ๊ฐ„ EMA ๊ณ„์ˆ˜ (0 = ๋”, 0.9 = ๊ฐ•ํ•œ ํ‰ํ™œ).
# A๋Š” ๋งค ํ”„๋ ˆ์ž„ ์žฅ๋ฉด ๋‚ด์šฉ์—์„œ ์ถ”์ •๋˜๋ฏ€๋กœ, ๋กœ๋ด‡์ด ์›€์ง์—ฌ ํ™”๋ฉด์ด ๋ฐ”๋€Œ๋ฉด
# **๊ฐ™์€ ๋ถˆ์”จ๊ฐ€ ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋‹ค๋ฅธ ๋ฐ๊ธฐ๋กœ ๋ณต์›**๋ฉ๋‹ˆ๋‹ค. ์˜์ƒ์—์„œ๋Š” ๊นœ๋นก์ž„์œผ๋กœ
# ๋ณด์ด๊ณ , YOLO ํ•™์Šต ๋ฐ์ดํ„ฐ๋กœ ์“ฐ๋ฉด ๊ฐ™์€ ๋ฌผ์ฒด์˜ ์™ธํ˜• ๋ถ„์‚ฐ์ด ์ปค์ง‘๋‹ˆ๋‹ค.
# ์‹œ๊ณ„์—ด(๋™์˜์ƒยทrosbagยท์‹ค์ฃผํ–‰)์—๋Š” ์ผœ๊ณ , ์ •์ง€์˜์ƒ ๋น„๊ต ์‹คํ—˜์—๋Š” ๋•๋‹ˆ๋‹ค.
# โ˜… ์ผœ๋ฉด ์ถœ๋ ฅ์ด ์ด์ „ ํ”„๋ ˆ์ž„์— ์˜์กดํ•˜๋ฏ€๋กœ ๋” ์ด์ƒ ๊ฒฐ์ •๋ก ์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
self.a_smoothing = a_smoothing
self._a_ema: np.ndarray | None = None
self.last_transmission: np.ndarray | None = None # ๋””๋ฒ„๊น…/๋ฐœํ‘œ ๊ทธ๋ฆผ์šฉ
self.last_a: np.ndarray | None = None
def reset_state(self) -> None:
"""ํ”„๋ ˆ์ž„ ๊ฐ„ ๋ˆ„์  ์ƒํƒœ ์ดˆ๊ธฐํ™”. ์ƒˆ bag/์˜์ƒ์„ ์‹œ์ž‘ํ•  ๋•Œ ํ˜ธ์ถœ."""
self._a_ema = None
# ------------------------------------------------------------------
def _patch_for_scale(self) -> int:
"""์ถ•์†Œ๋ณธ์—์„œ ๊ฐ™์€ '์‹ค์ œ ๊ณต๊ฐ„ ๋ฒ”์œ„'๋ฅผ ๋ฎ๋„๋ก ํŒจ์น˜๋„ ํ•จ๊ป˜ ์ค„์ž…๋‹ˆ๋‹ค.
์ด๊ฑธ ์•ˆ ์ค„์ด๋ฉด ์ถ•์†Œ๋ณธ์—์„œ ํŒจ์น˜๊ฐ€ ์ƒ๋Œ€์ ์œผ๋กœ ๋„ˆ๋ฌด ์ปค์ ธ ํˆฌ๊ณผ์œจ์ด
๊ณผ๋„ํ•˜๊ฒŒ ๋ญ‰๊ฐœ์ง‘๋‹ˆ๋‹ค(halo).
"""
p = int(round(self.patch * self.scale))
p = max(3, p)
return p if p % 2 == 1 else p + 1
def estimate_transmission(self, img_f: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""์ถ•์†Œ๋ณธ์—์„œ A์™€ ํˆฌ๊ณผ์œจ t๋ฅผ ์ถ”์ •ํ•ด (t_full, A) ๋ฐ˜ํ™˜. img_f: float32 0~1 ์›๋ณธ ํ•ด์ƒ๋„."""
h, w = img_f.shape[:2]
if self.scale < 1.0:
small = cv2.resize(img_f, None, fx=self.scale, fy=self.scale,
interpolation=cv2.INTER_AREA)
else:
small = img_f
patch_s = self._patch_for_scale()
dark = dark_channel(small, patch_s)
a = estimate_atmospheric_light(small, dark, self.a_top_ratio,
self.a_max, self.sky_ratio)
# ํ”„๋ ˆ์ž„ ๊ฐ„ ํ‰ํ™œ โ€” t ๊ณ„์‚ฐ์— ๋“ค์–ด๊ฐ€๊ธฐ **์ „์—** ์ ์šฉํ•ด์•ผ
# ํˆฌ๊ณผ์œจ๊นŒ์ง€ ํ•จ๊ป˜ ์•ˆ์ •๋ฉ๋‹ˆ๋‹ค.
if self.a_smoothing > 0.0:
if self._a_ema is None:
self._a_ema = a.copy()
else:
k = float(np.clip(self.a_smoothing, 0.0, 0.99))
self._a_ema = k * self._a_ema + (1.0 - k) * a
a = self._a_ema
# t = 1 - omega * darkchannel(I / A)
t_small = 1.0 - self.omega * dark_channel(small / a, patch_s)
if self.use_guided:
gray = cv2.cvtColor((small * 255).astype(np.uint8), cv2.COLOR_BGR2GRAY)
gray = gray.astype(np.float32) / 255.0
t_small = guided_filter(gray, t_small, self.guided_radius, self.guided_eps)
if self.scale < 1.0:
# ํˆฌ๊ณผ์œจ์€ ์ €์ฃผํŒŒ ์‹ ํ˜ธ๋ผ ์—…์ƒ˜ํ”Œํ•ด๋„ ์†์‹ค์ด ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด๊ฒŒ ์„ฑ๋ฆฝํ•˜๋Š”
# ๋•๋ถ„์— ์ถ•์†Œ ์ถ”์ •์ด ์ •๋‹นํ™”๋ฉ๋‹ˆ๋‹ค.
t = cv2.resize(t_small, (w, h), interpolation=cv2.INTER_LINEAR)
else:
t = t_small
return t, a
# ------------------------------------------------------------------
def process(self, bgr: np.ndarray) -> np.ndarray:
"""bgr uint8 -> ๋””ํ—ค์ด์ฆˆ๋œ bgr uint8."""
img_f = bgr.astype(np.float32) / 255.0
t, a = self.estimate_transmission(img_f)
t = np.clip(t, self.t0, 1.0)
self.last_transmission = t
self.last_a = a
# ๋ณต์›์‹ J = (I - A) / t + A
out = (img_f - a) / t[..., None] + a
return np.clip(out * 255.0, 0, 255).astype(np.uint8)
def process_lowlight(
self,
bgr: np.ndarray,
omega: float = 0.8,
t0: float = 0.25,
) -> np.ndarray:
"""์ €์กฐ๋„ ๋ณด์ • (์˜ต์…˜).
"์ €์กฐ๋„ ์˜์ƒ์„ ๋ฐ˜์ „ํ•˜๋ฉด ํ—ค์ด์ฆˆ ์˜์ƒ๊ณผ ํ†ต๊ณ„์ ์œผ๋กœ ๋‹ฎ๋Š”๋‹ค"๋Š” ๊ด€์ฐฐ
(Dong et al., 2011)์„ ์ด์šฉํ•ด **๊ฐ™์€ DCP ์ฝ”๋“œ๋ฅผ ์žฌ์‚ฌ์šฉ**ํ•ฉ๋‹ˆ๋‹ค.
๋ฐ˜์ „ -> ๋””ํ—ค์ด์ฆˆ -> ๋ฐ˜์ „
CLAHE๊ฐ€ ๊ตญ์†Œ ๋Œ€๋น„๋ฅผ ์˜ฌ๋ฆฌ๋Š” ๊ฒƒ๊ณผ ๋‹ฌ๋ฆฌ ์ด์ชฝ์€ ์ „์—ญ ๋ฐ๊ธฐ๋ฅผ ๋Œ์–ด์˜ฌ๋ฆฝ๋‹ˆ๋‹ค.
โ˜… ๋ฐ˜์ „ ์˜์—ญ์€ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋‹ฌ๋ผ์•ผ ํ•ฉ๋‹ˆ๋‹ค (๋กœ์ปฌ ํ…Œ์ŠคํŠธ๋กœ ๋ฐœ๊ฒฌํ•œ ํ•จ์ •).
- `a_max`: ์—ฐ๊ธฐ ์˜์—ญ์—์„œ๋Š” ํ™”์—ผ์— ๋Œ๋ฆฌ๋Š” ๊ฑธ ๋ง‰์œผ๋ ค 0.92๋กœ ์กฐ์˜€์ง€๋งŒ,
๋ฐ˜์ „ ์˜์ƒ์€ ์›๋ž˜ ์ „์ฒด๊ฐ€ ๋ฐ์•„ A๊ฐ€ ์ •๋ง 1.0 ๊ทผ์ฒ˜์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—
0.92๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด t๊ฐ€ ํ•˜ํ•œ๊นŒ์ง€ ๋ˆŒ๋ ค ๊ฒฐ๊ณผ๊ฐ€ **์˜คํžˆ๋ ค ์–ด๋‘์›Œ์ง‘๋‹ˆ๋‹ค.**
- `omega`/`t0`: ๋ฐ˜์ „ ์˜์ƒ์€ 'ํ—ค์ด์ฆˆ'๊ฐ€ ํ™”๋ฉด ์ „์ฒด์— ๊น”๋ฆฐ ์ƒํƒœ๋ผ
๊ฐ•๋„๋ฅผ ๋‚ฎ์ถ”๊ณ (0.8) ํ•˜ํ•œ์„ ์˜ฌ๋ ค์•ผ(0.25) ๋…ธ์ด์ฆˆ๊ฐ€ ์•ˆ ํ„ฐ์ง‘๋‹ˆ๋‹ค.
์กฐ๋ช…์ด ์™„์ „ํžˆ ๊บผ์ง„ ๊ตฌ๊ฐ„์—์„œ๋งŒ ์ผœ์„ธ์š”. ๋…ธ์ด์ฆˆ๊ฐ€ ๊ฐ™์ด ์ฆํญ๋ฉ๋‹ˆ๋‹ค.
"""
sub = DarkChannelDehazer(
omega=omega,
t0=t0,
patch=self.patch,
scale=self.scale,
use_guided=self.use_guided,
guided_radius=self.guided_radius,
guided_eps=self.guided_eps,
a_top_ratio=self.a_top_ratio,
a_max=1.0, # โ˜… ๋ฐ˜์ „ ์˜์—ญ์—์„œ๋Š” ์กฐ์ด์ง€ ์•Š์Œ
sky_ratio=1.0, # ๋ฐ˜์ „๋˜๋ฉด ์œ„/์•„๋ž˜ ์˜๋ฏธ๊ฐ€ ๋’ค์ง‘ํ˜€ ์ œํ•œ์ด ๋ฌด์˜๋ฏธ
)
out = 255 - sub.process(255 - bgr)
self.last_transmission = sub.last_transmission
self.last_a = sub.last_a
return out
# ---------------------------------------------------------------- CLAHE
class ClaheEnhancer:
"""LAB์˜ L ์ฑ„๋„์—๋งŒ CLAHE.
BGR ๊ฐ ์ฑ„๋„์— ๋”ฐ๋กœ ๊ฑธ๋ฉด ์ฑ„๋„๋ณ„ ํžˆ์Šคํ† ๊ทธ๋žจ์ด ์ œ๊ฐ๊ฐ ๋Š˜์–ด๋‚˜ **์ƒ‰์ด ํ‹€์–ด์ง‘๋‹ˆ๋‹ค**.
LAB๋Š” ๋ฐ๊ธฐ(L)์™€ ์ƒ‰(a,b)์ด ๋ถ„๋ฆฌ๋ผ ์žˆ์–ด L๋งŒ ๊ฑด๋“œ๋ฆฌ๋ฉด ์ƒ‰์ƒ์€ ๋ณด์กด๋ฉ๋‹ˆ๋‹ค.
"""
def __init__(self, clip_limit: float = 2.0, tile_grid: tuple[int, int] = (8, 8)):
self.clip_limit = clip_limit
self.tile_grid = tuple(tile_grid)
self._clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=self.tile_grid)
def update(self, clip_limit: float, tile_grid) -> None:
self.clip_limit = clip_limit
self.tile_grid = tuple(tile_grid)
self._clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=self.tile_grid)
def process(self, bgr: np.ndarray) -> np.ndarray:
lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB)
lab[:, :, 0] = self._clahe.apply(lab[:, :, 0])
return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
def apply_gamma(bgr: np.ndarray, gamma: float) -> np.ndarray:
"""๊ฐ๋งˆ ๋ณด์ •: out = (in/255)^gamma * 255.
**gamma < 1 ์ด๋ฉด ๋ฐ์•„์ง€๊ณ , gamma > 1 ์ด๋ฉด ์–ด๋‘์›Œ์ง‘๋‹ˆ๋‹ค.**
๊ฐ๋งˆ๋Š” ๋ฌธํ—Œ๋งˆ๋‹ค ์ง€์ˆ˜๋ฅผ ๋’ค์ง‘์–ด ์“ฐ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด(์–ด๋–ค ์ฝ”๋“œ๋Š” 1/gamma๋ฅผ ์”€)
๋ฐฉํ–ฅ์ด ๋ฐ˜๋Œ€๊ฐ€ ๋˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ์œ„ ์‹์œผ๋กœ ๊ณ ์ •ํ–ˆ๊ณ 
tests/test_dehaze.py::TestGamma ๊ฐ€ ๋ฐฉํ–ฅ์„ ์ž ๊ฐ€๋‘ก๋‹ˆ๋‹ค.
LUT 256๊ฐœ ์กฐํšŒ๋ผ ํ•ด์ƒ๋„์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋น„์šฉ์ด ๊ฑฐ์˜ 0์ž…๋‹ˆ๋‹ค.
"""
if abs(gamma - 1.0) < 1e-3:
return bgr
g = max(gamma, 1e-3)
lut = np.clip((np.arange(256) / 255.0) ** g * 255.0, 0, 255).astype(np.uint8)
return cv2.LUT(bgr, lut)