tests/test_jav_poster_crop.py
Rama master · réplica de lectura del repositorio original.
# -*- coding: utf-8 -*-
"""横版封套里正封取景框的判据。
图都是 Pillow 当场合成的,不依赖真实封面:折痕是一条画上去的竖线,位置已知,
所以「找到的那一列对不对」有确定答案可比。
"""
from __future__ import annotations
import functools
import io
import json
import tempfile
import unittest
from unittest.mock import patch
from pathlib import Path
from PIL import Image
from peach import images, jav_poster_crop
from peach.jav_poster_crop import CENTER, FOLD, MANUAL, MANUAL_SOURCE, NONE, RATIO
class CompressedSpineTests(unittest.TestCase):
def test_a_face_left_of_the_cut_keeps_its_right_side(self):
face = {'cx': .5, 'cy': .2, 'w': .1, 'h': .2, 'score': .9}
box = jav_poster_crop.crop_record('STARS-350', 840, 563, [0.] * 840, face=face)['box']
self.assertEqual(box, {'x0': 0, 'y0': 0, 'x1': 840, 'y1': 563, 'method': NONE})
def test_blurred_padding_around_a_centered_front_is_excluded(self):
profile = jav_poster_crop.ColumnProfile([.02] * 224 + [.2] * 394 + [.02] * 222, [0.] * 840)
profile.seams[224] = profile.seams[618] = .95
face = {'cx': .487, 'cy': .231, 'w': .135, 'h': .242, 'score': .93}
box = jav_poster_crop.crop_record('HHL-018', 840, 563, profile, face=face)['box']
self.assertEqual(box, {'x0': 225, 'y0': 0, 'x1': 618, 'y1': 563, 'method': CENTER})
def test_busy_side_pictures_do_not_form_blurred_padding(self):
profile = jav_poster_crop.ColumnProfile([.2] * 840, [0.] * 840)
profile.seams[224] = profile.seams[618] = .95
self.assertIsNone(jav_poster_crop.padded_center_panel(840, 563, profile))
def test_both_boundaries_are_required_for_blurred_padding(self):
profile = jav_poster_crop.ColumnProfile([.02] * 224 + [.2] * 394 + [.02] * 222, [0.] * 840)
profile.seams[224] = .95
self.assertIsNone(jav_poster_crop.padded_center_panel(840, 563, profile))
def test_soft_spine_edge_is_separated_from_the_lettering(self):
record = json.loads((Path(__file__).parent / 'fixtures/cover-profiles/ipzz-477.json').read_text(encoding='utf-8'))
profile = jav_poster_crop.ColumnProfile(record['gradient'], record['seams'])
box = jav_poster_crop.front_panel_box(*record['px'], profile, code='IPZZ-477')
self.assertEqual(box['method'], FOLD)
self.assertGreaterEqual(box['x0'], 422)
self.assertLessEqual(box['x0'], 423)
def sleeve(width: int, height: int, fold: int | None) -> Image.Image:
"""一张封套:左边是背面,右边是正面,中间可选一条书脊折痕。"""
image = Image.new("RGB", (width, height), (200, 200, 200))
if fold is None:
return image
for x in range(fold, min(fold + 3, width)):
for y in range(height):
image.putpixel((x, y), (0, 0, 0))
for x in range(min(fold + 3, width), width):
for y in range(height):
image.putpixel((x, y), (150, 150, 150))
return image
def centred(width: int, height: int, x0: int, x1: int,
right: tuple[int, int, int] = (60, 60, 60)) -> Image.Image:
"""16:9 拼图:左右两块剧照,正中 `x0`~`x1` 是正封。"""
image = Image.new("RGB", (width, height), (90, 90, 90))
image.paste((200, 200, 200), (x0, 0, x1, height))
image.paste(right, (x1, 0, width, height))
return image
def edge_left(width: int, height: int) -> Image.Image:
"""折痕带之外有一条强边,带内平整:这张图没有可采信的折痕。"""
image = Image.new("RGB", (width, height), (200, 200, 200))
for x in range(0, int(width * 0.1)):
for y in range(height):
image.putpixel((x, y), (10, 10, 10))
return image
def gradient_of(image: Image.Image):
import cv2
import numpy
array = cv2.cvtColor(numpy.array(image), cv2.COLOR_RGB2BGR)
return jav_poster_crop.column_gradient(array)
def opencv_available() -> bool:
try:
import cv2 # noqa: F401
import numpy # noqa: F401
except ImportError:
return False
return True
@functools.lru_cache(maxsize=3)
def batch_script(name='poster_crop_boxes'):
"""按文件路径载入封面维护脚本。"""
import importlib.util
root = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
name, root / 'scripts' / (name + '.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class CropShapeTests(unittest.TestCase):
"""框的形状:从折痕一路到源图的右下角,满高,中间不另取子区域。"""
def assert_reaches_the_corner(self, box: dict, width: int, height: int):
self.assertEqual((box["x1"], box["y0"], box["y1"]), (width, 0, height),
f"框不是「折痕到右缘、满高」:{box}")
self.assertGreater(box["x1"], box["x0"])
self.assertGreaterEqual(box["x0"], 0)
def test_the_box_runs_from_the_fold_to_the_bottom_right_corner(self):
for width, height in ((800, 539), (2184, 1464), (1000, 674), (760, 600)):
with self.subTest(size=(width, height)):
box = jav_poster_crop.front_panel_box(width, height)
self.assertEqual(box["method"], RATIO)
self.assert_reaches_the_corner(box, width, height)
def test_the_prior_puts_the_fold_where_the_front_panel_has_its_real_shape(self):
"""没有梯度可用时,正封的形状就是先验本身——按高度从右缘量回去。"""
for width, height in ((800, 539), (2184, 1464)):
with self.subTest(size=(width, height)):
box = jav_poster_crop.front_panel_box(width, height, [0.0] * width)
self.assertEqual(box["method"], RATIO)
self.assertAlmostEqual((box["x1"] - box["x0"]) / height,
jav_poster_crop.PANEL_ASPECT, places=2)
def test_a_degenerate_size_gives_an_empty_box_and_no_method(self):
self.assertEqual(jav_poster_crop.front_panel_box(0, 0),
{"x0": 0, "y0": 0, "x1": 0, "y1": 0, "method": NONE})
class MethodTests(unittest.TestCase):
"""各档各自的触发条件。"""
@unittest.skipUnless(opencv_available(), "缺 vision 依赖组")
def test_a_visible_spine_is_found_and_the_front_starts_there(self):
width, height, fold = 800, 540, 420
profile = gradient_of(sleeve(width, height, fold))
self.assertIsNotNone(profile)
found = jav_poster_crop.fold_column(width, height, profile)
self.assertIsNotNone(found, "画上去的折痕没被找到")
# 那条三列宽的黑线整条留在框外,正封从它右边开始。
self.assertGreaterEqual(found, fold + 3, found)
self.assertLessEqual(found - (fold + 3), 2, found)
box = jav_poster_crop.front_panel_box(width, height, profile)
self.assertEqual(box["method"], FOLD)
self.assertEqual((box["x0"], box["x1"], box["y0"], box["y1"]),
(found, width, 0, height))
@unittest.skipUnless(opencv_available(), "缺 vision 依赖组")
def test_without_a_spine_the_front_falls_back_to_the_prior_shape(self):
width, height = 800, 540
profile = gradient_of(edge_left(width, height))
self.assertIsNone(jav_poster_crop.fold_column(width, height, profile))
box = jav_poster_crop.front_panel_box(width, height, profile)
self.assertEqual(box["method"], RATIO)
self.assertEqual(box["x0"],
round(width - jav_poster_crop.PANEL_ASPECT * height))
def test_a_missing_gradient_falls_back_to_the_prior_shape(self):
"""OpenCV 不在时取景仍然给得出框,只是退化成先验几何。"""
box = jav_poster_crop.front_panel_box(800, 540, None)
self.assertEqual(box["method"], RATIO)
def test_a_portrait_or_square_image_is_not_cropped(self):
for width, height in ((600, 900), (800, 800), (900, 800)):
with self.subTest(size=(width, height)):
box = jav_poster_crop.front_panel_box(width, height)
self.assertEqual(box, {"x0": 0, "y0": 0, "x1": width, "y1": height,
"method": NONE})
def test_a_sixteen_by_nine_still_is_not_cropped(self):
"""整幅都是画面的官方剧照没有「正面那一块」,按先验切会裁出半张背景。"""
box = jav_poster_crop.front_panel_box(1920, 1080)
self.assertEqual(box["method"], NONE)
@unittest.skipUnless(opencv_available(), "缺 vision 依赖组")
def test_a_centred_front_panel_in_a_still_is_framed_between_its_seams(self):
"""「剧照 | 正封 | 剧照」的 16:9 拼图:框在两条满高拼接缝之间,满高。"""
width, height = 1348, 758
box = jav_poster_crop.front_panel_box(
width, height, gradient_of(centred(width, height, 408, 940)))
self.assertEqual(box["method"], CENTER)
self.assertEqual((box["y0"], box["y1"]), (0, height))
self.assertLessEqual(abs(box["x0"] - 408), 1, box)
self.assertLessEqual(abs(box["x1"] - 940), 1, box)
@unittest.skipUnless(opencv_available(), "缺 vision 依赖组")
def test_one_full_height_seam_is_enough_and_the_other_side_mirrors_it(self):
"""一侧剧照压暗后和正封边缘同色,那一侧没有缝;居中是版式本身,按对称补上。"""
width, height = 1348, 758
box = jav_poster_crop.front_panel_box(
width, height, gradient_of(centred(width, height, 408, 940, right=(200, 200, 200))))
self.assertEqual(box["method"], CENTER)
self.assertEqual(box["x1"], width - box["x0"])
@unittest.skipUnless(opencv_available(), "缺 vision 依赖组")
def test_a_strong_edge_that_stops_short_is_content_not_a_seam(self):
"""边位上的强边只占半截高度:那是画面里的人或物,不是拼接缝。"""
width, height = 1348, 758
image = Image.new("RGB", (width, height), (200, 200, 200))
image.paste((20, 20, 20), (0, 0, 408, height // 2))
box = jav_poster_crop.front_panel_box(width, height, gradient_of(image))
self.assertEqual(box["method"], NONE)
def test_a_still_without_seam_evidence_is_not_cropped(self):
"""只给列梯度、没有接缝覆盖率的调用方,16:9 那一档照旧不给框。"""
profile = [0.0] * 1348
profile[408] = 1.0
self.assertEqual(jav_poster_crop.front_panel_box(1348, 758, profile)["method"], NONE)
def test_the_window_peak_must_be_a_real_cliff(self):
"""窗里总有一个最大值,它不够陡就只是噪声的最高点,按它切会切进画面。"""
profile = [0.0] * 800
profile[80] = 1.0
profile[425] = 0.1
self.assertIsNone(jav_poster_crop.fold_column(800, 540, profile))
profile[425] = 0.9
self.assertIsNotNone(jav_poster_crop.fold_column(800, 540, profile))
def test_a_stronger_edge_outside_the_window_never_wins(self):
"""窗外那道边正是误判的来路:画面里的强边、书脊的另一条边都在窗外。"""
profile = [0.0] * 800
profile[330] = 1.0 # 正封会连着整条书脊
profile[470] = 1.0 # 切进正封,大标题被削掉一截
self.assertIsNone(jav_poster_crop.fold_column(800, 539, profile))
profile[418] = 0.5 # 一列宽的峭壁,斜坡下一列就落回基线
self.assertEqual(jav_poster_crop.fold_column(800, 539, profile), 419)
def test_the_window_is_measured_off_the_height_not_the_width(self):
"""同一个正封形状在低清和高清封套上都要认得出,尽管像素位置差着上千。"""
for width, height in ((800, 539), (2184, 1464)):
with self.subTest(size=(width, height)):
fold = round(width - 0.71 * height)
profile = [0.0] * width
profile[fold] = 1.0
self.assertEqual(jav_poster_crop.fold_column(width, height, profile),
fold + 1)
def test_the_cut_lands_past_the_slope_not_in_the_middle_of_it(self):
"""折痕是一道有宽度的斜坡,梯度的峰在最陡处,书脊最后几列还在它右边。"""
profile = [0.3] * 800 # 画面忙的封套,处处都有梯度响应
for column, value in zip(range(417, 422), (0.35, 0.48, 0.39, 0.44, 0.24)):
profile[column] = value
self.assertEqual(jav_poster_crop.fold_column(800, 539, profile), 421)
def test_the_walk_past_the_slope_is_bounded(self):
"""梯度一路不落回基线时不能一直走下去,最多走源图宽的 1%。"""
profile = [0.0] * 800
profile[418] = 1.0
for column in range(419, 500):
profile[column] = 0.5 # 斜坡右边一直不落回基线
self.assertEqual(jav_poster_crop.fold_column(800, 539, profile), 426)
@staticmethod
def banded_fold(closing_edge: int) -> list[float]:
"""ABF-328 的折痕照着量出来的梯度:书脊边、七列灰色阴影、另一道边才到正封。"""
profile = [0.3] * 2184
profile[1139], profile[1140] = 0.73, 0.56
for column in range(1141, 1147):
profile[column] = 0.2
profile[closing_edge - 1], profile[closing_edge] = 0.56, 0.59
profile[closing_edge + 1] = 0.06
return profile
def test_a_shadow_band_beside_the_fold_stays_outside_the_front(self):
"""两道边之间梯度短暂落回基线;停在那儿,卡片左缘就留下一道灰线。"""
self.assertEqual(jav_poster_crop.fold_column(2184, 1477, self.banded_fold(1148)), 1149)
def test_an_edge_well_inside_the_front_is_content_not_the_band(self):
"""离折痕超过一道边的宽度,那是正封里的标题字或人物边缘,不是阴影带的另一侧。"""
self.assertEqual(jav_poster_crop.fold_column(2184, 1477, self.banded_fold(1160)), 1141)
def test_a_busy_front_beside_a_weak_fold_is_not_mistaken_for_the_band(self):
"""折痕本身偏弱时,正封里一片花哨的画面处处都和它相当,那些边都不够峭壁。"""
profile = [0.1] * 2184
profile[100] = 1.0 # 窗外画面里最强的那道边
profile[1139] = 0.4
for column in range(1141, 1150):
profile[column] = 0.3 if column % 2 else 0.32
self.assertEqual(jav_poster_crop.fold_column(2184, 1477, profile), 1140)
def test_a_thick_spine_puts_both_of_its_edges_in_the_window(self):
"""书脊厚到两条边都落进窗里时按形状挑:左边那条更强,右边那条才是折痕。"""
profile = [0.0] * 3762
profile[1939] = 0.57 # 书脊左缘,挨着封底的留白
profile[1978] = 0.46 # 书脊右缘,正封从这里开始
self.assertEqual(jav_poster_crop.fold_column(3762, 2535, profile), 1979)
def test_an_edge_too_weak_to_be_a_rival_never_gets_to_vote(self):
"""形状更像正封不足以让一条弱边胜出:强度差太多的根本不进候选。"""
profile = [0.0] * 3762
profile[1939] = 0.57
profile[1978] = 0.20 # 不到窗内最强边的七成
self.assertEqual(jav_poster_crop.fold_column(3762, 2535, profile), 1940)
def test_a_profile_that_does_not_match_the_image_is_ignored(self):
self.assertIsNone(jav_poster_crop.fold_column(800, 540, [1.0] * 400))
class SeamBeyondTests(unittest.TestCase):
"""厚书脊里的文字边压过了折痕时,切点右边那条满高缝才是折痕。
ABW-147 的形状:3762×2535,书脊 1922~1978 列,窗内最强的边是书脊文字的 1939,
书脊右缘 1978 的梯度不到它的七成、进不了候选,可它从上贯到下,文字边不满高。
"""
WIDTH, HEIGHT = 3762, 2535
TEXT_EDGE, SPINE_EDGE = 1939, 1978
def profile(self, seam_at_text: float = 0.36, spine_edge: int = SPINE_EDGE,
spine_strength: float = 0.66, spine_seam: float = 0.91):
gradient = [0.0] * self.WIDTH
seams = [0.0] * self.WIDTH
gradient[self.TEXT_EDGE], seams[self.TEXT_EDGE] = 0.99, seam_at_text
gradient[spine_edge], seams[spine_edge] = spine_strength, spine_seam
return jav_poster_crop.ColumnProfile(gradient, seams)
def test_a_full_height_seam_right_of_a_cut_that_is_not_one_becomes_the_fold(self):
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT, self.profile()),
self.SPINE_EDGE + 1)
def test_a_cut_that_is_itself_a_full_height_seam_stays_where_it_is(self):
"""正封里紧挨折痕的边框、色带同样满高;折痕已经满高就没有理由再往右走。"""
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(seam_at_text=0.9)),
self.TEXT_EDGE + 1)
def test_a_seam_outside_the_window_is_content_not_the_fold(self):
"""窗外的满高缝切出来的正封形状不对,那是正封里的一条竖线。"""
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(spine_edge=2100)),
self.TEXT_EDGE + 1)
def test_a_seam_left_of_the_cut_is_never_looked_at(self):
"""书脊左缘也是满高缝,往左看会把整条书脊带回框里。"""
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(spine_edge=1900, spine_strength=0.5)),
self.TEXT_EDGE + 1)
def test_a_faint_seam_that_is_no_cliff_does_not_pull_the_cut(self):
"""淡淡的印刷分隔线能满高,但过不了峭壁那一关。"""
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(spine_strength=0.2)),
self.TEXT_EDGE + 1)
def test_a_bare_gradient_without_seams_behaves_as_before(self):
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT, list(self.profile())),
self.TEXT_EDGE + 1)
@unittest.skipUnless(opencv_available(), "需要 OpenCV")
def test_a_lettered_spine_is_cut_at_its_far_edge_from_the_image_itself(self):
"""从画面量:书脊底色上一列竖排文字,文字边和书脊右缘都在窗里,正封从右缘起。"""
width, height, spine, edge = 800, 538, 400, 430
image = Image.new("RGB", (width, height), (40, 40, 40)) # 封底和书脊一样暗,左缘弱
image.paste((20, 20, 20), (spine, 0, edge, height))
image.paste((120, 120, 120), (edge, 0, width, height))
for y in range(0, height, 6): # 书脊里的白色文字:窗内最强、却不满高的边
image.paste((255, 255, 255), (spine + 12, y, spine + 15, min(y + 4, height)))
box = jav_poster_crop.front_panel_box(width, height, gradient_of(image))
self.assertEqual(box["method"], FOLD)
self.assertGreaterEqual(box["x0"], edge)
self.assertLessEqual(box["x0"], edge + 3)
class SharpFoldTests(unittest.TestCase):
"""书脊里竖排片名是一片宽的高地,折痕是一条窄线:按突出度折痕才是窗里最显眼的那条。
DDK-023 的形状:800×539,折痕窗 390~433,书脊片名的高地 391~408 最高 0.50,
折痕 419 列只有 0.34(不到高地七成),满高覆盖 0.68(不到 `_seam_beyond` 的 0.75)。
"""
WIDTH, HEIGHT, FOLD_AT = 800, 539, 419
def profile(self, fold_seam: float = 0.68):
gradient = [0.1] * self.WIDTH
seams = [0.3] * self.WIDTH
gradient[3] = 1.0 # 封底左缘,全图最强
for column in range(391, 409): # 片名那片高地
gradient[column] = 0.5 - abs(column - 401) * 0.01
gradient[self.FOLD_AT], gradient[self.FOLD_AT + 1] = 0.34, 0.28
seams[self.FOLD_AT] = fold_seam
return jav_poster_crop.ColumnProfile(gradient, seams)
def test_a_narrow_fold_beside_a_lettered_plateau_is_the_fold(self):
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT, self.profile()),
self.FOLD_AT + 2)
def test_a_narrow_edge_that_is_not_full_height_is_front_lettering(self):
"""正封里紧挨折痕的大字也是窄尖峰,但只占几行,不进候选。"""
self.assertLess(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(fold_seam=0.3)),
self.FOLD_AT)
class SharpBeyondTests(unittest.TestCase):
"""书脊和正封同底色时折痕只在书脊内容所在的行里存在:切点右边那根孤立尖峰才是折痕。
IPZ-602 的形状:800×536,折痕窗 393~436,书脊内一块带边框的面板与竖排文字形成
390~409 的高地(最高 0.51,满高覆盖 0.5 上下),按形状选中高地右端、切在 410;
折痕 420 列梯度 0.24、满高覆盖 0.20,左边面板内部 0.06~0.11、右边正封留白 0.02~0.04,
周围 16 列中位数 0.065,折痕是它的 3.7 倍。合成剖面左侧取 0.07、右侧 0.02(3.4 倍)。
"""
WIDTH, HEIGHT, FOLD_AT = 800, 536, 420
def profile(self, left: float = 0.07, right: float = 0.02, panel_seam: float = 0.5,
seams: bool = True):
gradient = [0.05] * self.WIDTH
coverage = [0.1] * self.WIDTH
gradient[3] = 1.0 # 封底左缘,全图最强
for column in range(390, 410): # 面板边框与文字那片高地
gradient[column] = 0.4
coverage[column] = panel_seam
gradient[396], gradient[404] = 0.51, 0.45
for column in range(410, self.FOLD_AT): # 面板内部到折痕
gradient[column] = left
gradient[self.FOLD_AT], gradient[self.FOLD_AT + 1] = 0.24, 0.14
coverage[self.FOLD_AT] = 0.2
for column in range(self.FOLD_AT + 2, 440): # 正封左缘
gradient[column] = right
return jav_poster_crop.ColumnProfile(gradient, coverage) if seams else gradient
def test_an_isolated_edge_past_a_bordered_panel_is_the_fold(self):
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT, self.profile()),
self.FOLD_AT + 2)
def test_an_edge_that_rises_into_front_content_is_the_start_of_lettering(self):
"""左边留白、右边忙:那是正封内容的起笔,切点已经在折痕上(PPT-018 的金色片名)。
右侧取 0.12 而不是更高:周围中位数是左右两侧合起来算的,右侧再高,尖峰就先在
孤立度上被拦下,方向判据反而没被测到。
"""
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(left=0.02, right=0.12)),
410)
def test_an_edge_among_other_strokes_is_not_isolated(self):
"""周围中位数压不下去的尖峰是大字里的一笔,哪怕左边比右边忙(MIMK-009 的 423 列)。"""
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(left=0.12, right=0.09)),
410)
def test_a_cut_that_is_already_a_full_height_seam_stays(self):
"""高地本身满高就是折痕,右边再孤立的尖峰也是正封内容。"""
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(panel_seam=0.8)),
410)
def test_a_bare_gradient_without_seams_behaves_as_before(self):
self.assertEqual(jav_poster_crop.fold_column(self.WIDTH, self.HEIGHT,
self.profile(seams=False)),
410)
class CodeShapeTests(unittest.TestCase):
"""哪些番号的封面是横版封套。判据全部走 `catalog_rules` 现有函数。"""
def test_studio_and_amateur_codes_are_sleeves(self):
for code in ("ABW-232", "abw-232", "278GYAN-017", "259LUXU-1475"):
with self.subTest(code=code):
self.assertTrue(jav_poster_crop.crops_to_portrait(code))
def test_finished_landscape_artwork_is_not_cropped(self):
for code in ("HEYZO-1380", "FC2-PPV-1812235", "FC2-1812235",
"040221-001", "092620-001"):
with self.subTest(code=code):
self.assertFalse(jav_poster_crop.crops_to_portrait(code))
def test_non_jav_and_korean_codes_are_not_cropped(self):
for code in ("", None, "20211103_JENNIFERMENDEZ", "HHD800",
"AR-101", "NOAH-101"):
with self.subTest(code=code):
self.assertFalse(jav_poster_crop.crops_to_portrait(code))
def test_a_western_release_is_stopped_by_the_shape_of_its_artwork(self):
"""欧美片的编号和厂牌番号同形,番号这一关拦不住它,宽高比这一关拦得住。
它们的封面是成品横版剧照,落在 16:9 一档,所以走的是 `none`。
"""
self.assertTrue(jav_poster_crop.crops_to_portrait("BLACKED-2021"))
self.assertEqual(
jav_poster_crop.crop_record("BLACKED-2021", 1920, 1080)["box"]["method"],
NONE)
class WideSleeveTests(unittest.TestCase):
"""Blu-ray 模板的宽封套:厂牌开门,比例决定走不走,正封形状另有一套先验。"""
WIDE = (750, 419)
def test_only_the_listed_labels_open_the_door(self):
for code in ("CWPBD-126", "cwpbd-119", "SMBD-110"):
with self.subTest(code=code):
self.assertTrue(jav_poster_crop.is_wide_sleeve_label(code))
for code in ("IDBD-446", "PBD-390", "ABW-232", "259LUXU-1475", "", None):
with self.subTest(code=code):
self.assertFalse(jav_poster_crop.is_wide_sleeve_label(code))
def test_a_wide_sleeve_of_a_listed_label_is_cut_at_its_own_prior(self):
"""没有梯度时按宽封套的先验从右缘量回去,不是 DVD 那一档的 0.704。"""
width, height = self.WIDE
box = jav_poster_crop.front_panel_box(width, height, None, "CWPBD-126")
self.assertEqual(box["method"], RATIO)
self.assertEqual(box["x0"], round(width - jav_poster_crop.WIDE_PANEL_ASPECT * height))
self.assertEqual((box["x1"], box["y0"], box["y1"]), (width, 0, height))
def test_the_same_image_without_the_label_is_still_a_still(self):
"""比例一样的图,厂牌不在名单里就照旧当 16:9 剧照:这一档全靠厂牌开门。"""
width, height = self.WIDE
for code in ("ABW-232", None):
with self.subTest(code=code):
box = jav_poster_crop.front_panel_box(width, height, None, code)
self.assertEqual(box["method"], NONE)
def test_a_listed_label_with_a_dvd_shaped_cover_walks_the_dvd_path(self):
"""厂牌只开门,不定切法:同一厂牌哪一期换成 DVD 比例的封套,仍按那一档的形状切;
比例超出宽封套区间的仍是剧照。"""
record = jav_poster_crop.crop_record("CWPBD-126", 800, 540, [0.0] * 800)
self.assertEqual(record["box"]["method"], RATIO)
self.assertEqual(record["box"]["x0"],
round(800 - jav_poster_crop.PANEL_ASPECT * 540))
self.assertEqual(jav_poster_crop.crop_record("SMBD-110", 1900, 1000)["box"]["method"],
NONE)
@unittest.skipUnless(opencv_available(), "缺 vision 依赖组")
def test_a_spine_inside_the_narrow_window_is_found(self):
width, height = self.WIDE
fold = round(width - 0.855 * height)
profile = gradient_of(sleeve(width, height, fold))
box = jav_poster_crop.front_panel_box(width, height, profile, "SMBD-172")
self.assertEqual(box["method"], FOLD)
# 那条三列宽的黑线整条留在框外,正封从它右边开始。
self.assertGreaterEqual(box["x0"], fold + 3, box)
self.assertLessEqual(box["x0"] - (fold + 3), 2, box)
@unittest.skipUnless(opencv_available(), "缺 vision 依赖组")
def test_an_edge_outside_the_narrow_window_is_content_not_the_spine(self):
"""书脊上竖排的片名字边比书脊右缘强,但它切出的形状不在窗里,不按它切。"""
width, height = self.WIDE
text_edge = round(width - 0.90 * height)
profile = gradient_of(sleeve(width, height, text_edge))
box = jav_poster_crop.front_panel_box(width, height, profile, "CWPBD-126")
self.assertEqual(box["method"], RATIO)
self.assertEqual(box["x0"], round(width - jav_poster_crop.WIDE_PANEL_ASPECT * height))
def test_the_record_routes_the_code_into_the_shape_decision(self):
record = jav_poster_crop.crop_record("SMBD-110", *self.WIDE, [0.0] * self.WIDE[0])
self.assertEqual(record["box"]["method"], RATIO)
self.assertEqual(record["px"], list(self.WIDE))
class RecordTests(unittest.TestCase):
"""边车的内容、读写与失效判定。"""
def setUp(self):
self.root = Path(tempfile.mkdtemp()).resolve()
self.cover = self.root / "ABW-232.jpg"
sleeve(800, 540, 420).save(self.cover)
def test_a_shape_that_is_not_cropped_never_touches_the_gradient(self):
"""番号形态先判:不该裁的图连解码都不做。"""
calls = []
def source():
calls.append(1)
return [0.0] * 800
record = jav_poster_crop.crop_record("HEYZO-1380", 800, 540, source)
self.assertEqual(record["box"]["method"], NONE)
self.assertEqual(calls, [])
def test_the_record_carries_the_version_the_size_and_the_box(self):
record = jav_poster_crop.crop_record("ABW-232", 800, 540, [0.0] * 800)
self.assertEqual(record["version"], jav_poster_crop.ALGORITHM_VERSION)
self.assertEqual(record["px"], [800, 540])
self.assertEqual(record["box"]["method"], RATIO)
def test_the_sidecar_sits_beside_the_cover_and_round_trips(self):
record = jav_poster_crop.crop_record("ABW-232", 800, 540, [0.0] * 800)
path = jav_poster_crop.write_sidecar(self.cover, record)
self.assertEqual(path.name, "ABW-232.poster.json")
self.assertEqual(jav_poster_crop.read_sidecar(self.cover), record)
self.assertTrue(jav_poster_crop.is_current(record, 800, 540))
def test_an_outdated_version_or_a_replaced_cover_forces_a_recount(self):
record = jav_poster_crop.crop_record("ABW-232", 800, 540, [0.0] * 800)
self.assertFalse(jav_poster_crop.is_current(record, 2184, 1464),
"封面被更大的那张换掉之后,旧框落在新图上是一块错位的区域")
stale = dict(record, version="poster-crop-v0")
self.assertFalse(jav_poster_crop.is_current(stale, 800, 540))
self.assertFalse(jav_poster_crop.is_current(None, 800, 540))
def test_a_broken_sidecar_reads_as_missing(self):
jav_poster_crop.sidecar_path(self.cover).write_text("{not json",
encoding="utf-8")
self.assertIsNone(jav_poster_crop.read_sidecar(self.cover))
class ProjectionTests(unittest.TestCase):
"""API 字段的投影:只有真的算出框的那一档才给值。"""
def test_a_real_box_projects_with_the_source_size(self):
record = jav_poster_crop.crop_record("ABW-232", 800, 540, [0.0] * 800)
projected = jav_poster_crop.projection(record)
self.assertEqual(projected["px"], [800, 540])
self.assertEqual(projected["method"], RATIO)
self.assertEqual((projected["x1"], projected["y0"], projected["y1"]),
(800, 0, 540))
def test_everything_unusable_projects_to_none(self):
for record in (None, {}, {"version": "poster-crop-v0"},
jav_poster_crop.crop_record("HEYZO-1380", 800, 540),
{"version": jav_poster_crop.ALGORITHM_VERSION,
"px": [800, 540], "box": {"x0": 0, "x1": 0, "y0": 0,
"y1": 0, "method": RATIO}},
{"version": jav_poster_crop.ALGORITHM_VERSION,
"px": [800, 540], "box": {"x0": 0, "x1": 900, "y0": 0,
"y1": 540, "method": RATIO}}):
with self.subTest(record=record):
self.assertIsNone(jav_poster_crop.projection(record))
class ManualBoxTests(unittest.TestCase):
"""人在详情页自己框的那一块,与算出来的框共用同一份边车。"""
def test_a_hand_drawn_box_records_its_four_edges_and_its_source(self):
record = jav_poster_crop.manual_record(800, 540, {"x0": 200, "y0": 40,
"x1": 600, "y1": 500})
self.assertEqual(record["box"], {"x0": 200, "y0": 40, "x1": 600, "y1": 500,
"method": MANUAL})
self.assertEqual(record["px"], [800, 540])
self.assertEqual(record["source"], MANUAL_SOURCE)
self.assertTrue(jav_poster_crop.is_manual(record))
self.assertEqual(jav_poster_crop.projection(record)["method"], MANUAL)
def test_a_box_that_is_not_a_box_is_refused_before_it_reaches_the_disk(self):
for box in (None, {}, {"x0": 0, "y0": 0, "x1": 0, "y1": 100},
{"x0": "左", "y0": 0, "x1": 100, "y1": 100},
# 夹回图里之后只剩一条线:整幅右侧之外的框没有内容可取。
{"x0": 900, "y0": 0, "x1": 1000, "y1": 100}):
with self.subTest(box=box):
self.assertIsNone(jav_poster_crop.manual_record(800, 540, box))
def test_edges_outside_the_image_are_pulled_back_in(self):
record = jav_poster_crop.manual_record(800, 540, {"x0": -20, "y0": -5,
"x1": 1200, "y1": 900})
self.assertEqual(record["box"]["x0"], 0)
self.assertEqual((record["box"]["x1"], record["box"]["y1"]), (800, 540))
def test_a_hand_drawn_box_survives_an_algorithm_bump_but_not_a_new_cover(self):
"""手工框后面没有算法,改判据不构成重算它的理由;换了封面它就作废。"""
record = jav_poster_crop.manual_record(800, 540, {"x0": 200, "y0": 0,
"x1": 600, "y1": 540})
stale = dict(record, version="poster-crop-v0")
self.assertTrue(jav_poster_crop.is_current(stale, 800, 540))
self.assertIsNotNone(jav_poster_crop.projection(stale))
# 算出来的框换个版本号就该重算,两档判据不能混。
computed = dict(jav_poster_crop.crop_record("ABW-232", 800, 540, [0.0] * 800),
version="poster-crop-v0")
self.assertFalse(jav_poster_crop.is_current(computed, 800, 540))
self.assertIsNone(jav_poster_crop.projection(computed))
# 封面被更大的那张换掉:框描述的是另一张图,手工框也一样作废。
self.assertFalse(jav_poster_crop.is_current(record, 1600, 1080))
class CropBytesTests(unittest.TestCase):
"""按框切出新字节:原图不动,格式只在 PNG 与 JPEG 之间取一个。"""
@staticmethod
def payload(image: Image.Image, fmt: str) -> bytes:
buffer = io.BytesIO()
image.save(buffer, format=fmt)
return buffer.getvalue()
def test_a_jpeg_crop_comes_back_as_a_jpeg_of_the_boxed_size(self):
body = self.payload(Image.new("RGB", (800, 540), (10, 120, 200)), "JPEG")
cropped = images.crop_to_box(body, (200, 40, 600, 500))
self.assertEqual(images.measure_image_size(cropped), (400, 460))
with Image.open(io.BytesIO(cropped)) as opened:
self.assertEqual(opened.format, "JPEG")
def test_a_png_stays_a_png_and_transparency_lands_on_white(self):
source = Image.new("RGBA", (100, 100), (0, 0, 0, 0))
cropped = images.crop_to_box(self.payload(source, "PNG"), (0, 0, 50, 50))
with Image.open(io.BytesIO(cropped)) as opened:
self.assertEqual(opened.format, "PNG")
# 同一张透明图存成 JPEG 那一路要铺白底,否则透明处落成黑块。
flattened = images.crop_to_box(self.payload(source.convert("RGBA"), "WEBP"),
(0, 0, 50, 50))
with Image.open(io.BytesIO(flattened)) as opened:
self.assertEqual(opened.convert("RGB").getpixel((10, 10)), (255, 255, 255))
def test_a_box_outside_the_image_or_a_junk_payload_yields_nothing(self):
body = self.payload(Image.new("RGB", (100, 100)), "JPEG")
self.assertIsNone(images.crop_to_box(body, (0, 0, 200, 50)))
self.assertIsNone(images.crop_to_box(body, (50, 0, 50, 50)))
self.assertIsNone(images.crop_to_box(b"not an image", (0, 0, 10, 10)))
class BatchScriptTests(unittest.TestCase):
"""批处理只写边车、只处理该处理的那些图。"""
def setUp(self):
self.script = batch_script()
self.root = Path(tempfile.mkdtemp()).resolve()
self.cover = self.root / "ABW-232.jpg"
sleeve(800, 540, 420).save(self.cover)
self.before = self.cover.read_bytes()
def run_script(self, *argv):
return self.script.main([*argv, "--covers", str(self.root)])
def test_a_dry_run_writes_nothing(self):
self.assertEqual(self.run_script(), 0)
self.assertFalse(jav_poster_crop.sidecar_path(self.cover).exists())
def test_online_upgrade_checks_official_sources_without_local_snapshots(self):
from peach.jav_cover_fetch import Candidate
script = batch_script('upgrade_audited_covers')
candidate = Candidate('pics.dmm.co.jp', 'https://pics.dmm.co.jp/a.jpg', '')
report = self.root / 'report'
with patch.object(script, 'best_cover', return_value=(candidate, (2184, 1468), self.before)) as fetch:
with patch.object(script, 'install_cover') as install:
result = script.upgrade(self.cover, [], object(), report, live=True,
metadata_root=self.root / 'metadata')
self.assertEqual(result['status'], 'installed')
self.assertEqual(fetch.call_args.args[1], 'ABW-232')
self.assertEqual(fetch.call_args.kwargs['metadata_root'], self.root / 'metadata')
self.assertEqual(install.call_args.kwargs['evidence']['resolver'], 'official-live-upgrade-v1')
self.assertEqual((report / 'originals/ABW-232.jpg').read_bytes(), self.before)
def test_upgrade_resets_connections_only_for_retryable_source_failures(self):
from peach.jav_cover_fetch import NotFound, Unavailable
from unittest.mock import Mock
script = batch_script('upgrade_audited_covers')
for error, expected in ((NotFound('HTTP 404'), False), (Unavailable('HTTP 503'), True)):
transport = Mock()
result = script.failure_result('ABW-232', error, transport)
self.assertEqual(result['retryable'], expected)
self.assertEqual(transport.renew.call_count, int(expected))
def test_javdb_quarantine_preserves_bytes_and_resumes_without_duplicates(self):
covers = self.root / 'generated/covers'
covers.mkdir(parents=True)
target = covers / 'MIDV-377.jpg'
target.write_bytes(self.before)
evidence = target.with_suffix('.scraping.json')
evidence.write_text(json.dumps({'source': 'javdb', 'source_url': 'https://c0.jdbstatic.com/a.jpg'}), encoding='utf-8')
official = covers / 'SPONE-001.jpg'
official.write_bytes(self.before)
report = self.root / 'review/quarantine'
args = ['--data-root', str(self.root), '--report', str(report)]
script = batch_script('quarantine_javdb_covers')
self.assertEqual(script.main(args), 0)
self.assertTrue(target.exists())
self.assertEqual(script.main([*args, '--apply']), 0)
self.assertFalse(target.exists())
self.assertFalse(evidence.exists())
self.assertEqual((report / 'originals/MIDV-377.jpg').read_bytes(), self.before)
self.assertEqual(official.read_bytes(), self.before)
self.assertEqual(script.main([*args, '--apply']), 0)
result = json.loads((report / 'manifest.json').read_text(encoding='utf-8'))
self.assertEqual((result['matched'], result['quarantined']), (0, 1))
def test_quarantine_collision_does_not_partially_move_an_item(self):
covers = self.root / 'generated/covers'
covers.mkdir(parents=True)
target = covers / 'MIDV-377.jpg'
target.write_bytes(self.before)
evidence = target.with_suffix('.scraping.json')
evidence.write_text(json.dumps({'source': 'javdb'}), encoding='utf-8')
report = self.root / 'review/quarantine'
(report / 'originals').mkdir(parents=True)
(report / 'originals/MIDV-377.scraping.json').write_text('{}', encoding='utf-8')
with self.assertRaises(FileExistsError):
batch_script('quarantine_javdb_covers').main([
'--data-root', str(self.root), '--report', str(report), '--apply'])
self.assertEqual(target.read_bytes(), self.before)
self.assertTrue(evidence.exists())
def test_audit_keeps_manual_frames_and_reports_unknown_provenance(self):
manual = jav_poster_crop.manual_record(800, 540, {'x0': 420, 'y0': 0, 'x1': 800, 'y1': 540})
jav_poster_crop.write_sidecar(self.cover, manual)
script = batch_script('audit_cover_artwork')
row, proposed, _, _ = script.inspect(self.cover, self.root / 'metadata')
self.assertEqual(proposed, manual)
self.assertEqual(row['method'], MANUAL)
self.assertTrue('missing_provenance' in row['reasons'])
self.assertEqual(self.cover.read_bytes(), self.before)
def test_face_crossing_the_spine_is_preserved_by_offline_reframing(self):
target = self.root / 'SPONE-001.jpg'
Image.new('RGB', (800, 600), 'gray').save(target)
target.with_suffix('.face.json').write_text(json.dumps({
'px': [800, 600], 'face': {'cx': .555, 'cy': .331, 'w': .257, 'h': .422, 'score': .918},
}), encoding='utf-8')
row, proposed, _, layout = batch_script('audit_cover_artwork').inspect(target, self.root / 'metadata')
self.assertEqual((proposed['box']['method'], layout), (NONE, 'still'))
self.assertEqual(row['x0'], 0)
def test_offline_reframing_preserves_a_centered_front_with_padding(self):
from peach.cover_artwork import FACE_ALGORITHM_VERSION
target = self.root / 'HHL-018.jpg'
Image.new('RGB', (840, 563), 'gray').save(target)
target.with_suffix('.face.json').write_text(json.dumps({
'version': FACE_ALGORITHM_VERSION, 'px': [840, 563],
'face': {'cx': .487, 'cy': .231, 'w': .135, 'h': .242, 'score': .93},
}), encoding='utf-8')
profile = jav_poster_crop.ColumnProfile([.02] * 224 + [.2] * 394 + [.02] * 222, [0.] * 840)
profile.seams[224] = profile.seams[618] = .95
with patch.object(jav_poster_crop, 'file_gradient', return_value=profile):
row, proposed, _, layout = batch_script('audit_cover_artwork').inspect(target, self.root / 'metadata')
self.assertEqual((proposed['box']['method'], layout), (CENTER, ''))
self.assertEqual((row['x0'], proposed['box']['x1']), (225, 618))
def test_audit_refreshes_legacy_front_only_faces_before_classifying_the_cover(self):
from peach.face_detect import Face
from peach.cover_artwork import FACE_ALGORITHM_VERSION
from unittest.mock import Mock
target = self.root / 'SPONE-001.jpg'
Image.new('RGB', (800, 600), 'gray').save(target)
target.with_suffix('.face.json').write_text(json.dumps({
'px': [800, 600], 'face': {'cx': .8, 'cy': .3, 'w': .06, 'h': .1, 'score': .9},
}), encoding='utf-8')
detector = Mock()
detector.detect.return_value = [Face(.555, .331, .257, .422, .918), Face(.8, .3, .06, .1, .9)]
row, proposed, face, layout = batch_script('audit_cover_artwork').inspect(target, self.root / 'metadata', detector=detector)
self.assertEqual((proposed['box']['method'], layout), (NONE, 'still'))
self.assertEqual((face['version'], face['face']['cx']), (FACE_ALGORITHM_VERSION, .555))
self.assertNotIn('stale_face', row['reasons'])
def test_face_cache_requires_current_algorithm_and_image_dimensions(self):
from peach.cover_artwork import FACE_ALGORITHM_VERSION
script = batch_script('detect_cover_faces')
target = self.cover.with_suffix('.face.json')
for version, size, expected in (('old', [800, 540], False),
(FACE_ALGORITHM_VERSION, [1, 1], False), (FACE_ALGORITHM_VERSION, [800, 540], True)):
target.write_text(json.dumps({'version': version, 'px': size}), encoding='utf-8')
self.assertEqual(script.current_face(self.cover), expected)
def test_apply_writes_the_sidecar_and_leaves_the_image_untouched(self):
self.assertEqual(self.run_script("--apply"), 0)
record = json.loads(
jav_poster_crop.sidecar_path(self.cover).read_text(encoding="utf-8"))
self.assertEqual(record["px"], [800, 540])
self.assertIn(record["box"]["method"], (FOLD, RATIO))
self.assertEqual(self.cover.read_bytes(), self.before,
"取景是边车元数据,原图一个字节都不动")
@unittest.skipUnless(opencv_available(), "缺 vision 依赖组")
def test_a_centred_still_is_counted_in_its_own_bucket(self):
"""16:9 居中拼图是单独一档,批处理照样计数、写边车,不因为认不得这档停下。"""
still = self.root / "PASN-027.jpg"
centred(1348, 758, 408, 940).save(still)
self.assertEqual(self.run_script("--apply"), 0)
self.assertEqual(jav_poster_crop.read_sidecar(still)["box"]["method"], CENTER)
def test_a_current_sidecar_is_skipped_and_a_stale_one_is_redone(self):
self.run_script("--apply")
self.assertEqual(self.script.pending([self.cover], False), [])
stale = jav_poster_crop.read_sidecar(self.cover)
jav_poster_crop.write_sidecar(self.cover, dict(stale, version="poster-crop-v0"))
self.assertEqual(self.script.pending([self.cover], False),
[(self.cover, (800, 540))])
def test_an_unreadable_cover_is_counted_and_never_given_a_box(self):
broken = self.root / "BAD-001.jpg"
broken.write_bytes(b"not an image")
self.assertEqual(self.run_script("--apply"), 0)
self.assertFalse(jav_poster_crop.sidecar_path(broken).exists())
class ImageSizeTests(unittest.TestCase):
def test_the_size_probe_reports_none_for_junk(self):
root = Path(tempfile.mkdtemp()).resolve()
broken = root / "x.jpg"
broken.write_bytes(b"nope")
self.assertIsNone(batch_script().image_size(broken))
good = root / "y.jpg"
buffer = io.BytesIO()
Image.new("RGB", (40, 30)).save(buffer, "JPEG")
good.write_bytes(buffer.getvalue())
self.assertEqual(batch_script().image_size(good), (40, 30))
if __name__ == "__main__":
unittest.main()