160 lines
6.5 KiB
Python
160 lines
6.5 KiB
Python
"""图像管线测试 — 算法参数证据:analysis/web/timedjs.js(docs/protocol.md §5)。"""
|
||
import pytest
|
||
from PIL import Image
|
||
|
||
from ppclock import image_pipeline as IP
|
||
|
||
|
||
def make_img(w, h, pixels):
|
||
"""pixels: list[(r,g,b)] 长度 w*h,行主序。"""
|
||
img = Image.new("RGB", (w, h))
|
||
img.putdata(pixels)
|
||
return img
|
||
|
||
|
||
class TestPack:
|
||
def test_all_white_bw_plane(self):
|
||
px = [(255, 255, 255)] * 16
|
||
assert IP.pack_plane(px, "bw") == bytes([0xFF, 0xFF])
|
||
|
||
def test_all_black_bw_plane(self):
|
||
px = [(0, 0, 0)] * 16
|
||
assert IP.pack_plane(px, "bw") == bytes(2)
|
||
|
||
def test_msb_first_order(self):
|
||
"""首像素=bit7:仅(0,0)白→0x80;仅第8像素白→0x01;第9像素→第二字节0x80"""
|
||
px = [(255, 255, 255)] + [(0, 0, 0)] * 15
|
||
assert IP.pack_plane(px, "bw") == bytes([0x80, 0x00])
|
||
px = [(0, 0, 0)] * 7 + [(255, 255, 255)] + [(0, 0, 0)] * 8
|
||
assert IP.pack_plane(px, "bw") == bytes([0x01, 0x00])
|
||
px = [(0, 0, 0)] * 8 + [(255, 255, 255)] + [(0, 0, 0)] * 7
|
||
assert IP.pack_plane(px, "bw") == bytes([0x00, 0x80])
|
||
|
||
def test_red_pixel_planes(self):
|
||
"""红(255,0,0):bw 平面=0,红平面=1(timedjs.js:573-576,599-601)"""
|
||
px = [(255, 0, 0)] * 8
|
||
assert IP.pack_plane(px, "bw") == bytes([0x00])
|
||
assert IP.pack_plane(px, "red") == bytes([0xFF])
|
||
|
||
def test_partial_channels_count_as_white(self):
|
||
"""bw 规则:R,G,B 全>0 → 1;(1,1,1) 也是白"""
|
||
px = [(1, 1, 1)] * 8
|
||
assert IP.pack_plane(px, "bw") == bytes([0xFF])
|
||
|
||
def test_length_not_multiple_of_8_raises(self):
|
||
with pytest.raises(ValueError):
|
||
IP.pack_plane([(0, 0, 0)] * 7, "bw")
|
||
|
||
|
||
class TestDither:
|
||
def test_none_threshold(self):
|
||
"""none:gray<threshold→0 否则 255(timedjs.js:42-51)"""
|
||
px = [(100, 100, 100), (200, 200, 200)] * 4
|
||
out = IP.dither_pixels(px, 8, 1, "none", threshold=125)
|
||
assert out == [(0, 0, 0), (255, 255, 255)] * 4
|
||
|
||
def test_none_grayscale_coefficients(self):
|
||
"""灰度=0.299R+0.587G+0.114B:纯绿128 → gray=75.1 <125 → 黑"""
|
||
out = IP.dither_pixels([(0, 128, 0)] * 8, 8, 1, "none", threshold=125)
|
||
assert out == [(0, 0, 0)] * 8
|
||
|
||
def test_red_detected_before_quantize(self):
|
||
"""r>g*1.5 且 r>b*1.5 且 r>threshold → 纯红且不扩散误差"""
|
||
px = [(200, 50, 50)] * 8
|
||
out = IP.dither_pixels(px, 8, 1, "none", threshold=125, tricolor=True)
|
||
assert out == [(255, 0, 0)] * 8
|
||
|
||
def test_not_red_when_threshold_high(self):
|
||
px = [(200, 50, 50)] * 8
|
||
out = IP.dither_pixels(px, 8, 1, "none", threshold=250, tricolor=True)
|
||
# 200>250 假 → 走红判定失败 → 灰度 0.299*200+0.587*50+0.114*50=94.9 <250 → 黑
|
||
assert out == [(0, 0, 0)] * 8
|
||
|
||
def test_floyd_steinberg_error_propagation(self):
|
||
"""2x1 灰100:px0→黑 err=100*7/16=43.75 → px1=143.75→白(阈值125,扩散1.0)"""
|
||
px = [(100, 100, 100)] * 2
|
||
out = IP.dither_pixels(px, 2, 1, "floydsteinberg", threshold=125)
|
||
assert out[0] == (0, 0, 0)
|
||
assert out[1] == (255, 255, 255)
|
||
|
||
def test_diffusion_zero_behaves_like_none(self):
|
||
px = [(100, 100, 100)] * 2
|
||
out = IP.dither_pixels(px, 2, 1, "floydsteinberg", threshold=125, diffusion=0.0)
|
||
assert out == [(0, 0, 0)] * 2
|
||
|
||
def test_atkinson_kernel_smaller_error(self):
|
||
"""Atkinson 只扩散 6/8:px0 黑 err=100*1/8=12.5 → px1=112.5<125 仍黑"""
|
||
px = [(100, 100, 100)] * 2
|
||
out = IP.dither_pixels(px, 2, 1, "atkinson", threshold=125)
|
||
assert out == [(0, 0, 0), (0, 0, 0)]
|
||
|
||
def test_bayer_matrix_origin(self):
|
||
"""bayer (0,0) 阈值=15:灰10→黑,灰20→白(矩阵 timedjs.js:116-136)"""
|
||
out = IP.dither_pixels([(10, 10, 10)] * 8, 8, 1, "bayer", threshold=125)
|
||
assert out[0] == (0, 0, 0)
|
||
out = IP.dither_pixels([(20, 20, 20)] * 8, 8, 1, "bayer", threshold=125)
|
||
assert out[0] == (255, 255, 255)
|
||
|
||
def test_stucki_and_jarvis_run(self):
|
||
"""大核算法冒烟:全灰输入输出只有黑白"""
|
||
px = [(128, 128, 128)] * 64
|
||
for algo in ("stucki", "jarvis"):
|
||
out = IP.dither_pixels(px, 8, 8, algo, threshold=125)
|
||
assert set(out) <= {(0, 0, 0), (255, 255, 255)}
|
||
|
||
def test_unknown_algo_raises(self):
|
||
with pytest.raises(ValueError):
|
||
IP.dither_pixels([(0, 0, 0)] * 8, 8, 1, "magic")
|
||
|
||
|
||
class TestAdjust:
|
||
def test_brightness(self):
|
||
"""亮度+50 → v+=127.5(timedjs.js:9-17)"""
|
||
px = [(100, 100, 100)]
|
||
out = IP.adjust_pixels(px, brightness=50, contrast=0, saturation=100)
|
||
assert out == [(228, 228, 228)] # 100+127.5=227.5 → round=228
|
||
|
||
def test_contrast(self):
|
||
"""对比度+100 → factor=2, v=v*2-128(timedjs.js:18-27)"""
|
||
out = IP.adjust_pixels([(200, 50, 128)], brightness=0, contrast=100, saturation=100)
|
||
assert out == [(255, 0, 128)]
|
||
|
||
def test_saturation_desaturate(self):
|
||
"""饱和度35 → v=gray+(v-gray)*0.35(timedjs.js:28-38)"""
|
||
out = IP.adjust_pixels([(200, 50, 50)], brightness=0, contrast=0, saturation=35)
|
||
gray = 0.299 * 200 + 0.587 * 50 + 0.114 * 50
|
||
exp = round(gray + (200 - gray) * 0.35)
|
||
assert out[0][0] == exp
|
||
|
||
|
||
class TestProcess:
|
||
def test_full_size_output_lengths(self):
|
||
"""400x300 → bw 15000B;三色另有红平面 15000B"""
|
||
img = Image.new("RGB", (400, 300), (255, 255, 255))
|
||
bw, red = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=True)
|
||
assert len(bw) == 15000
|
||
assert len(red) == 15000
|
||
assert bw == bytes([0xFF]) * 15000
|
||
assert red == bytes(15000)
|
||
|
||
def test_mono_has_no_red_plane(self):
|
||
img = Image.new("RGB", (400, 300), (0, 0, 0))
|
||
bw, red = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=False)
|
||
assert len(bw) == 15000
|
||
assert red is None
|
||
|
||
def test_resize_to_fit(self):
|
||
img = Image.new("RGB", (800, 600), (255, 255, 255))
|
||
bw, _ = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=False)
|
||
assert len(bw) == 15000
|
||
|
||
def test_rotate_180(self):
|
||
"""rotate=180:左上角白块移到右下"""
|
||
img = Image.new("RGB", (8, 8), (0, 0, 0))
|
||
for x in range(8):
|
||
img.putpixel((x, 0), (255, 255, 255))
|
||
bw, _ = IP.process_image(img, size=(8, 8), algorithm="none",
|
||
tricolor=False, rotate=180)
|
||
assert bw[0] == 0x00
|
||
assert bw[-1] == 0xFF
|