측면 억제 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""측면 억제 검증 (측면 억제 예제 사다리의 답 포함).
문서: 18.측면 억제, 4.연습문제/18.측면 억제 예제 사다리
모형: 각 세포의 최종 반응 = 자기 수용기 반응 - 이웃마다 (이웃 수용기 반응 x k).
문서 예시는 k = 0.1 (100 -> 10, 20 -> 2). 1차원은 좌우 이웃 둘, 헤르만 격자는 상하좌우 넷.
줄 끝 바깥은 끝 세포와 같은 밝기가 이어진다고 본다.
주장:
1. 마하 띠: 수용기 (100, 100, 100, 20, 20, 20) -> (80, 80, 88, 8, 16, 16).
2. 이 계산은 커널 (-0.1, 1, -0.1)과의 합성곱과 같다.
3. 헤르만 격자: 교차점 A = 100 - 4 x 10 = 60, 두 검은 칸 사이 길 D = 100 - 10 - 2 - 10 - 2 = 76.
실제 격자 영상(길 폭 1칸)에 상하좌우 억제를 걸어도 같은 값이 나온다.
4. 균일한 영역의 반응은 (1 - 2k)배로 줄 뿐이라 영역 사이 비(100:20 = 80:16)가 유지된다.
5. 사다리 문제 3: (50, 50, 50, 150, 150, 150) -> (40, 40, 30, 130, 120, 120).
사다리 문제 4: (20, 20, 100, 20, 20) -> (16, 8, 96, 8, 16).
사다리 변형: k = 0.2, (100, 100, 100, 20, 20, 20) -> 계산값 (60, 60, 76, -4, 12, 12),
발화율은 음수가 될 수 없으므로 D는 0.
6. 신호 관점: 입력이 cos(omega n)이면 출력은 (1 - 2k cos omega) cos(omega n)이다.
k = 0.1이면 고른 빛(omega = 0)은 0.8배, 가장 촘촘한 줄무늬(omega = pi)는 1.2배가 된다.
낮은 주파수보다 높은 주파수를 더 살리는 고주파 강조 필터다.
7. y_n = (1 - 2k) [x_n + (k / (1 - 2k)) (2 x_n - x_{n-1} - x_{n+1})].
전체 밝기를 (1 - 2k)배로 낮춘 것 말고는, 원래 신호에 "이차 차분의 음수"를 더한 선명화 꼴이다.
"""
import math
import random
from fractions import Fraction as F
def inhibit_1d(r, k):
ext = [r[0]] + list(r) + [r[-1]]
return [ext[i] - k * ext[i - 1] - k * ext[i + 1] for i in range(1, len(ext) - 1)]
def convolve_same(r, kernel):
ext = [r[0]] + list(r) + [r[-1]]
return [sum(kernel[j] * ext[i - 1 + j] for j in range(3)) for i in range(1, len(ext) - 1)]
def inhibit_2d(img, k):
h, w = len(img), len(img[0])
def at(y, x):
return img[min(max(y, 0), h - 1)][min(max(x, 0), w - 1)]
return [[img[y][x] - k * (at(y - 1, x) + at(y + 1, x) + at(y, x - 1) + at(y, x + 1))
for x in range(w)] for y in range(h)]
def main() -> None:
k = F(1, 10)
mach = inhibit_1d([100, 100, 100, 20, 20, 20], k)
assert mach == [80, 80, 88, 8, 16, 16], mach
print(f"[OK] 마하 띠: {[int(v) for v in mach]}")
assert convolve_same([100, 100, 100, 20, 20, 20], [-k, 1, -k]) == mach
print("[OK] 커널 (-0.1, 1, -0.1) 합성곱과 같다")
assert 100 - 4 * 10 == 60 and 100 - 10 - 2 - 10 - 2 == 76
# 격자 영상: 검은 칸 3x3개(각 3x3 픽셀, 밝기 20), 길 폭 1픽셀(밝기 100)
size, street = 3, 1
n = 3 * size + 4 * street
img = [[100] * n for _ in range(n)]
for by in range(3):
for bx in range(3):
y0 = street + by * (size + street)
x0 = street + bx * (size + street)
for y in range(y0, y0 + size):
for x in range(x0, x0 + size):
img[y][x] = 20
out = inhibit_2d(img, k)
cross = street + size # 첫 교차점 좌표 (길과 길이 만나는 곳)
between = street + size // 2 # 두 검은 칸 사이 길
assert img[cross][cross] == 100 and out[cross][cross] == 60
assert img[between][cross] == 100 and out[between][cross] == 76
print(f"[OK] 헤르만 격자 영상: 교차점 {out[cross][cross]}, 두 검은 칸 사이 길 {out[between][cross]}")
assert mach[0] / mach[5] == F(100, 20) == 5
print("[OK] 균일한 영역: 80:16 = 100:20 = 5")
assert inhibit_1d([50, 50, 50, 150, 150, 150], k) == [40, 40, 30, 130, 120, 120]
print("[OK] 사다리 문제 3: (40, 40, 30, 130, 120, 120)")
assert inhibit_1d([20, 20, 100, 20, 20], k) == [16, 8, 96, 8, 16]
print("[OK] 사다리 문제 4: (16, 8, 96, 8, 16)")
raw = inhibit_1d([100, 100, 100, 20, 20, 20], F(1, 5))
assert raw == [60, 60, 76, -4, 12, 12]
assert [max(F(0), v) for v in raw] == [60, 60, 76, 0, 12, 12]
print("[OK] 사다리 변형: k=0.2 -> (60, 60, 76, -4, 12, 12), 음수는 0으로")
# 주장 6: 주파수 응답
kf = 0.1
for omega, gain in ((0.0, 0.8), (math.pi / 2, 1.0), (math.pi, 1.2), (math.pi / 3, 0.9)):
xs = [math.cos(omega * n) for n in range(200)]
ys = [xs[n] - kf * (xs[n - 1] + xs[n + 1]) for n in range(1, 199)]
assert all(abs(y - gain * xs[n]) < 1e-12 for y, n in zip(ys, range(1, 199))), omega
print("[OK] 주파수 응답 1 - 2k cos(omega): omega=0 -> 0.8, pi/2 -> 1.0, pi -> 1.2")
# 주장 7: 선명화 꼴 (정확 계산)
rnd = random.Random(0)
for _ in range(100):
xs = [F(rnd.randint(0, 255)) for _ in range(10)]
for n in range(1, 9):
lhs = xs[n] - k * (xs[n - 1] + xs[n + 1])
rhs = (1 - 2 * k) * (xs[n] + (k / (1 - 2 * k)) * (2 * xs[n] - xs[n - 1] - xs[n + 1]))
assert lhs == rhs
print("[OK] 측면 억제 = (1-2k) x [원래 신호 + (k/(1-2k)) x (2x_n - x_(n-1) - x_(n+1))]")
print("ALL CHECKS PASSED")
if __name__ == "__main__":
main()