Files
qianbian/tests/test_image.py
T

160 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""图像管线测试 — 算法参数证据: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