Coverage for paintmaskanimator/display_shrink.py: 86%
39 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
1"""縮小表示のしかた(Issue #46)。
3キャンバス全体が見えるところまで縮めると、線が目に見えて荒れる。表示は常に
4ニアレストネイバー(``FastTransformation``)で縮めており、1画素の線は縮小率
5しだいで拾われたり落ちたりするためで、線が途切れて見えるのも同じ理由による。
7ここでは「線を残す」縮小を用意する。半分ずつ縮めながら、2×2 のうち最も不透明
8(同じ不透明さなら最も暗い)画素を残す。細い線が消えず、太りもしない。ソース
9の画素は触らず、表示のためだけに使う。
10"""
11import numpy as np
12from PySide6.QtGui import QImage
14from . import imaging
16#: 設定値。``nearest`` が従来どおりの表示。
17MODE_NEAREST = "nearest"
18MODE_KEEP_LINES = "keep_lines"
19MODES = (MODE_NEAREST, MODE_KEEP_LINES)
22def _winners(a, b):
23 """2枚の RGBA 配列から、画素ごとに「線らしい方」を選ぶマスクを返す。
25 不透明な方を優先し、同じ不透明さなら暗い方(線は背景より暗い)を選ぶ。
26 """
27 alpha_a = a[:, :, 3].astype(np.int16)
28 alpha_b = b[:, :, 3].astype(np.int16)
29 luma_a = a[:, :, :3].astype(np.int16).sum(axis=2)
30 luma_b = b[:, :, :3].astype(np.int16).sum(axis=2)
31 return (alpha_a > alpha_b) | ((alpha_a == alpha_b) & (luma_a <= luma_b))
34def _pick(a, b):
35 keep = _winners(a, b)[:, :, None]
36 return np.where(keep, a, b)
39def halve_keeping_lines(rgba):
40 """RGBA 配列を縦横半分にする。2×2 のうち線らしい画素を残す。"""
41 height, width = rgba.shape[:2]
42 height -= height % 2
43 width -= width % 2
44 if height < 2 or width < 2: 44 ↛ 45line 44 didn't jump to line 45 because the condition on line 44 was never true
45 return rgba
46 block = rgba[:height, :width]
47 return _pick(
48 _pick(block[0::2, 0::2], block[0::2, 1::2]),
49 _pick(block[1::2, 0::2], block[1::2, 1::2]),
50 )
53def shrink_keeping_lines(image: QImage, width: int, height: int) -> QImage:
54 """``image`` を ``width`` × ``height`` へ、線を残したまま縮める。
56 半分ずつ縮めて目標の2倍以内まで寄せ、最後の端数だけニアレストで合わせる。
57 拡大や等倍は呼び出し側で扱う(ここでは縮小だけを受け持つ)。
58 """
59 width = max(1, int(width))
60 height = max(1, int(height))
61 if image.width() <= width and image.height() <= height:
62 return image
64 rgba = imaging.qimage_rgba_array(image)
65 while rgba.shape[0] >= height * 2 and rgba.shape[1] >= width * 2:
66 halved = halve_keeping_lines(rgba)
67 if halved.shape == rgba.shape: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true
68 break
69 rgba = halved
71 reduced = imaging.rgba_array_to_qimage(np.ascontiguousarray(rgba))
72 if (reduced.width(), reduced.height()) == (width, height): 72 ↛ 74line 72 didn't jump to line 74 because the condition on line 72 was always true
73 return reduced
74 from PySide6.QtCore import Qt
76 return reduced.scaled(
77 width,
78 height,
79 Qt.AspectRatioMode.IgnoreAspectRatio,
80 Qt.TransformationMode.FastTransformation,
81 )