Coverage for paintmaskanimator/auto_paint.py: 84%
1185 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"""色分けの線を手がかりに、見本のセルから同じレイヤーのほかのセルを自動で塗る。
3原画では、黒の主線と、原色の赤・緑・青の色トレス線(範囲線・斜線・影の境目など)
4が色分けの境界になる。黄・水色・赤紫(原色2つを重ねた色)の線も同じに扱う。
5そこで対象セルを、これらの線で区切られた「部分」に分け、
6部分ごとに、もう塗り終えた隣のセルの同じ場所の色を写す。
8* 見本から前後へ1セルずつ進み、塗り終えたセルを次のセルの手本にする。隣どうしは
9 動きが小さいので、遠い見本と直接比べるより取り違えが少ない。
10* 部分の場所は、部分のまわりの線画のずれから求める(:func:`fill_tracking.estimate_shifts`)。
11 ずらした先の手本の色が一色にそろっていなければ、塗らずに残す。
12* 色トレス線そのものも、手本で同じ場所が何色かを見て決める。手本でも同じ線の色なら
13 (影の境目の青など)そのまま残し、塗りの色になっていれば(頬の範囲線、斜線の
14 仕上げ色など)その色で塗る。
15* もう塗ってある部分を塗り替えるのは、手本の色がほぼ完全にそろっているときだけ。
16 手本と大きく違う色で塗ってあっても、形が合わなければ塗り替えず「塗り間違いかも」
17 として知らせる。
18* 色トレス線の小さな切れ目(数画素)は閉じたものとして部分を分ける。範囲線が
19 少し切れていても、頬と顔が1つの部分にならない。
20* 見本で斜線などが仕上げ色(頬の中の赤い斜線なら、頬より濃い色)になっていれば、
21 「どの色の線が、どの色の中にあると、何色になるか」を覚えて、見本から遠いセルでも
22 その色にする。
23* 一通り塗ったら、迷った所の少ないセルを見本に加えて、残りのセルを塗り直す。
24 手本が増えるので、向きの変わる所でも反対側から手がかりが得られる。
25* 細い部分は1〜2画素のずれで手本の線に重なりやすいので、色がそろわなければ
26 位置を少しだけ動かして試す。
27* 手本に無い形(開いた口、新しく見えてきたフリルなど)は、前のセルのその場所の
28 色で塗ると間違えるので、塗らずに「新しく出てきた部分」として知らせる。
29* 手がかりの無い色トレス線(見本に写っていない所の線)は残し、「決められなかった
30 線」として知らせる。
32画面も Qt も知らない純粋な処理で、書き込むのは呼び出し側(``main_window_auto_paint``)。
33"""
34from dataclasses import dataclass, field
35from functools import lru_cache
36from contextlib import ExitStack
37from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, cast, overload
39import numpy as np
41from . import bucket_fill, fill_tracking, line_flow, reference_check
43Rgb = Tuple[int, int, int]
44Bounds = Tuple[int, int, int, int]
46#: 色トレス線とみなす原色。強いチャンネルは 255 からこれだけ離れていてよい。
47PRIMARY_TOLERANCE = 40
48#: 弱いチャンネルは 0 からこれだけ離れていてよい(黄の線 ``#FFFF32`` など)。
49PRIMARY_LOW_TOLERANCE = 60
50#: これより小さい部分は手本と比べない(線のふちの数画素など)。透明のままの穴は、
51#: まわりが1色なら最後にその色で埋める(:func:`_tiny_holes`)。
52MIN_REGION_PIXELS = 3
53#: 塗り終えたあとに残った透明の穴は、この大きさ(画素)まで、まわりの色で埋める。
54TINY_HOLE_PIXELS = 4
55#: 穴を埋めるのは、となりの色がこの割合以上そろっているとき。
56TINY_HOLE_SHARE = 0.6
57#: これだけ小さい穴は、となりで一番多い色がどれだけ少なくても埋める。2つの塗りの
58#: 境目にできた1画素は、どちらの色にしても見た目は変わらないが、透けると必ず目立つ。
59TINY_HOLE_ANY_PIXELS = 2
60#: 原画のしるしの色(原色でない色トレス線)とみなす、1つの色の画素数の上限
61#: (キャンバス全体に対する割合)。
62MARK_MAX_SHARE = 0.02
63#: しるしの色は細い:これだけ収縮させて残る所があれば、塗り色とみなす。
64MARK_THICKNESS = 2
65#: まだ塗っていない部分は、手本の色がこの割合以上そろっていれば塗る。
66FILL_SHARE = 0.75
67#: もう塗ってある部分は、手本の色がこの割合以上そろっていれば塗り替える。
68REPAINT_SHARE = 0.8
69#: 色トレス線は、手本の色がこの割合以上そろっていれば1本まるごとその色にする。
70#: そろわなければ線のまま未決定として残す。
71LINE_SHARE = 0.75
72#: 色トレス線は、手本でもこの割合以上が線の色なら残す。
73KEEP_LINE_SHARE = 0.5
74#: 「手本でも線」とみなす距離。中割りの線は1〜2画素ずれる。
75KEEP_LINE_RADIUS = 2
76#: ずらした先のうち、手本で線でない画素がこの割合以上あること。
77MIN_SAMPLED_SHARE = 0.5
78#: 重ねた先の手本の部分と、大きさがこれ以上違えば塗らない。
79MAX_AREA_RATIO = 3.0
80#: 色トレス線の「となり」とみなす幅。
81LINE_NEIGHBOUR = 2
82#: ずれを探す範囲(隣のセルとのあいだの動きの最大)。
83SEARCH_MARGIN = 48
84#: ずれを求めるときに重ねる、部分のまわりの幅。
85CONTEXT = 24
86#: ずらして重ねた境界線が、この割合以上重ならなければ場所が分からないとみなす。
87MIN_LINE_OVERLAP = 0.4
88#: 色トレス線は細く取り違えが目立つので、重なりをもう少し求める。
89MIN_COLOR_LINE_OVERLAP = 0.5
90#: 人が塗った見本を直接の手本にするのは、見本からこのセル数以内だけ(遠いと
91#: 動きが大きく、似た線へずれて重なりやすい)。
92TEACHER_REACH = 2
93#: 1画素ずれて重なった線を、ぴったり重なった線のどれだけに数えるか。
94NEAR_LINE_WEIGHT = 0.8
95#: ずれ1画素あたりの割り引き(重なった割合から引く)。
96MOTION_PENALTY = 0.003
97#: 色トレス線の、これだけの幅までの切れ目を閉じたものとして部分を分ける。
98GAP_RADIUS = 2
99#: 切れ目を閉じた画素を表す値(:func:`fill_tracking._labels` の値には現れない)。
100GAP_VALUE = 1
101#: 塗り残しを一覧に載せる大きさ(これより小さい隙間は数えるだけ)。
102REPORT_MIN_PIXELS = 30
103#: 細い部分の色がそろわないとき、位置をこの画素数まで動かして試す。
104REFINE_RADIUS = 2
105#: 手本で重なった部分がこの倍より大きく、
106NEW_PART_RATIO = 6.0
107#: 部分を囲む線のうち手本にもある割合がこれ未満なら、手本に無い新しい形とみなす。
108NEW_PART_OUTLINE = 0.35
109#: 区画ごとの動きで決めるのは、動きの確かさ(重なった線の割合)がこれ以上のとき。
110FLOW_MIN_SCORE = 0.35
111#: 一番の色がこの割合以上で、二番の色のこの倍以上あれば、FILL_SHARE に届かなくても決める。
112FLOW_LEAD_SHARE = 0.6
113FLOW_LEAD = 2.5
114FLOW_AREA_CHECK = False
115#: 線の重なりがこれより弱いときは、透明な部分でも重ねた先と大きさを比べる。
116WEAK_OVERLAP = 0.5
117#: 手本で透明だった所のうち、この割合以上が「決められずに透明のまま」なら答えにしない。
118OPEN_SHARE = 0.5
119#: 元の線画から塗るとき、線の切れ目で2つ以上の部分(顔と髪など)がつながって1つに
120#: なっていることがある。動きで重ねた手本の色が、それぞれこの割合以上の色に分かれ、
121#: 合わせてほぼ全部(``CONFLICT_TOTAL``)なら、境界を創作せず色を決めきれない部分とする。
122CONFLICT_SHARE = 0.15
123#: 部分を囲む線が、動きで重ねた手本の線にこの割合も重ならなければ、動きで決めない。
124FLOW_OUTLINE = 0.45
125#: 囲む線を確かめるのは、この大きさ(画素)までの部分。大きな部分は囲む線が長く、
126#: 場所ごとに動きが違うので重なりにくい(髪の前を動く手のような部分は小さい)。
127FLOW_OUTLINE_MAX_PIXELS = 6000
128#: 部分だけをずらしたほうが、囲む線がこれだけ多く重なるなら、まわりと違う動きとみなす。
129FLOW_OUTLINE_GAIN = 0.2
130CONFLICT_TOTAL = 0.9
131#: 複数色の競合を判定する部分の大きさの下限(画素)。小さな部分は1色で決める。
132CONFLICT_MIN_PIXELS = 2000
133#: 形で探す範囲、似ているとみなす重なりの割合、ほかの色の候補との差、大きさの範囲(倍)。
134SHAPE_REACH = 64
135SHAPE_MATCH = 0.6
136SHAPE_MARGIN = 0.1
137SHAPE_SIZE_RATIO = 1.5
138#: 決められなかった部分を、今の絵(塗りかけ)の色に従わせるときの、一色にそろう割合。
139HINT_SHARE = 0.9
140#: 手で塗った部分を答えにするとき、囲む線のうち同じ色で塗りつぶされている割合の上限。
141#: これより多ければ、線を越えてはみ出した塗り(塗りかけの失敗)とみなして使わない。
142HAND_CROSSED = 0.25
143#: 手で塗ったセルを、何セル先までの手本にするか。
144HAND_REACH = 6
145#: 大きな部分は、この大きさの区画ごとにずれを求めて色を数える。
146TILE = 64
147#: 区画の中の部分の画素がこれより少なければ数えない。
148MIN_TILE_PIXELS = 200
149#: 決められなかった色トレス線を一覧に載せる大きさ。
150LINE_REPORT_MIN_PIXELS = 20
151#: 「塗り間違いかも」とするのは、手本の色がこの割合以上そろっているとき。
152SUSPECT_SHARE = 0.9
153#: これより小さな部分は「塗り間違いかも」にしない(髪のハイライトなどの小さな形は、
154#: セルごとに形や場所が変わる)。
155SUSPECT_MIN_PIXELS = 100
156#: 迷った部分がこの割合以下のセルは、次の回の見本に加える。
157CONFIDENT_UNSURE_SHARE = 0.1
158#: 見本を増やして塗り直す回数の上限(最初の1回を含む)。
159MAX_ROUNDS = 3
160#: 覚えた仕上げ色を使うのは、線のとなりがこの割合以上、まわりの色か仕上げ色のとき。
161FINISH_ENCLOSED = 0.9
162#: 仕上げ色を覚えるのに要る画素数。
163MIN_FINISH_PIXELS = 6
164_MAX_WINDOW = 384
167@dataclass
168class CellPaint:
169 """1セル分の塗り替え。``(色, 外接矩形, 矩形の中のマスク)`` の並び。"""
171 paints: List[Tuple[Rgb, Bounds, np.ndarray]] = field(default_factory=list)
172 #: 通常バケツで実行した位置と色。塗り範囲の再現・原因確認に使う。
173 clicks: List[Tuple[int, int, Rgb]] = field(default_factory=list)
174 #: 塗った部分の数、塗るか決められなかった部分の数。
175 filled: int = 0
176 unsure: int = 0
177 #: 塗り替えた色トレス線の画素数。
178 line_pixels: int = 0
179 #: 調べた部分の数(迷った割合を出すのに使う)。
180 checked: int = 0
181 #: 人が確かめたほうがよい所(塗り残し・塗り間違いかも・新しく出てきた部分・
182 #: 決められなかった線)。
183 notes: List[reference_check.Finding] = field(default_factory=list)
184 #: 決められずに残した色トレス線 ``(外接矩形, マスク)``。次のセルの手本になったとき、
185 #: 「手本でも線のまま」を「残すと決めた」と取り違えないために使う。
186 undecided: List[Tuple[Bounds, np.ndarray]] = field(default_factory=list)
187 #: 決められずに透明のまま残した部分 ``(外接矩形, マスク)``。次のセルの手本に
188 #: なったとき、「手本でも透明」を「塗らないと決めた」と取り違えないために使う。
189 #: 元の線画から塗ると、取り違えた透明が次々のセルへ伝わってしまう。
190 unpainted: List[Tuple[Bounds, np.ndarray]] = field(default_factory=list)
192 @property
193 def unsure_share(self) -> float:
194 return self.unsure / self.checked if self.checked else 0.0
197@dataclass
198class AutoPlan:
199 """自動塗りの計画。セルの番号は呼び出し側が渡した並びの位置。"""
201 cells: Dict[int, CellPaint] = field(default_factory=dict)
202 #: 見本に使ったセル(最初の見本と、途中で加えたセル)。
203 references: List[int] = field(default_factory=list)
204 #: 覚えた仕上げ色:``(線の色, まわりの色) -> 仕上げ色``。
205 finishes: Dict[Tuple[Rgb, Rgb], Rgb] = field(default_factory=dict)
206 #: 塗った回数(見本を増やして塗り直した回数 + 1)。
207 rounds: int = 0
208 #: 高速化の検証用。保存するプロジェクトの内容には含めない。
209 optimization_stats: dict = field(default_factory=dict)
210 #: 選択した見本の元TP色 -> 仕上げ色と画素数。自動結果からは追加しない。
211 trace_palette: dict = field(default_factory=dict)
212 trace_face_rules: set = field(default_factory=set)
214 @property
215 def painted_cells(self) -> int:
216 return sum(1 for cell in self.cells.values() if cell.paints)
219def primary_line_mask(pixels, tolerance=PRIMARY_TOLERANCE,
220 low_tolerance=PRIMARY_LOW_TOLERANCE) -> np.ndarray:
221 """色トレス線の画素:原色の赤・緑・青と、2つを重ねた黄・水色・赤紫。
223 どのチャンネルも「強い」か「弱い」のどちらかで、強いチャンネルが1つか2つの色。
224 """
225 # チャンネルごとに比べる(uint8 のまま。int16 に直すとキャンバス3枚ぶんの
226 # 複製と、飛び飛びの並びの畳み込みが要る)。
227 floor = np.uint8(255 - tolerance)
228 ceiling = np.uint8(low_tolerance)
229 red, green, blue = pixels[:, :, 0], pixels[:, :, 1], pixels[:, :, 2]
230 high_red, high_green, high_blue = red >= floor, green >= floor, blue >= floor
231 strong = high_red.astype(np.uint8)
232 strong += high_green
233 strong += high_blue
234 either = high_red | (red <= ceiling)
235 either &= high_green | (green <= ceiling)
236 either &= high_blue | (blue <= ceiling)
237 either &= pixels[:, :, 3] > 0
238 either &= strong >= 1
239 either &= strong <= 2
240 return either
243def _is_primary(rgb) -> bool:
244 pixel = np.array([[[*rgb, 255]]], dtype=np.uint8)
245 return bool(primary_line_mask(pixel)[0, 0])
248def line_only(pixels):
249 """黒の主線・色トレス線・しるしの色だけを残した絵(ほかは透明)。元の線画として保持する。"""
250 black = fill_tracking.line_art(pixels)
251 primary = primary_line_mask(pixels)
252 keep = black | primary | mark_mask(pixels, black, primary)
253 out = np.zeros_like(pixels)
254 out[keep] = pixels[keep]
255 return out
258def boundary_mask(pixels) -> np.ndarray:
259 """色分けの境界:黒の主線と、色トレス線。"""
260 return fill_tracking.line_art(pixels) | primary_line_mask(pixels)
263def mark_mask(pixels, black=None, primary=None, max_share=MARK_MAX_SHARE,
264 thickness=MARK_THICKNESS) -> np.ndarray:
265 """原画のしるしの色の画素:原色ではないのに、細い線としてだけ置かれている色。
267 頭のチャームのピンクのように、原色でない色で描き足されたしるしがある。
268 わざわざ違う色で描いてあるのは「まわりと色が違う」という指示なので、まわりの色で
269 塗りつぶしてはいけない。色トレス線と同じに扱い、手本で同じ場所が何色かを見て決める。
271 塗り色(髪や肌のような広い面)と見分けるため、その色がこのセルのどこにも太い塊
272 として現れず(``thickness`` 画素ぶん収縮させると消える)、面積も ``max_share``
273 までのときだけ、しるしとみなす。
274 """
275 black = fill_tracking.line_art(pixels) if black is None else black
276 primary = primary_line_mask(pixels) if primary is None else primary
277 candidate = (pixels[:, :, 3] > 0) & ~black & ~primary
278 out = np.zeros(candidate.shape, dtype=bool)
279 if not candidate.any():
280 return out
281 values = fill_tracking._labels(pixels)
282 limit = candidate.size * max_share
283 found, counts = np.unique(values[candidate], return_counts=True)
284 for value, count in zip(found.tolist(), counts.tolist()):
285 if count > limit:
286 continue # 広い面はしるしではない
287 same = candidate & (values == value)
288 bounds = fill_tracking.mask_bounds(same)
289 if bounds is None: 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true
290 continue
291 x0, y0, x1, y1 = bounds
292 if bucket_fill.erode(same[y0:y1, x0:x1], thickness).any():
293 continue # 太い所がある(塗り色)
294 out |= same
295 return out
298def gap_mask(boundary, primary, radius=GAP_RADIUS) -> np.ndarray:
299 """色トレス線の近くで、``radius`` の幅までの切れ目をふさぐ画素。"""
300 closed = bucket_fill.erode(bucket_fill.dilate(boundary, radius), radius)
301 return closed & ~boundary & bucket_fill.dilate(primary, radius + 1)
304class _TargetRegions(reference_check.CellRegions):
305 """対象の線だけで範囲を固定する。既存の塗り色は境界にしない。"""
307 def __init__(self, pixels, values, barriers, gaps):
308 topology = np.where(barriers, values, 0).astype(values.dtype)
309 topology[gaps] = GAP_VALUE
310 super().__init__(pixels, values=topology)
311 self.source_values = values
312 self._colors = {}
314 def value(self, index):
315 if index not in self._colors:
316 value = super().value(index)
317 if value == 0:
318 x0, y0, x1, y1 = self.bounds(index)
319 sample = self.source_values[y0:y1, x0:x1][self.local_mask(index)]
320 # 一色の既存塗りは塗り替え判定に使う。色むらは面を分割しない。
321 value = int(sample[0]) if np.all(sample == sample[0]) else 0
322 self._colors[index] = value
323 return self._colors[index]
326class _Cell:
327 """1セル分の、部分分けと境界。"""
329 def __init__(self, pixels, *, target=False):
330 self.pixels = pixels
331 self.values = fill_tracking._labels(pixels)
332 self.black = fill_tracking.line_art(pixels)
333 self.primary = primary_line_mask(pixels)
334 self._mark: Optional[np.ndarray] = None
335 self.boundary = self.black | self.primary
336 self.gaps = gap_mask(self.boundary, self.primary)
337 # 通常のバケツと同じく、画面端で途切れた線を仮想延長して区切る。
338 # 元の実線・TP線は動かさない。対象の仮想境界は塗り色に依存させない。
339 self.gaps |= bucket_fill.extend_line_ends(
340 self.boundary, bucket_fill.BucketOptions.extend_length,
341 ) & (~self.boundary if target else self.values == 0)
342 region_values = self.values
343 if self.gaps.any():
344 region_values = self.values.copy()
345 region_values[self.gaps] = GAP_VALUE
346 self.regions = (_TargetRegions(
347 pixels, self.values, self.boundary | self.mark, self.gaps,
348 ) if target else reference_check.CellRegions(pixels, values=region_values))
349 # 中割りの線は1画素ずれることが多いので、ずれを求めるときは1画素太らせた
350 # 線にも重なりを認める(ぴったり重なるよりは少なく数える)。
351 self.wide_boundary = bucket_fill.dilate(self.boundary, 1)
352 self.wide_primary = bucket_fill.dilate(self.primary, KEEP_LINE_RADIUS)
353 #: 決められずに残した色トレス線と、透明のまま残した部分(このセルが手本のとき)。
354 self.undecided = np.zeros(self.values.shape, dtype=bool)
355 self.open = np.zeros(self.values.shape, dtype=bool)
356 #: 元の線画から塗るセルか。塗りかけの色の手がかりが無いので、大きな部分の
357 #: 区画ごとの位置合わせや形での照合など、追加の手がかりを使う。
358 self.from_lines = False
359 #: 塗り進める並びでの位置(動きの推定を使い回すのに使う)。
360 self.index: Optional[int] = None
361 self.initial: Optional[_Cell] = None
362 #: 自動結果を部位の手本にするとき、今回色を決めた画素だけを記録する。
363 self.confirmed: Optional[np.ndarray] = None
364 # 計画作成中、全フラグの設定後にだけRustの圧縮見本を再利用する。
365 self._native_immutable = False
367 @property
368 def mark(self) -> np.ndarray:
369 """原画のしるしの色(:func:`mark_mask`)。塗るセルでだけ使うので、要るときに作る。"""
370 if self._mark is None:
371 self._mark = mark_mask(self.pixels, self.black, self.primary)
372 return self._mark
374 def mark_undecided(self, items, unpainted=()) -> "_Cell":
375 for (x0, y0, x1, y1), mask in items:
376 self.undecided[y0:y1, x0:x1] |= mask
377 for (x0, y0, x1, y1), mask in unpainted:
378 self.open[y0:y1, x0:x1] |= mask
379 return self
382def _majority(values) -> Tuple[Optional[int], float]:
383 if not values.size:
384 return None, 0.0
385 if values.size >= 128:
386 # 一色の面ではソートを省く。過半数があれば最多色は一意に決まる。
387 candidate = values.flat[0]
388 count = int(np.count_nonzero(values == candidate))
389 if count * 2 > values.size:
390 return int(candidate), float(count) / float(values.size)
391 found, counts = np.unique(values, return_counts=True)
392 best = int(np.argmax(counts))
393 return int(found[best]), float(counts[best]) / float(values.size)
396def _shifted(target: _Cell, local, bounds, shift):
397 """``local`` の画素を ``shift`` だけずらした座標。はみ出した画素は ``inside`` が偽。"""
398 height, width = target.values.shape
399 x0, y0 = bounds[0], bounds[1]
400 ys, xs = np.nonzero(local)
401 sy, sx = ys + y0 + shift[0], xs + x0 + shift[1]
402 inside = (sy >= 0) & (sy < height) & (sx >= 0) & (sx < width)
403 return np.clip(sy, 0, height - 1), np.clip(sx, 0, width - 1), inside
406def _sample(target: _Cell, guide: _Cell, local, bounds, shift):
407 """``local``(``bounds`` の中のマスク)をずらして手本へ重ね、一番多い色を返す。
409 手本の黒い線に重なった画素は数えない。
410 :returns: ``(色の値, そろっている割合)``。決められなければ ``(None, 0)``。
411 """
412 sy, sx, inside = _shifted(target, local, bounds, shift)
413 keep = inside & ~guide.black[sy, sx]
414 if keep.sum() < keep.size * MIN_SAMPLED_SHARE: 414 ↛ 415line 414 didn't jump to line 415 because the condition on line 414 was never true
415 return None, 0.0
416 return _majority(guide.values[sy[keep], sx[keep]])
419def _similar_size(target: _Cell, guide: _Cell, index, local, bounds, shift) -> bool:
420 """部分を手本へずらして重ねた先の部分が、大きさの近い部分か。
422 動きの大きいセルでは、小さな部分(頬など)がとなりの大きな部分(髪など)へ
423 ずれて重なることがある。大きさが ``MAX_AREA_RATIO`` 倍より違えば別の部分とみなす。
424 """
425 sy, sx, inside = _shifted(target, local, bounds, shift)
426 keep = inside & ~guide.black[sy, sx]
427 if not keep.any(): 427 ↛ 428line 427 didn't jump to line 428 because the condition on line 427 was never true
428 return False
429 labels = guide.regions.labels[sy[keep], sx[keep]]
430 found, counts = np.unique(labels, return_counts=True)
431 size = float(guide.regions.sizes[int(found[int(np.argmax(counts))])])
432 own = float(target.regions.sizes[index])
433 return own / MAX_AREA_RATIO <= size <= own * MAX_AREA_RATIO
436def _line_paints(target: _Cell, guide: _Cell, local, bounds, shift, painted, value,
437 finishes, learned):
438 """色トレス線1本を、手本で同じ場所の色にする塗り。手本でも線なら空。
440 線の形は対象から決める。手本の色がそろわなければ未決定として残し、
441 手本の境界で線を画素単位に切り分けない。
443 使う色は、このセルで線のとなりにある色(``painted`` は部分を塗ったあとの色の
444 値)か、手本で線の幅ほどの小さな部分の色(斜線の仕上げ色など)に限る。
445 ずれて重なった先の、となりに無い大きな部分の色(髪など)では塗らない。
447 :param learned: 仕上げ色を覚えるための集計。小さな部分の色に重なった画素数を足す。
448 :returns: 塗り(空なら線のまま残す)。この手本では決められなければ ``None``。
449 """
450 sy, sx, inside = _shifted(target, local, bounds, shift)
451 on_line = inside & guide.wide_primary[sy, sx]
452 if on_line.sum() >= inside.sum() * KEEP_LINE_SHARE:
453 if (on_line & guide.undecided[sy, sx]).sum() * 2 > on_line.sum(): 453 ↛ 454line 453 didn't jump to line 454 because the condition on line 453 was never true
454 return None # 手本でも決められずに残した線。手がかりにならない
455 return [] # 手本でも線のまま(影の境目など)
456 values = guide.values[sy, sx]
457 colored = inside & ~guide.black[sy, sx] & ~guide.primary[sy, sx] & (values != 0)
458 if colored.sum() < inside.sum() * MIN_SAMPLED_SHARE:
459 return None # この手本では決められない
460 majority, share = _majority(values[colored])
461 if majority is None: 461 ↛ 462line 461 didn't jump to line 462 because the condition on line 461 was never true
462 return None
463 neighbours, host, _around = _line_neighbours(target, local, bounds, painted)
464 small = _small_colors(guide, sy[colored], sx[colored], int(local.sum()))
465 small -= neighbours
466 if host is not None: 466 ↛ 473line 466 didn't jump to line 473 because the condition on line 466 was always true
467 sampled = values[colored]
468 for color in small:
469 count = int((sampled == color).sum())
470 if count: 470 ↛ 468line 470 didn't jump to line 468 because the condition on line 470 was always true
471 counts = learned.setdefault((value, host), {})
472 counts[color] = counts.get(color, 0) + count
473 allowed = neighbours | small
474 # 見本は色を決めるためだけに使い、見本の色境界を対象へ写さない。
475 if share < LINE_SHARE or majority not in allowed:
476 return None
477 return [(_opaque_rgb(majority), bounds, local.copy())]
480def _finish_paints(target: _Cell, local, bounds, painted, value, finishes):
481 """覚えた仕上げ色 ``(線の値, まわりの値) -> 色の値`` が当てはまれば、その塗り。
483 見本で頬の中の斜線などが仕上げ色になっていた組み合わせ。線のとなりが、まわりの
484 色か仕上げ色だけで囲まれていれば、手本に線が残っていても(手本の塗り残し)、
485 手本で場所が決められなくても仕上げ色にする。服と肌のあいだの線のように、
486 別の部分との境目にある線には使わない。
487 """
488 if not finishes:
489 return None
490 _neighbours, host, around = _line_neighbours(target, local, bounds, painted)
491 finish = finishes.get((value, host))
492 if finish is None or np.isin(around, np.array([host, finish])).mean() < FINISH_ENCLOSED:
493 return None
494 return _paints_by_value(local, bounds, np.full(int(local.sum()), finish))
497def _paints_by_value(local, bounds, chosen):
498 """線の画素ごとの色の値 ``chosen`` を、色ごとの塗りにまとめる。"""
499 ys, xs = np.nonzero(local)
500 paints = []
501 for value in np.unique(chosen).tolist():
502 rgb = reference_check._to_rgb(value)
503 if rgb is None or reference_check._is_line(rgb) or _is_primary(rgb): 503 ↛ 504line 503 didn't jump to line 504 because the condition on line 503 was never true
504 continue
505 mask = np.zeros(local.shape, dtype=bool)
506 pick = chosen == value
507 mask[ys[pick], xs[pick]] = True
508 paints.append((rgb, bounds, mask))
509 return paints
512def _line_neighbours(target: _Cell, local, bounds, painted):
513 """線のとなりにある色の値の集まり、一番多い色(無ければ ``None``)、
514 となりの画素の色の値(透明も含む)。"""
515 height, width = painted.shape
516 x0, y0, x1, y1 = bounds
517 ax0, ay0 = max(0, x0 - LINE_NEIGHBOUR), max(0, y0 - LINE_NEIGHBOUR)
518 ax1, ay1 = min(width, x1 + LINE_NEIGHBOUR), min(height, y1 + LINE_NEIGHBOUR)
519 around = np.zeros((ay1 - ay0, ax1 - ax0), dtype=bool)
520 around[y0 - ay0:y1 - ay0, x0 - ax0:x1 - ax0] = local
521 around = bucket_fill.dilate(around, LINE_NEIGHBOUR) & ~target.boundary[ay0:ay1, ax0:ax1]
522 around &= ~target.gaps[ay0:ay1, ax0:ax1]
523 every = painted[ay0:ay1, ax0:ax1][around]
524 found = every[every != 0]
525 host, _share = _majority(found)
526 return set(np.unique(found).tolist()), host, every
529def _small_colors(guide: _Cell, sy, sx, line_size):
530 """手本で、線の幅ほどの小さな部分の色(斜線の仕上げ色など)。"""
531 colors = set()
532 labels = guide.regions.labels[sy, sx]
533 for label in np.unique(labels).tolist():
534 if guide.regions.sizes[label] <= line_size * MAX_AREA_RATIO:
535 value = guide.regions.value(label)
536 if value not in (0, GAP_VALUE): 536 ↛ 533line 536 didn't jump to line 533 because the condition on line 536 was always true
537 colors.add(value)
538 return colors
541def _local_shift(target: _Cell, guide: _Cell, bounds, context=CONTEXT, margin=SEARCH_MARGIN):
542 """部分のまわり(``bounds`` を ``context`` だけ広げた矩形)の境界線が、
543 手本でどれだけずれた所にあるか。
545 小さな部分は自分の縁だけでは場所を決めにくいので、まわりの線もいっしょに
546 重ねる。線が一番多く重なるずれ ``(dy, dx)`` と、重なった割合を返す。
547 """
548 problem = _prepare_local_shift(target, guide, bounds, context, margin)
549 return ((0, 0), 0.0) if problem is None else _solve_local_shift(problem)
552class _ShiftProblem(dict):
553 """GPUで作る見本の窓は、CPUへ戻す必要が生じたときだけ構築する。"""
555 def __getitem__(self, key):
556 if key == 'after' and key not in self:
557 _target, guide, wx0, wy0, *_crop = dict.__getitem__(self, 'device_spec')
558 step = dict.__getitem__(self, 'step')
559 height, width = dict.__getitem__(self, 'before').shape
560 roi = np.s_[wy0:wy0+height*step:step, wx0:wx0+width*step:step]
561 exact = guide.boundary[roi].astype(np.float32)
562 near = guide.wide_boundary[roi].astype(np.float32)
563 self[key] = exact * (1.0 - NEAR_LINE_WEIGHT) + near * NEAR_LINE_WEIGHT
564 return super().__getitem__(key)
567def _prepare_local_shift(target, guide, bounds, context=CONTEXT, margin=SEARCH_MARGIN, *, lazy=False):
568 height, width = target.values.shape
569 x0, y0, x1, y1 = bounds
570 tx0, ty0 = max(0, x0 - context), max(0, y0 - context)
571 tx1, ty1 = min(width, x1 + context), min(height, y1 + context)
572 wx0, wy0 = max(0, tx0 - margin), max(0, ty0 - margin)
573 wx1, wy1 = min(width, tx1 + margin), min(height, ty1 + margin)
574 step = max(1, int(np.ceil(max(wx1 - wx0, wy1 - wy0) / _MAX_WINDOW)))
575 before = np.zeros((wy1 - wy0, wx1 - wx0), dtype=np.float32)
576 before[ty0 - wy0:ty1 - wy0, tx0 - wx0:tx1 - wx0] = target.boundary[ty0:ty1, tx0:tx1]
577 before = before[::step, ::step]
578 # ぴったり重なる線は1、1画素ずれて重なる線は ``NEAR_LINE_WEIGHT`` に数える。
579 total = float(before.sum())
580 if total == 0 or not guide.wide_boundary[wy0:wy1:step, wx0:wx1:step].any():
581 return None
582 limit = margin // step
583 size = _correlation_size(before, limit)
584 ys = np.r_[0:limit + 1, size[0] - limit:size[0]]
585 xs = np.r_[0:limit + 1, size[1] - limit:size[1]]
586 dys = np.where(ys >= size[0] // 2, ys - size[0], ys)
587 dxs = np.where(xs >= size[1] // 2, xs - size[1], xs)
588 problem = _ShiftProblem(before=before, size=size, total=total, step=step,
589 limit=limit, ys=ys, xs=xs, dys=dys, dxs=dxs,
590 device_spec=(target, guide, wx0, wy0, tx0, ty0, tx1, ty1))
591 if not lazy: 591 ↛ 593line 591 didn't jump to line 593 because the condition on line 591 was always true
592 problem['after']
593 return problem
596@lru_cache(maxsize=1024)
597def _fft_size(length):
598 """線形相関を保ったまま、小さな素因数だけのFFT寸法へ広げる。"""
599 candidate = length
600 while True:
601 remainder = candidate
602 for factor in (2, 3, 5):
603 while remainder % factor == 0:
604 remainder //= factor
605 if remainder == 1:
606 return candidate
607 candidate += 1
610def _local_shift_window(problem):
611 before, after = problem["before"], problem["after"]
612 fast_size = problem.get("size") or _correlation_size(before, problem["limit"])
613 correlation = np.fft.irfft2(
614 np.fft.rfft2(after, fast_size) * np.conj(np.fft.rfft2(before, fast_size)), fast_size
615 )
616 return correlation[np.ix_(problem["dys"] % fast_size[0], problem["dxs"] % fast_size[1])]
619def _correlation_size(before, limit):
620 """線のある範囲と探索幅から、相関の回り込みを防ぐ最小余白を求める。"""
621 result = []
622 for length, occupied in zip(before.shape, (before.any(axis=1), before.any(axis=0))):
623 positions = np.flatnonzero(occupied)
624 padding = limit
625 if positions.size and length > 2 * limit:
626 padding = max(0, limit - int(positions[0]), int(positions[-1]) + limit + 1 - length)
627 result.append(_fft_size(length + padding))
628 return tuple(result)
631def _finish_local_shift(problem, window):
632 total, step = problem["total"], problem["step"]
633 dys, dxs = problem["dys"], problem["dxs"]
634 near = window / total
635 # 隣のセルどうしの動きは小さいので、大きく動くずれほど割り引く。似た形の線が
636 # 離れた所にあっても(髪の筋、斜線など)、そちらへ飛びにくくなる。
637 near = near - MOTION_PENALTY * step * (np.abs(dys)[:, None] + np.abs(dxs)[None, :])
638 peak_y, peak_x = np.unravel_index(int(np.argmax(near)), near.shape)
639 share = float(window[peak_y, peak_x]) / total
640 return (int(dys[peak_y]) * step, int(dxs[peak_x]) * step), share
643def _solve_local_shift(problem):
644 window = _local_shift_window(problem)
645 shift, overlap = _finish_local_shift(problem, window)
646 if not problem.get('share_motion', False):
647 return shift, overlap
648 from .auto_motion import LocalShift
649 scores = window / problem['total'] - MOTION_PENALTY * problem['step'] * (
650 np.abs(problem['dys'])[:, None] + np.abs(problem['dxs'])[None, :])
651 best = np.partition(scores.ravel(), -2)[-2:] if scores.size > 1 else np.array([-np.inf, 0.0])
652 return LocalShift(*shift, best[-1] - best[-2]), overlap
655def _refine(target: _Cell, guide: _Cell, local, bounds, shift):
656 """``shift`` のまわり ``REFINE_RADIUS`` 画素で、色が一番そろうずれ。
658 :returns: ``(ずれ, 色の値, そろっている割合)``。
659 """
660 value, share = _sample(target, guide, local, bounds, shift)
661 best: Tuple[Tuple[int, int], Optional[int], float] = (shift, value, share)
662 for dy in range(-REFINE_RADIUS, REFINE_RADIUS + 1):
663 for dx in range(-REFINE_RADIUS, REFINE_RADIUS + 1):
664 if dy == 0 and dx == 0:
665 continue
666 moved = (shift[0] + dy, shift[1] + dx)
667 value, share = _sample(target, guide, local, bounds, moved)
668 if value is not None and share > best[2]:
669 best = (moved, value, share)
670 return best
673def _is_new_part(target: _Cell, guide: _Cell, index, local, bounds, shift) -> bool:
674 """手本に無い形か(開いた口、新しく見えてきたフリルなど)。
676 手本で重なった部分がずっと大きく(口なら顔全体の肌)、部分を囲む線も手本に
677 ほとんど無いとき。塗り残しの隙間なら、囲む線は手本にもある。
678 """
679 sy, sx, inside = _shifted(target, local, bounds, shift)
680 keep = inside & ~guide.black[sy, sx]
681 if not keep.any(): 681 ↛ 682line 681 didn't jump to line 682 because the condition on line 681 was never true
682 return False
683 labels = guide.regions.labels[sy[keep], sx[keep]]
684 found, counts = np.unique(labels, return_counts=True)
685 size = float(guide.regions.sizes[int(found[int(np.argmax(counts))])])
686 if size <= float(target.regions.sizes[index]) * NEW_PART_RATIO:
687 return False
688 height, width = target.values.shape
689 x0, y0, x1, y1 = bounds
690 ax0, ay0 = max(0, x0 - 2), max(0, y0 - 2)
691 ax1, ay1 = min(width, x1 + 2), min(height, y1 + 2)
692 area = np.zeros((ay1 - ay0, ax1 - ax0), dtype=bool)
693 area[y0 - ay0:y1 - ay0, x0 - ax0:x1 - ax0] = local
694 ring = bucket_fill.dilate(area, 2) & ~area & target.boundary[ay0:ay1, ax0:ax1]
695 ys, xs = np.nonzero(ring)
696 if not ys.size: 696 ↛ 697line 696 didn't jump to line 697 because the condition on line 696 was never true
697 return False
698 ry = np.clip(ys + ay0 + shift[0], 0, height - 1)
699 rx = np.clip(xs + ax0 + shift[1], 0, width - 1)
700 return float(guide.wide_boundary[ry, rx].mean()) < NEW_PART_OUTLINE
703def _open_share(target: _Cell, guide: _Cell, local, bounds, shift) -> float:
704 """重ねた先のうち、手本で決められずに透明のまま残した画素の割合。"""
705 sy, sx, inside = _shifted(target, local, bounds, shift)
706 if not inside.any():
707 return 0.0
708 return float(guide.open[sy[inside], sx[inside]].mean())
711def _hint_color(hint, local, bounds) -> Optional[Rgb]:
712 """今の絵(塗りかけ)で、部分が一色に塗ってあればその色。"""
713 x0, y0, x1, y1 = bounds
714 values = fill_tracking._labels(hint[y0:y1, x0:x1])[local]
715 value, share = _majority(values)
716 if value is None or share < HINT_SHARE: 716 ↛ 717line 716 didn't jump to line 717 because the condition on line 716 was never true
717 return None
718 rgb = reference_check._to_rgb(value)
719 if rgb is None or reference_check._is_line(rgb) or _is_primary(rgb):
720 return None
721 return rgb
724def _hand_color(target: _Cell, hint, local, bounds) -> Optional[Rgb]:
725 """元の線画の部分を、今の絵で人が一色に塗ってあれば、その色(確かな答え)。
727 囲む線まで同じ色で塗りつぶしてあるのは、線を越えたはみ出しなので使わない。
728 """
729 rgb = _hint_color(hint, local, bounds)
730 if rgb is None:
731 return None
732 ry, rx = _outline_pixels(target, local, bounds)
733 if ry.size: 733 ↛ 738line 733 didn't jump to line 738 because the condition on line 733 was always true
734 ring = hint[ry, rx]
735 crossed = (ring[:, 3] > 0) & (ring[:, :3] == np.array(rgb)).all(axis=1)
736 if float(crossed.mean()) > HAND_CROSSED: 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true
737 return None
738 return rgb
741def _hand_paint(pixels, hint, *, target=None) -> CellPaint:
742 """元の線画 ``pixels`` のうち、今の絵 ``hint`` で人が塗ってある部分。
744 確かでない透明な部分は ``unpainted`` に入れる(手本にしたとき、決めていない所)。
745 """
746 target = _Cell(pixels) if target is None else target
747 regions = target.regions
748 result = CellPaint()
749 for index in np.nonzero(regions.sizes >= MIN_REGION_PIXELS)[0].tolist():
750 value = regions.value(index)
751 if value == GAP_VALUE or reference_check._to_rgb(value) is not None:
752 continue
753 if index in regions.on_edge:
754 continue
755 bounds = regions.bounds(index)
756 local = regions.local_mask(index)
757 rgb = _hand_color(target, hint, local, bounds)
758 if rgb is None:
759 result.unpainted.append((bounds, local))
760 continue
761 paint_bounds, mask = _region_mask(target, local, bounds, value)
762 result.paints.append((rgb, paint_bounds, mask))
763 result.filled += 1
764 return result
767def _shape_match(target: _Cell, guide: _Cell, index, local, bounds) -> Optional[Rgb]:
768 """手本の近くで、大きさと形がよく似た塗ってある部分の色。無ければ None。
770 大きく動いたり回ったりした部分(手など)は、線の重なりでは位置が決まらない。
771 どの手本でも決められなかった透明な部分にだけ、最後の手がかりとして使う。
772 """
773 own = int(target.regions.sizes[index])
774 regions = guide.regions
775 height, width = guide.values.shape
776 x0, y0, x1, y1 = bounds
777 wx0, wy0 = max(0, x0 - SHAPE_REACH), max(0, y0 - SHAPE_REACH)
778 wx1, wy1 = min(width, x1 + SHAPE_REACH), min(height, y1 + SHAPE_REACH)
779 cy, cx = np.nonzero(local)
780 centre = (cy.mean() + y0, cx.mean() + x0)
781 scores: Dict[Rgb, float] = {}
782 for other in np.unique(regions.labels[wy0:wy1, wx0:wx1]).tolist():
783 size = int(regions.sizes[other])
784 if not own / SHAPE_SIZE_RATIO <= size <= own * SHAPE_SIZE_RATIO:
785 continue
786 rgb = reference_check._to_rgb(regions.value(other))
787 if rgb is None or reference_check._is_line(rgb) or _is_primary(rgb):
788 continue
789 gx0, gy0, gx1, gy1 = regions.bounds(other)
790 mask = regions.local_mask(other)
791 gy, gx = np.nonzero(mask)
792 base_y = int(round(gy.mean() + gy0 - centre[0]))
793 base_x = int(round(gx.mean() + gx0 - centre[1]))
794 best = 0.0
795 for oy in range(base_y - 2, base_y + 3):
796 for ox in range(base_x - 2, base_x + 3):
797 # 両方の形を同じ座標へ置いて、重なりの割合(共通 / 合わせた範囲)を出す。
798 ay0, ax0 = min(y0, gy0 - oy), min(x0, gx0 - ox)
799 ay1, ax1 = max(y1, gy1 - oy), max(x1, gx1 - ox)
800 a = np.zeros((ay1 - ay0, ax1 - ax0), dtype=bool)
801 b = np.zeros_like(a)
802 a[y0 - ay0:y1 - ay0, x0 - ax0:x1 - ax0] = local
803 b[gy0 - oy - ay0:gy1 - oy - ay0, gx0 - ox - ax0:gx1 - ox - ax0] = mask
804 best = max(best, float((a & b).sum()) / float((a | b).sum()))
805 scores[rgb] = max(scores.get(rgb, 0.0), best)
806 ranked = sorted(scores.items(), key=lambda item: -item[1])
807 if not ranked or ranked[0][1] < SHAPE_MATCH:
808 return None
809 if len(ranked) > 1 and ranked[1][1] > ranked[0][1] - SHAPE_MARGIN:
810 return None # 違う色の部分にも同じくらい似ている
811 return ranked[0][0]
814def _tiled_outcome(target: _Cell, guide: _Cell, local, bounds, margin):
815 """大きな部分を ``TILE`` 画素の区画に分け、区画ごとにずれを求めて色を数える。
817 元の線画だけから塗ると、髪の片側のような大きな部分が一つになる。場所によって
818 動きが違うので、部分全体で一つのずれでは手本と重ならない。透明な部分にだけ使う。
819 """
820 x0, y0, x1, y1 = bounds
821 votes: Dict[int, int] = {}
822 open_votes = 0
823 for ty in range(y0, y1, TILE):
824 for tx in range(x0, x1, TILE):
825 sub = local[ty - y0:ty - y0 + TILE, tx - x0:tx - x0 + TILE]
826 if int(sub.sum()) < MIN_TILE_PIXELS:
827 continue
828 tile = (tx, ty, tx + sub.shape[1], ty + sub.shape[0])
829 shift, overlap = _local_shift(target, guide, tile, margin=margin)
830 if overlap < MIN_LINE_OVERLAP:
831 continue
832 sy, sx, inside = _shifted(target, sub, tile, shift)
833 keep = inside & ~guide.black[sy, sx]
834 found, counts = np.unique(guide.values[sy[keep], sx[keep]], return_counts=True)
835 for value, count in zip(found.tolist(), counts.tolist()):
836 votes[value] = votes.get(value, 0) + count
837 open_votes += int(guide.open[sy[keep], sx[keep]].sum())
838 total = sum(votes.values())
839 if total < int(local.sum()) * MIN_SAMPLED_SHARE:
840 return "unsure", None
841 best = max(votes, key=lambda value: votes[value])
842 if votes[best] < total * FILL_SHARE:
843 return "unsure", None
844 new_rgb = reference_check._to_rgb(best)
845 if new_rgb is None and open_votes >= votes[best] * OPEN_SHARE:
846 return "unsure", None # 手本でも決められずに透明のままだった
847 if new_rgb is None or reference_check._is_line(new_rgb) or _is_primary(new_rgb):
848 return "keep", None
849 return "paint", new_rgb
852def _flow_votes(target: _Cell, guide: _Cell, flow: line_flow.Flow, local, bounds):
853 """透明な部分1つを、区画ごとの動き ``flow`` で手本へ重ねたときの色の割合。
855 手本で決められずに透明のまま残した所(``guide.open``)と線は数えない。
856 :returns: ``(色の値ごとの割合, 重み, 重ねた先の座標)``。動きが確かでなければ None。
857 重なった先の多くが手本でも決められなかった所なら ``"open"``(この手本では
858 決めない。まわりの色で塗ると、前のセルの迷いが広がるため)。
859 """
860 x0, y0 = bounds[0], bounds[1]
861 ys, xs = np.nonzero(local)
862 ys = ys + y0
863 xs = xs + x0
864 score = float(flow.score_at(ys, xs).mean())
865 if score < FLOW_MIN_SCORE: 865 ↛ 866line 865 didn't jump to line 866 because the condition on line 865 was never true
866 return None
867 dy, dx = flow.at(ys, xs)
868 height, width = target.values.shape
869 gy = np.round(ys + dy).astype(np.int64)
870 gx = np.round(xs + dx).astype(np.int64)
871 inside = (gy >= 0) & (gy < height) & (gx >= 0) & (gx < width)
872 gy, gx = gy[inside], gx[inside]
873 inner = ~guide.boundary[gy, gx]
874 # 手本で絵の外(キャンバスの端につながる透明)に当たる所も、決められなかった所と
875 # 同じに扱う。線の切れ目から絵の外とつながって塗られなかっただけのことがある。
876 outside = (guide.values[gy, gx] == 0) & np.isin(guide.regions.labels[gy, gx],
877 list(guide.regions.on_edge))
878 keep = inner & ~guide.open[gy, gx] & ~outside
879 if not keep.any() or (inner & ~keep).sum() >= inner.sum() * OPEN_SHARE:
880 return "open"
881 gy, gx = gy[keep], gx[keep]
882 found, counts = np.unique(guide.values[gy, gx], return_counts=True)
883 shares = {int(value): float(count) / float(keep.sum())
884 for value, count in zip(found.tolist(), counts.tolist())}
885 return shares, score * float(keep.sum()) / float(ys.size), (gy, gx)
888def _flow_decision(target: _Cell, votes, index, local, bounds, near=True):
889 """手本ごとの色の割合 ``votes`` ``[(手本, 動き, 割合, 重み, 座標)]`` を重みで足して決める。
891 :returns: ``_region_outcome`` と同じ ``(種類, 色)``。
892 """
893 total: Dict[int, float] = {}
894 weight = 0.0
895 for _guide, _flow, shares, vote_weight, _where in votes:
896 weight += vote_weight
897 for value, share in shares.items():
898 total[value] = total.get(value, 0.0) + share * vote_weight
899 ranked = sorted(total.items(), key=lambda item: -item[1])
900 sampled = ranked[0][0]
901 share = ranked[0][1] / weight
902 second = ranked[1][1] / weight if len(ranked) > 1 else 0.0
903 if near and share < FILL_SHARE and int(target.regions.sizes[index]) >= CONFLICT_MIN_PIXELS:
904 colours = [value for value, amount in ranked if amount >= weight * CONFLICT_SHARE]
905 if (len(colours) >= 2 and sum(total[value] for value in colours) >= weight * CONFLICT_TOTAL 905 ↛ 910line 905 didn't jump to line 910 because the condition on line 905 was always true
906 and all(_paintable(value) for value in colours)):
907 return "ambiguous", colours
908 # 大きな部分は、端の方が動きの違うとなりの部分へずれて重なりやすい。一番の色が
909 # 二番よりずっと多ければ決めてよい。
910 if share < FILL_SHARE and not ( 910 ↛ 913line 910 didn't jump to line 913 because the condition on line 910 was never true
911 near and share >= FLOW_LEAD_SHARE and share >= second * FLOW_LEAD
912 ):
913 return "unsure", None
914 guide, flow, _shares, _weight, (gy, gx) = max(
915 votes, key=lambda vote: vote[2].get(sampled, 0.0) * vote[3]
916 )
917 outline = _flow_outline(target, guide, flow, local, bounds)
918 new_rgb = reference_check._to_rgb(sampled)
919 if new_rgb is None or reference_check._is_line(new_rgb) or _is_primary(new_rgb): 919 ↛ 922line 919 didn't jump to line 922 because the condition on line 919 was never true
920 # 透明のままにするのも、囲む線が手本と重なるときだけ(髪が背景へずれて重なる
921 # ことがある)。
922 return ("keep", None) if outline >= FLOW_OUTLINE else ("unsure", None)
923 # 手本で重なった部分がずっと大きく、囲む線も手本に無ければ、新しく出てきた部分。
924 labels = guide.regions.labels[gy, gx]
925 found, counts = np.unique(labels, return_counts=True)
926 size = float(guide.regions.sizes[int(found[int(np.argmax(counts))])])
927 if size > float(target.regions.sizes[index]) * NEW_PART_RATIO and outline < NEW_PART_OUTLINE: 927 ↛ 928line 927 didn't jump to line 928 because the condition on line 927 was never true
928 return "new", None
929 if (outline < FLOW_OUTLINE and int(target.regions.sizes[index]) <= FLOW_OUTLINE_MAX_PIXELS 929 ↛ 934line 929 didn't jump to line 934 because the condition on line 929 was never true
930 and _own_outline(target, guide, local, bounds) >= outline + FLOW_OUTLINE_GAIN):
931 # 囲む線が手本の線に重ならず、部分だけをずらすとよく重なる。手や顔が髪の前を
932 # 動くときのように、この部分だけまわりと違う動きをしている。部分ごとのずれで
933 # 決め直す。
934 return "unsure", None
935 return "paint", new_rgb
938def _outline_pixels(target: _Cell, local, bounds):
939 """部分を囲む線の画素の座標 ``(ys, xs)``。"""
940 height, width = target.values.shape
941 x0, y0, x1, y1 = bounds
942 ax0, ay0 = max(0, x0 - 2), max(0, y0 - 2)
943 ax1, ay1 = min(width, x1 + 2), min(height, y1 + 2)
944 area = np.zeros((ay1 - ay0, ax1 - ax0), dtype=bool)
945 area[y0 - ay0:y1 - ay0, x0 - ax0:x1 - ax0] = local
946 ring = bucket_fill.dilate(area, 2) & ~area & target.boundary[ay0:ay1, ax0:ax1]
947 ry, rx = np.nonzero(ring)
948 return ry + ay0, rx + ax0
951def _flow_outline(target: _Cell, guide: _Cell, flow: line_flow.Flow, local, bounds) -> float:
952 """部分を囲む線を、動き ``flow`` で手本へ重ねたとき、手本の線に重なる割合。"""
953 height, width = target.values.shape
954 ry, rx = _outline_pixels(target, local, bounds)
955 if not ry.size: 955 ↛ 956line 955 didn't jump to line 956 because the condition on line 955 was never true
956 return 1.0
957 rdy, rdx = flow.at(ry, rx)
958 my = np.clip(np.round(ry + rdy).astype(np.int64), 0, height - 1)
959 mx = np.clip(np.round(rx + rdx).astype(np.int64), 0, width - 1)
960 return float(guide.wide_boundary[my, mx].mean())
963def _own_outline(target: _Cell, guide: _Cell, local, bounds) -> float:
964 """部分を囲む線だけを、一番よく重なるようにずらしたとき、手本の線に重なる割合。"""
965 height, width = target.values.shape
966 ry, rx = _outline_pixels(target, local, bounds)
967 if not ry.size:
968 return 0.0
970 def overlap(dy, dx):
971 my = np.clip(ry + dy, 0, height - 1)
972 mx = np.clip(rx + dx, 0, width - 1)
973 return float(guide.wide_boundary[my, mx].mean())
975 reach = line_flow.REACH // 2
976 step = 2
977 best, best_dy, best_dx = 0.0, 0, 0
978 for dy in range(-reach, reach + 1, step):
979 for dx in range(-reach, reach + 1, step):
980 found = overlap(dy, dx)
981 if found > best:
982 best, best_dy, best_dx = found, dy, dx
983 for dy in (-1, 0, 1):
984 for dx in (-1, 0, 1):
985 best = max(best, overlap(best_dy + dy, best_dx + dx))
986 return best
989def _flow_outside(target: _Cell, guide: _Cell, flow: line_flow.Flow, local, bounds):
990 """画面端に接する、対象セルの線で区切った領域を一色で塗る。
992 見本は色の投票にだけ使う。見本のシルエットでマスクを切り抜いたり、
993 細い所を収縮で削ったりしない。背景の大部分が透明なら塗らない。
994 """
995 x0, y0, _x1, _y1 = bounds
996 ys, xs = np.nonzero(local)
997 dy, dx = flow.at(ys + y0, xs + x0)
998 height, width = guide.values.shape
999 gy = np.round(ys + y0 + dy).astype(np.int64)
1000 gx = np.round(xs + x0 + dx).astype(np.int64)
1001 inside = (gy >= 0) & (gy < height) & (gx >= 0) & (gx < width)
1002 gy, gx = np.clip(gy, 0, height - 1), np.clip(gx, 0, width - 1)
1003 usable = inside & ~guide.boundary[gy, gx] & ~guide.open[gy, gx]
1004 if usable.sum() < local.sum() * MIN_SAMPLED_SHARE: 1004 ↛ 1005line 1004 didn't jump to line 1005 because the condition on line 1004 was never true
1005 return []
1006 value, share = _majority(guide.values[gy[usable], gx[usable]])
1007 if value is None or not _paintable(value) or share < FILL_SHARE:
1008 return []
1009 if _flow_outline(target, guide, flow, local, bounds) < FLOW_OUTLINE: 1009 ↛ 1010line 1009 didn't jump to line 1010 because the condition on line 1009 was never true
1010 return []
1011 paint_bounds, mask = _region_mask(target, local, bounds, 0)
1012 return [(_opaque_rgb(value), paint_bounds, mask)]
1015def _paintable(value) -> bool:
1016 rgb = reference_check._to_rgb(value)
1017 return rgb is not None and not reference_check._is_line(rgb) and not _is_primary(rgb)
1021def _region_outcome(target: _Cell, guide: _Cell, index, value, rgb, local, bounds, margin):
1022 """部分1つを ``guide`` で決める。``(種類, 色)``。
1024 種類は ``"paint"``(その色で塗る)、``"keep"``(今のままでよい)、
1025 ``"suspect"``(違う色で塗ってあるが、形が合わず塗り替えない)、
1026 ``"new"``(手本に無い形なので塗らない)、``"unsure"``。
1027 """
1028 shift, overlap = _local_shift(target, guide, bounds, margin=margin)
1029 large = target.from_lines and max(bounds[2] - bounds[0], bounds[3] - bounds[1]) > TILE * 3 // 2
1030 if overlap < MIN_LINE_OVERLAP: 1030 ↛ 1031line 1030 didn't jump to line 1031 because the condition on line 1030 was never true
1031 if rgb is None and large:
1032 return _tiled_outcome(target, guide, local, bounds, margin)
1033 return "unsure", None
1034 sampled, share = _sample(target, guide, local, bounds, shift)
1035 if rgb is None and (sampled is None or share < FILL_SHARE):
1036 if large: 1036 ↛ 1037line 1036 didn't jump to line 1037 because the condition on line 1036 was never true
1037 return _tiled_outcome(target, guide, local, bounds, margin)
1038 # 細い部分は1〜2画素のずれで手本の線に重なりやすい。少し動かして試す。
1039 shift, sampled, share = _refine(target, guide, local, bounds, shift)
1040 if sampled is None: 1040 ↛ 1041line 1040 didn't jump to line 1041 because the condition on line 1040 was never true
1041 return "unsure", None
1042 if sampled == value:
1043 if (rgb is None and target.from_lines 1043 ↛ 1045line 1043 didn't jump to line 1045 because the condition on line 1043 was never true
1044 and _open_share(target, guide, local, bounds, shift) >= OPEN_SHARE):
1045 return "unsure", None # 手本でも決められずに透明のままだった
1046 return "keep", None
1047 new_rgb = reference_check._to_rgb(sampled)
1048 if rgb is not None and not _similar_size(target, guide, index, local, bounds, shift):
1049 # もう塗ってある部分が、大きさの違う部分へずれて重なった。塗り残しの
1050 # 透明な所は、手本ではまわりとつながって大きな部分になっているので比べない。
1051 if new_rgb is not None and share >= SUSPECT_SHARE: 1051 ↛ 1053line 1051 didn't jump to line 1053 because the condition on line 1051 was always true
1052 return "suspect", new_rgb
1053 return "unsure", None
1054 if share < (FILL_SHARE if rgb is None else REPAINT_SHARE):
1055 return "unsure", None
1056 if (rgb is None and target.from_lines and new_rgb is not None and overlap < WEAK_OVERLAP 1056 ↛ 1059line 1056 didn't jump to line 1059 because the condition on line 1056 was never true
1057 and not _similar_size(target, guide, index, local, bounds, shift)):
1058 # 線の重なりが弱いうえ、重ねた先がずっと大きな部分(顔が髪へずれたなど)。
1059 return "unsure", None
1060 if new_rgb is None: 1060 ↛ 1061line 1060 didn't jump to line 1061 because the condition on line 1060 was never true
1061 return "keep", None # 手本では透明(塗らない所)。塗ってある色も消さない。
1062 if reference_check._is_line(new_rgb) or _is_primary(new_rgb): 1062 ↛ 1063line 1062 didn't jump to line 1063 because the condition on line 1062 was never true
1063 return "keep", None # 線の色では塗らない
1064 if rgb is None and _is_new_part(target, guide, index, local, bounds, shift):
1065 return "new", None
1066 return "paint", new_rgb
1069def _region_mask(target: _Cell, local, bounds, value):
1070 """塗る範囲。部分分けのために閉じた切れ目のうち、同じ色でとなり合う画素も含める。"""
1071 height, width = target.values.shape
1072 x0, y0, x1, y1 = bounds
1073 gx0, gy0 = max(0, x0 - GAP_RADIUS), max(0, y0 - GAP_RADIUS)
1074 gx1, gy1 = min(width, x1 + GAP_RADIUS), min(height, y1 + GAP_RADIUS)
1075 mask = np.zeros((gy1 - gy0, gx1 - gx0), dtype=bool)
1076 mask[y0 - gy0:y1 - gy0, x0 - gx0:x1 - gx0] = local
1077 gaps = target.gaps[gy0:gy1, gx0:gx1] & (target.values[gy0:gy1, gx0:gx1] == value)
1078 if gaps.any():
1079 mask |= bucket_fill.dilate(mask, GAP_RADIUS) & gaps
1080 return (gx0, gy0, gx1, gy1), mask
1083def _neighbour_color(target: _Cell, painted, local, window):
1084 """穴に接している色の値のうち一番多いもの、その割合、それが線の色か。
1086 接している画素だけを見る。少し離れた窓で多数決を取ると、2つの塗りの境目に
1087 できた穴では、関わりのない色まで数に入ってしまう。接している所がぜんぶ線なら
1088 線の色を返す(主線の中にできた1画素の穴は、背景が透けるより線の色が正しい)。
1089 """
1090 wy0, wx0, wy1, wx1 = window
1091 values = painted[wy0:wy1, wx0:wx1]
1092 ring = bucket_fill.dilate(local, 1) & ~local
1093 every = values[ring & ~target.boundary[wy0:wy1, wx0:wx1]]
1094 every = every[every != 0]
1095 if every.size:
1096 host, share = _majority(every)
1097 return host, share, False
1098 every = values[ring]
1099 host, share = _majority(every[every != 0])
1100 return host, share, True
1103def _tiny_holes(target: _Cell, painted):
1104 """塗り終えたあとに残った透明の画素を、となりの色で埋める塗り。
1106 線のきわや頬の中には、数画素だけ透明の穴が残る。部分として扱うには小さすぎて
1107 手本と比べられなかった画素や、部分分けのために閉じた切れ目の画素などで、
1108 仕上げでは背景が透けて見えてしまう。埋めるのは次の2つ。
1110 * ``TINY_HOLE_PIXELS`` までの小さな穴。``TINY_HOLE_ANY_PIXELS`` 以下なら、
1111 となりで一番多い色がどれだけ少なくても埋める(1画素の点は、どちらの塗りの
1112 色にしても見た目は変わらないが、透けると必ず目立つ)。
1113 * 部分分けのために閉じた色トレス線の切れ目(``_Cell.gaps``)。こちらで足した
1114 画素で、絵の一部ではないので、大きさによらず埋める。
1116 大きな塗り残しは埋めず、これまでどおり「確かめたい所」で知らせる。
1118 :param painted: ほかの部分と色トレス線を塗ったあとの、画素ごとの色の値。
1119 """
1120 height, width = painted.shape
1121 holes = (painted == 0) & ~target.boundary & ~target.mark
1122 if not holes.any(): 1122 ↛ 1123line 1122 didn't jump to line 1123 because the condition on line 1122 was never true
1123 return []
1124 labels, _count = reference_check.label_regions(holes.astype(np.uint8))
1125 ys, xs = np.nonzero(holes)
1126 ids = labels[ys, xs]
1127 order = np.argsort(ids, kind="stable")
1128 ys, xs, ids = ys[order], xs[order], ids[order]
1129 _found, starts, sizes = np.unique(ids, return_index=True, return_counts=True)
1130 paints = []
1131 for start, size in zip(starts.tolist(), sizes.tolist()):
1132 hy, hx = ys[start:start + size], xs[start:start + size]
1133 # 切れ目をふさいだ画素でできた穴は、大きさによらず埋める。
1134 gap = bool(target.gaps[hy, hx].mean() >= 0.5)
1135 if size > TINY_HOLE_PIXELS and not gap:
1136 continue # 大きな塗り残しは、塗らずに知らせてある
1137 y0, y1 = int(hy.min()), int(hy.max()) + 1
1138 x0, x1 = int(hx.min()), int(hx.max()) + 1
1139 wy0, wx0 = max(0, y0 - 1), max(0, x0 - 1)
1140 wy1, wx1 = min(height, y1 + 1), min(width, x1 + 1)
1141 local = np.zeros((wy1 - wy0, wx1 - wx0), dtype=bool)
1142 local[hy - wy0, hx - wx0] = True
1143 host, share, on_line = _neighbour_color(
1144 target, painted, local, (wy0, wx0, wy1, wx1),
1145 )
1146 if host is None: 1146 ↛ 1147line 1146 didn't jump to line 1147 because the condition on line 1146 was never true
1147 continue
1148 if share < TINY_HOLE_SHARE and size > TINY_HOLE_ANY_PIXELS and not gap: 1148 ↛ 1149line 1148 didn't jump to line 1149 because the condition on line 1148 was never true
1149 continue
1150 rgb = reference_check._to_rgb(host)
1151 if rgb is None or _is_primary(rgb): 1151 ↛ 1152line 1151 didn't jump to line 1152 because the condition on line 1151 was never true
1152 continue
1153 if reference_check._is_line(rgb) and not on_line: 1153 ↛ 1154line 1153 didn't jump to line 1154 because the condition on line 1153 was never true
1154 continue # 線の色で塗るのは、線の中にできた穴だけ
1155 mask = np.zeros((y1 - y0, x1 - x0), dtype=bool)
1156 mask[hy - y0, hx - x0] = True
1157 paints.append((rgb, (x0, y0, x1, y1), mask))
1158 return paints
1161def _trace_first(target, guides, result, margin):
1162 """見本の初期線の置換色を調べ、同色になった隣接領域へ含み塗りする。"""
1163 decided = set()
1164 regions = target.regions
1165 for index in np.nonzero(regions.sizes >= MIN_REGION_PIXELS)[0].tolist():
1166 bounds = regions.bounds(index)
1167 x0, y0, x1, y1 = bounds
1168 local = regions.local_mask(index)
1169 if not target.primary[y0:y1, x0:x1][local].any():
1170 continue
1171 answers = []
1172 for guide in guides:
1173 initial = guide.initial
1174 if initial is None:
1175 continue
1176 shift, overlap = _local_shift(target, initial, bounds, margin=margin)
1177 if overlap < MIN_COLOR_LINE_OVERLAP: 1177 ↛ 1178line 1177 didn't jump to line 1178 because the condition on line 1177 was never true
1178 continue
1179 sy, sx, inside = _shifted(target, local, bounds, shift)
1180 matched = inside & (initial.values[sy, sx] == regions.value(index))
1181 if matched.sum() < local.sum() * LINE_SHARE: 1181 ↛ 1182line 1181 didn't jump to line 1182 because the condition on line 1181 was never true
1182 continue
1183 color, share = _majority(guide.values[sy[matched], sx[matched]])
1184 if color is None or share < HINT_SHARE or not _paintable(color):
1185 continue
1186 # この線の周囲だけを調べる。線ごとにキャンバス全体を走査しない。
1187 height, width = regions.labels.shape
1188 ax0, ay0 = max(0, x0 - 1), max(0, y0 - 1)
1189 ax1, ay1 = min(width, x1 + 1), min(height, y1 + 1)
1190 labels = regions.labels[ay0:ay1, ax0:ax1]
1191 same = labels == index
1192 adjacent = bucket_fill.dilate(same, 1) & ~same
1193 sides = []
1194 for side in np.unique(labels[adjacent]).tolist():
1195 if side in regions.on_edge or regions.value(side) != 0:
1196 continue
1197 mask = regions.local_mask(side)
1198 box = regions.bounds(side)
1199 gy, gx, valid = _shifted(target, mask, box, shift)
1200 if valid.sum() < mask.sum() * HINT_SHARE: 1200 ↛ 1201line 1200 didn't jump to line 1201 because the condition on line 1200 was never true
1201 continue
1202 labels = initial.regions.labels[gy[valid], gx[valid]]
1203 label, fraction = _majority(labels)
1204 if label is None or fraction < HINT_SHARE: 1204 ↛ 1205line 1204 didn't jump to line 1205 because the condition on line 1204 was never true
1205 continue
1206 if initial.regions.value(label) != 0: 1206 ↛ 1207line 1206 didn't jump to line 1207 because the condition on line 1206 was never true
1207 continue
1208 if not (mask.sum() / MAX_AREA_RATIO <= initial.regions.sizes[label] 1208 ↛ 1210line 1208 didn't jump to line 1210 because the condition on line 1208 was never true
1209 <= mask.sum() * MAX_AREA_RATIO):
1210 continue
1211 ib = initial.regions.bounds(label)
1212 im = initial.regions.local_mask(label)
1213 value, fraction = _majority(guide.values[ib[1]:ib[3], ib[0]:ib[2]][im])
1214 if value == color and fraction >= HINT_SHARE:
1215 sides.append(side)
1216 answers.append((color, tuple(sides)))
1217 if not answers or any(answer != answers[0] for answer in answers[1:]):
1218 continue
1219 color, sides = answers[0]
1220 result.paints.append((_opaque_rgb(color), bounds, local))
1221 result.line_pixels += int(local.sum())
1222 decided.add(index)
1223 for side in sides:
1224 if side in decided: 1224 ↛ 1225line 1224 didn't jump to line 1225 because the condition on line 1224 was never true
1225 continue
1226 result.paints.append((_opaque_rgb(color), regions.bounds(side),
1227 regions.local_mask(side)))
1228 result.filled += 1
1229 decided.add(side)
1230 return decided
1233def paint_cell(pixels, guides: List[_Cell], *, finishes=None, learned=None,
1234 teacher: Optional[_Cell] = None, margin=SEARCH_MARGIN,
1235 hint=None, flow_for=None, hand_guides=(), hand_first=0,
1236 target: Optional[_Cell] = None) -> CellPaint:
1237 """塗り終えたセル ``guides`` を手本に、``pixels`` を塗る範囲と色を決める。
1239 部分ごとに ``guides`` を順に試し、最初に決まった手本に従う。仕上げ色を覚える
1240 (``learned`` へ足す)のは、人が塗った見本 ``teacher`` が一番目の手本のときだけ。
1242 :param hint: ``pixels`` が元の線画のとき、今の絵(塗りかけ)。どの手本でも決められ
1243 なかった部分だけ、今の絵で一色に塗ってあればその色に従う。渡したときは、元の
1244 線画から塗るための追加の手がかり(区画ごとの位置合わせ、形での照合)も使う。
1245 :param flow_for: ``flow_for(手本)`` で、このセルから手本への区画ごとの動き
1246 (:class:`line_flow.Flow`)。元の線画から塗るとき、透明な部分はまずこれで決める。
1247 :param hand_guides: 前後の、人が手で塗ったセル(手で塗った部分だけを塗り、ほかは
1248 決めていない所にした手本)。近い順。動きで重ねて、はっきり1色のときだけ使う。
1249 :param hand_first: ``hand_guides`` のうち、``guides`` の一番目より先に試す数
1250 (すぐ隣のセル)。ほかは一番目の手本の次に試す。
1251 :param target: ``pixels`` の部分分け。見本を増やして塗り直すとき、入力は同じ
1252 なので作り直さずに渡す(部分分けはセル1枚ぶんの計算で一番重い)。
1253 """
1254 finishes = {} if finishes is None else finishes
1255 learned = {} if learned is None else learned
1256 target = _Cell(pixels, target=True) if target is None else target
1257 target.from_lines = hint is not None
1258 result = CellPaint()
1259 trace_decided = _trace_first(target, guides, result, margin)
1260 # 手本を試す順:すぐ隣の手塗りのセル、一番目の手本、ほかの手塗りのセル、残りの手本。
1261 hand_set = set(hand_guides)
1262 order = list(hand_guides[:hand_first]) + guides[:1] + list(hand_guides[hand_first:]) \
1263 + guides[1:]
1264 regions = target.regions
1265 lines = []
1266 for index in np.nonzero(regions.sizes >= MIN_REGION_PIXELS)[0].tolist():
1267 if index in trace_decided:
1268 result.checked += 1
1269 continue
1270 value = regions.value(index)
1271 if value == GAP_VALUE:
1272 continue
1273 rgb = reference_check._to_rgb(value)
1274 bounds = regions.bounds(index)
1275 x0, y0, x1, y1 = bounds
1276 local = regions.local_mask(index)
1277 if rgb is None and index in regions.on_edge:
1278 # キャンバスの端につながる透明は、絵の外。ただ元の線画では、線の切れ目
1279 # から髪などが絵の外とつながることがある。
1280 if flow_for is not None and guides:
1281 flow = flow_for(guides[0])
1282 if flow is not None: 1282 ↛ 1284line 1282 didn't jump to line 1284 because the condition on line 1282 was always true
1283 result.paints.extend(_flow_outside(target, guides[0], flow, local, bounds))
1284 continue
1285 if rgb is not None and reference_check._is_line(rgb):
1286 continue # 黒の主線はそのまま
1287 result.checked += 1
1288 if (target.primary[y0:y1, x0:x1][local].any()
1289 or target.mark[y0:y1, x0:x1][local].any()):
1290 # 色トレス線と、原色でないしるしの色。どちらも手本の色に置き換える。
1291 lines.append((local, bounds, value))
1292 continue
1293 outcome, suspect, new_part = None, None, False
1294 ambiguous = False
1295 if hint is not None and rgb is None:
1296 # 人が手で塗った部分は確かな答え。そのまま残し、次のセルの手本にもする。
1297 hand = _hand_color(target, hint, local, bounds)
1298 if hand is not None:
1299 paint_bounds, mask = _region_mask(target, local, bounds, value)
1300 result.paints.append((hand, paint_bounds, mask))
1301 result.filled += 1
1302 continue
1303 for guide in order:
1304 if guide in hand_set:
1305 if rgb is not None: 1305 ↛ 1306line 1305 didn't jump to line 1306 because the condition on line 1305 was never true
1306 continue
1307 # 手で塗ったセルは、手で塗った部分だけを、はっきり1色のときに使う。
1308 flow = flow_for(guide) if flow_for is not None else None
1309 found = _flow_votes(target, guide, flow, local, bounds) if flow else None
1310 if flow is None or found is None or found == "open":
1311 continue
1312 kind, decided = _flow_decision(target, [(guide, flow, *found)], index, local,
1313 bounds, near=False)
1314 if kind == "paint": 1314 ↛ 1317line 1314 didn't jump to line 1317 because the condition on line 1314 was always true
1315 outcome = (kind, cast(Optional[Rgb], decided))
1316 break
1317 continue
1318 kind = None
1319 flow = flow_for(guide) if flow_for is not None and rgb is None else None
1320 if flow is not None:
1321 # 元の線画から塗るとき、透明な部分は区画ごとの動きで手本へ重ねて決める。
1322 found = _flow_votes(target, guide, flow, local, bounds)
1323 if found == "open": 1323 ↛ 1324line 1323 didn't jump to line 1324 because the condition on line 1323 was never true
1324 continue
1325 if found is not None: 1325 ↛ 1336line 1325 didn't jump to line 1336 because the condition on line 1325 was always true
1326 # 塗り分けと、一番の色が多いだけで決めるのは、動きの小さいすぐ隣の
1327 # セル(一番目の手本)でだけ。離れたセルは動きの見積もりが粗い。
1328 kind, decided = _flow_decision(target, [(guide, flow, *found)], index,
1329 local, bounds, near=guide is guides[0])
1330 new_rgb = None
1331 if kind == "ambiguous":
1332 # 対象セルでは一つの領域。見本の色分けで勝手な境界を作らない。
1333 ambiguous = True
1334 continue
1335 new_rgb = cast(Optional[Rgb], decided)
1336 if kind is None or kind == "unsure":
1337 kind, new_rgb = _region_outcome(target, guide, index, value, rgb, local,
1338 bounds, margin)
1339 if kind == "suspect":
1340 suspect = suspect or new_rgb
1341 elif kind == "new":
1342 new_part = True
1343 elif kind != "unsure":
1344 outcome = (kind, new_rgb)
1345 break
1346 size = int(regions.sizes[index])
1347 if (outcome is None and not ambiguous and rgb is None and suspect is None 1347 ↛ 1349line 1347 didn't jump to line 1349 because the condition on line 1347 was never true
1348 and guides and target.from_lines):
1349 matched = _shape_match(target, guides[0], index, local, bounds)
1350 if matched is not None:
1351 outcome = ("paint", matched)
1352 if (outcome is None and not ambiguous and rgb is None and suspect is None 1352 ↛ 1354line 1352 didn't jump to line 1354 because the condition on line 1352 was never true
1353 and hint is not None):
1354 kept = _hint_color(hint, local, bounds)
1355 if kept is not None:
1356 outcome = ("paint", kept)
1357 if new_part and (outcome is None or outcome[0] != "paint"):
1358 # どの手本でも塗れず、手本に無い形と分かった(手本で透明だった場合も、
1359 # 前のセルで同じく決められなかっただけなので知らせる)。
1360 if size >= REPORT_MIN_PIXELS: 1360 ↛ 1365line 1360 didn't jump to line 1365 because the condition on line 1360 was always true
1361 result.notes.append(reference_check.Finding(
1362 reference_check.KIND_NEW_PART, None, size, bounds=bounds,
1363 mask=local.copy(),
1364 ))
1365 if rgb is None: 1365 ↛ 1367line 1365 didn't jump to line 1367 because the condition on line 1365 was always true
1366 result.unpainted.append((bounds, local))
1367 continue
1368 if outcome is None:
1369 if suspect is not None and size >= SUSPECT_MIN_PIXELS:
1370 result.notes.append(reference_check.Finding(
1371 reference_check.KIND_SUSPECT, rgb, size, bounds=bounds,
1372 expected=suspect, mask=local.copy(),
1373 ))
1374 elif suspect is None: 1374 ↛ 1383line 1374 didn't jump to line 1383 because the condition on line 1374 was always true
1375 result.unsure += 1
1376 if rgb is None: 1376 ↛ 1378line 1376 didn't jump to line 1378 because the condition on line 1376 was always true
1377 result.unpainted.append((bounds, local))
1378 if rgb is None and size >= REPORT_MIN_PIXELS: 1378 ↛ 1383line 1378 didn't jump to line 1383 because the condition on line 1378 was always true
1379 result.notes.append(reference_check.Finding(
1380 reference_check.KIND_UNPAINTED, None, size, bounds=bounds,
1381 mask=local.copy(),
1382 ))
1383 continue
1384 kind, new_rgb = outcome
1385 if kind == "paint" and new_rgb is not None:
1386 paint_bounds, mask = _region_mask(target, local, bounds, value)
1387 result.paints.append((new_rgb, paint_bounds, mask))
1388 result.filled += 1
1389 # 色トレス線は、まわりの部分を塗ってから、となりの色を見て決める。線は1本の
1390 # 取り違えが目立つので、決められた手本みんなで「残す/塗る」を多数決にする
1391 # (同数なら先の手本に従う)。塗るときの色は、塗ると答えた最初の手本に従う。
1392 painted = fill_tracking._labels(apply_paint(pixels, result))
1393 line_start = len(result.paints)
1394 for local, bounds, value in lines:
1395 paints = _finish_paints(target, local, bounds, painted, value, finishes)
1396 if paints is not None:
1397 result.paints.extend(paints)
1398 result.line_pixels += int(local.sum())
1399 continue
1400 answers = []
1401 # 仕上げ色は、見本のすぐ隣のセル(見本が一番目の手本)からだけ覚える。
1402 learn = bool(guides) and guides[0] is teacher
1403 for guide in guides:
1404 shift, overlap = _local_shift(target, guide, bounds, margin=margin)
1405 if overlap < MIN_COLOR_LINE_OVERLAP:
1406 continue
1407 paints = _line_paints(target, guide, local, bounds, shift, painted, value,
1408 finishes, learned if learn and guide is teacher else {})
1409 if paints is not None:
1410 answers.append(paints)
1411 if not answers:
1412 # 見本から決められない線は、周辺色で推測せず初期状態のまま残す。
1413 result.undecided.append((bounds, local))
1414 size = int(local.sum())
1415 if size >= LINE_REPORT_MIN_PIXELS: 1415 ↛ 1420line 1415 didn't jump to line 1420 because the condition on line 1415 was always true
1416 result.notes.append(reference_check.Finding(
1417 reference_check.KIND_UNDECIDED_LINE, reference_check._to_rgb(value), size,
1418 bounds=bounds, mask=local.copy(),
1419 ))
1420 continue
1421 keep = sum(1 for paints in answers if not paints)
1422 if keep * 2 > len(answers) or (keep * 2 == len(answers) and not answers[0]):
1423 continue
1424 paints = next(paints for paints in answers if paints)
1425 result.paints.extend(paints)
1426 result.line_pixels += sum(int(mask.sum()) for _rgb, _bounds, mask in paints)
1427 # 小さすぎて手本と比べられなかった穴は、塗り終えたまわりの色で埋める。色トレス線の
1428 # ぶんだけ ``painted`` へ書き足す(もう一度セル全体に塗り直すより軽い)。
1429 for rgb, (x0, y0, x1, y1), mask in result.paints[line_start:]:
1430 painted[y0:y1, x0:x1][mask] = reference_check._pack(rgb)
1431 result.paints.extend(_tiny_holes(target, painted))
1432 return result
1435def paint_from_guide(pixels, guide_pixels, margin=SEARCH_MARGIN) -> CellPaint:
1436 """塗り終えたセル ``guide_pixels`` 1枚を手本に、``pixels`` を塗る範囲と色を決める。"""
1437 return paint_cell(pixels, [_Cell(guide_pixels)], margin=margin)
1440def apply_paint(pixels, paint: CellPaint) -> np.ndarray:
1441 """``paint`` を適用した複製を返す(次のセルの手本にする)。"""
1442 out = pixels.copy()
1443 for rgb, (x0, y0, x1, y1), mask in paint.paints:
1444 patch = out[y0:y1, x0:x1]
1445 patch[mask, :3] = rgb
1446 patch[mask, 3] = 255
1447 return out
1450def _opaque_rgb(value) -> Rgb:
1451 rgb = reference_check._to_rgb(value)
1452 assert rgb is not None, "仕上げ色の集計に透明は入らない"
1453 return rgb
1456def _finishes(learned) -> Dict[Tuple[int, int], int]:
1457 """集計から、覚えた仕上げ色を選ぶ。"""
1458 finishes = {}
1459 for key, counts in learned.items():
1460 color, count = max(counts.items(), key=lambda item: item[1])
1461 if count >= MIN_FINISH_PIXELS: 1461 ↛ 1459line 1461 didn't jump to line 1459 because the condition on line 1461 was always true
1462 finishes[key] = color
1463 return finishes
1466def _order(count, reference_index, second_reference_index=None, reference_indices=None):
1467 """見本から近い順に。``(セル, 見本の側の隣)``。"""
1468 references = {reference_index}
1469 references.update(reference_indices or [])
1470 if second_reference_index is not None: 1470 ↛ 1471line 1470 didn't jump to line 1471 because the condition on line 1470 was never true
1471 references.add(second_reference_index)
1472 nearest = {index: min(references, key=lambda ref: (abs(index - ref), ref))
1473 for index in range(count) if index not in references}
1474 order = sorted((abs(index - ref), index) for index, ref in nearest.items())
1475 return [
1476 (index, index - 1 if index > nearest[index] else index + 1)
1477 for _distance, index in order
1478 ]
1481@overload
1482def plan_auto_paint(
1483 cells: Sequence[np.ndarray], reference_index: int, *,
1484 progress: None = None, **options: Any,
1485) -> AutoPlan: ...
1488@overload
1489def plan_auto_paint(
1490 cells: Sequence[np.ndarray], reference_index: int, *,
1491 progress: Callable[[int, int], bool], **options: Any,
1492) -> Optional[AutoPlan]: ...
1495def plan_auto_paint(
1496 cells: Sequence[np.ndarray], reference_index: int, *,
1497 progress: Optional[Callable[[int, int], bool]] = None,
1498 **options: Any,
1499) -> Optional[AutoPlan]:
1500 """見本から順に塗る。先行準備は色の引き継ぎ順序を変えず、終了時に回収する。"""
1501 with ExitStack() as resources:
1502 return _plan_auto_paint(
1503 cells, reference_index, progress=progress, _resources=resources, **options,
1504 )
1507def _plan_auto_paint(
1508 cells: Sequence[np.ndarray],
1509 reference_index: int,
1510 *,
1511 second_reference_index: Optional[int] = None,
1512 reference_indices: Optional[List[int]] = None,
1513 progress: Optional[Callable[[int, int], bool]] = None,
1514 margin: int = SEARCH_MARGIN,
1515 max_rounds: int = MAX_ROUNDS,
1516 hints: Optional[Sequence[Optional[np.ndarray]]] = None,
1517 lines: Optional[Sequence[Optional[np.ndarray]]] = None,
1518 bucket_options: Optional[bucket_fill.BucketOptions] = None,
1519 gpu: str = "auto",
1520 preparation_workers: int = 2,
1521 reuse_reference_regions: bool = True,
1522 _resources: ExitStack,
1523) -> Optional[AutoPlan]:
1524 """``cells[reference_index]`` を見本に、前後へ1セルずつ塗り進める。
1526 ``reference_indices`` の見本も確定見本として保ち、近い見本から
1527 1セルずつ塗り進める。見本自身は ``plan.cells`` に含めず、塗り替えない。
1529 一通り塗ったら、迷った部分の少ないセルを見本に加えて、見本以外のセルを
1530 もう一度塗る(``max_rounds`` 回まで、加えるセルが無くなったら終わり)。
1531 加えた見本は、前の回の塗りを手本として使う。隣のセルで決められない部分も、
1532 少し離れた見本や、反対側の隣(前の回の塗り)から決められることがある。
1534 :param progress: ``progress(済んだ数, 全体)``。偽を返すと中止して ``None`` を返す。
1535 :param hints: ``cells`` が元の線画のとき、セルごとの今の絵(塗りかけ)。無いセルは None。
1536 :param lines: セルごとの元の線画(動きの推定に使う)。無いセルは ``cells`` の線を使う。
1537 見本は色トレス線を塗りつぶしてあることが多いので、元の線画があると動きが確かになる。
1538 :param bucket_options: 通常のバケツ設定。指定時は見本から色とクリック位置だけを選び、
1539 範囲は通常のバケツで決める。塗りかけを確定見本として扱わない。
1540 """
1541 fixed = {reference_index}
1542 fixed.update(reference_indices or [])
1543 if second_reference_index is not None:
1544 fixed.add(second_reference_index)
1545 for index in fixed:
1546 if not 0 <= index < len(cells): 1546 ↛ 1547line 1546 didn't jump to line 1547 because the condition on line 1546 was never true
1547 raise ValueError("Reference is outside the cell list")
1548 if cells[index].shape != cells[reference_index].shape: 1548 ↛ 1549line 1548 didn't jump to line 1549 because the condition on line 1548 was never true
1549 raise ValueError("Reference cells must have the same dimensions")
1550 plan = AutoPlan()
1551 shape = cells[reference_index].shape
1552 usable = {index for index, pixels in enumerate(cells) if pixels.shape == shape}
1553 work = [item for item in _order(len(cells), reference_index, reference_indices=fixed)
1554 if item[0] in usable]
1555 previous = {index: cells[index] for index in fixed}
1556 references = set(fixed)
1557 learned: Dict[Tuple[int, int], Dict[int, int]] = {}
1558 # 区画ごとの動きは線だけで決まるので、セルの組ごとに一度だけ求めて使い回す。
1559 # 塗るセルの部分分けも、入力が変わらないので回をまたいで使い回す。
1560 targets: Dict[int, _Cell] = {}
1561 boundaries: Dict[int, np.ndarray] = {}
1562 flows: Dict[Tuple[int, int], line_flow.Flow] = {}
1563 bucket_work = {}
1564 alignment = None
1565 preparation = None
1566 reference_regions = None
1567 if bucket_options is not None:
1568 from .auto_alignment import AlignmentCache
1569 from .auto_preparation import Preparation, ReferenceRegions
1570 alignment = AlignmentCache(gpu=gpu, expected_cells=len(work))
1571 _resources.callback(alignment.close)
1572 alignment.enable_regions(shape[0] * shape[1])
1573 if reuse_reference_regions:
1574 reference_regions = ReferenceRegions(gpu_regions=alignment.regions)
1576 def target_for(index):
1577 if index not in targets:
1578 ready = preparation.take(index) if preparation is not None else None
1579 if ready is None: 1579 ↛ 1583line 1579 didn't jump to line 1583 because the condition on line 1579 was always true
1580 targets[index] = _Cell(cells[index], target=True)
1581 targets[index]._native_immutable = True
1582 else:
1583 targets[index], bucket_work[index] = ready
1584 if preparation is not None: 1584 ↛ 1585line 1584 didn't jump to line 1585 because the condition on line 1584 was never true
1585 preparation.schedule(bucket_work)
1586 return targets[index]
1588 if (bucket_options is not None and preparation_workers > 0 and max_rounds > 0 and len(work) >= 4
1589 and shape[0] * shape[1] >= 256 * 256):
1590 from . import auto_bucket
1592 def prepare_target(index):
1593 target = targets.get(index)
1594 if target is None: 1594 ↛ 1597line 1594 didn't jump to line 1597 because the condition on line 1594 was always true
1595 target = _Cell(cells[index], target=True)
1596 target._native_immutable = True
1597 prepared = auto_bucket.BucketWork(
1598 target, bucket_options, alignment=alignment, reference_regions=reference_regions,
1599 )
1600 prepared.collect()
1601 return target, prepared
1603 preparation = _resources.enter_context(Preparation(
1604 (index for index, _toward in work), prepare_target, preparation_workers,
1605 ))
1606 preparation.schedule(bucket_work)
1608 def boundary(index):
1609 if index not in boundaries:
1610 source = lines[index] if lines is not None and lines[index] is not None else None
1611 boundaries[index] = boundary_mask(source if source is not None else cells[index])
1612 return boundaries[index]
1614 def flow_between(index, guide_cell):
1615 if guide_cell.index is None: 1615 ↛ 1616line 1615 didn't jump to line 1616 because the condition on line 1615 was never true
1616 return None
1617 key = (index, guide_cell.index)
1618 if key not in flows:
1619 flows[key] = line_flow.estimate_flow(boundary(index), boundary(guide_cell.index))
1620 return flows[key]
1622 # 人が手で塗ったセル:手で塗った部分だけを塗った手本(ほかは決めていない所)。
1623 hand_cells: Dict[int, _Cell] = {}
1624 for index, hint in enumerate(hints or []):
1625 if bucket_options is not None:
1626 break # 過去の自動塗りを手塗りと区別できないため、確定見本にはしない。
1627 if hint is None or index in fixed or index not in usable:
1628 continue
1629 hand = _hand_paint(cells[index], hint, target=target_for(index))
1630 if not hand.paints:
1631 continue
1632 hand_cell = _Cell(apply_paint(cells[index], hand)).mark_undecided([], hand.unpainted)
1633 hand_cell.index = index
1634 hand_cells[index] = hand_cell
1636 def hand_guides_for(index):
1637 # 前後それぞれ、一番近い手塗りのセルだけ(多く使うと動きの推定に時間がかかる)。
1638 near = []
1639 for direction in (-1, 1):
1640 side = [other for other in hand_cells
1641 if 0 < (other - index) * direction <= HAND_REACH]
1642 if side:
1643 near.append(min(side, key=lambda other: abs(other - index)))
1644 near.sort(key=lambda other: abs(other - index))
1645 return ([hand_cells[other] for other in near],
1646 sum(1 for other in near if abs(other - index) == 1))
1648 fixed_guides = {}
1649 trace_palette = None
1650 trace_contexts = None
1651 tracker = None
1652 if bucket_options is not None:
1653 from .auto_part_tracking import PartTracker
1655 def tracking_geometry(index):
1656 if index in fixed_guides:
1657 return fixed_guides[index].initial or fixed_guides[index]
1658 return target_for(index)
1660 tracker = PartTracker(shape[:2], tracking_geometry, alignment=alignment)
1661 total = len(work) * max_rounds
1662 done = 0
1663 for _round in range(max_rounds): 1663 ↛ 1802line 1663 didn't jump to line 1802 because the loop on line 1663 didn't complete
1664 plan.rounds += 1
1665 current = {index: cells[index] for index in fixed}
1666 made: Dict[Tuple[str, int], _Cell] = {}
1667 sources = {"now": current, "before": previous}
1668 # 決められずに残した線:この回の分と、前の回の分。
1669 undecided_before = {
1670 index: (paint.undecided, paint.unpainted) for index, paint in plan.cells.items()
1671 }
1672 undecided_now: Dict[int, tuple] = {}
1673 part_paints = {"now": {}, "before": dict(plan.cells)}
1675 def guide(source, index, made=made, sources=sources,
1676 undecided_now=undecided_now, undecided_before=undecided_before,
1677 part_paints=part_paints):
1678 key = (source, index)
1679 if index in fixed:
1680 if index not in fixed_guides:
1681 fixed_guides[index] = _Cell(cells[index])
1682 fixed_guides[index].index = index
1683 if lines is not None and lines[index] is not None:
1684 fixed_guides[index].initial = _Cell(lines[index])
1685 fixed_guides[index].initial._native_immutable = True
1686 fixed_guides[index]._native_immutable = True
1687 return fixed_guides[index]
1688 if key not in made:
1689 cell = _Cell(sources[source][index])
1690 if bucket_options is not None and lines is not None and lines[index] is not None:
1691 cell.initial = target_for(index)
1692 undecided = (undecided_now if source == "now" else undecided_before)
1693 made[key] = cell.mark_undecided(*undecided.get(index, ([], [])))
1694 made[key].index = index
1695 if bucket_options is not None:
1696 confirmed = np.zeros(cell.values.shape, dtype=bool)
1697 cell.confirmed = confirmed
1698 part = part_paints[source].get(index)
1699 if part is not None: 1699 ↛ 1702line 1699 didn't jump to line 1702 because the condition on line 1699 was always true
1700 for _rgb, (x0, y0, x1, y1), mask in part.paints:
1701 confirmed[y0:y1, x0:x1] |= mask
1702 made[key]._native_immutable = True
1703 return made[key]
1705 teachers = {index: guide("now", index) for index in sorted(fixed)}
1706 if bucket_options is not None and trace_palette is None:
1707 from . import auto_trace
1708 trace_palette = auto_trace.finish_palette(teachers.values())
1709 trace_contexts = tuple(auto_trace.palette(item) for item in teachers.values())
1710 plan.trace_palette = trace_palette
1711 plan.trace_face_rules = auto_trace.enclosed_face_rules(teachers.values(), trace_palette)
1712 assert tracker is not None
1713 for fixed_index, fixed_guide in teachers.items():
1714 tracker.remember_fixed(fixed_index, fixed_guide)
1715 for index, toward in work:
1716 if progress is not None and not progress(done, total):
1717 return None
1718 if preparation is not None:
1719 preparation.schedule(bucket_work)
1720 ready = preparation.take(index)
1721 if ready is not None: 1721 ↛ 1723line 1721 didn't jump to line 1723 because the condition on line 1721 was always true
1722 targets[index], bucket_work[index] = ready
1723 preparation.schedule(bucket_work)
1724 done += 1
1725 nearest = min(fixed, key=lambda ref: (abs(index - ref), ref))
1726 teacher = teachers[nearest]
1727 guides = [guide("now", toward)] if toward in current else []
1728 # 前の回の塗り:反対側の隣と、近くの見本(両側)。
1729 side = 2 * index - toward
1730 extra = [side] if side in previous else []
1731 for direction in (-1, 1):
1732 near = [ref for ref in references if (ref - index) * direction > 0]
1733 if near:
1734 extra.append(min(near, key=lambda ref: abs(ref - index)))
1735 for other in dict.fromkeys(extra):
1736 if other in fixed:
1737 if abs(index - other) <= TEACHER_REACH and teachers[other] not in guides:
1738 guides.append(teachers[other])
1739 elif other != toward:
1740 guides.append(guide("before", other))
1741 if teacher not in guides and abs(index - nearest) <= TEACHER_REACH:
1742 guides.append(teacher)
1743 hint = hints[index] if hints is not None else None
1744 hand_guides, hand_first = hand_guides_for(index)
1745 if bucket_options is not None:
1746 from . import auto_bucket
1747 assert tracker is not None
1749 def tracked_guide(donor_index, current=current, guide=guide):
1750 return guide("now" if donor_index in current else "before", donor_index)
1751 part_indices = sorted(
1752 (other for other in set(current) | set(previous)
1753 if other not in fixed and other != index and abs(other - index) <= 4
1754 and all(item.index != other for item in guides)),
1755 key=lambda other: (abs(other - index), other),
1756 )[:2]
1757 part_guides = [guide("now" if other in current else "before", other)
1758 for other in part_indices]
1759 if index not in bucket_work:
1760 bucket_work[index] = auto_bucket.BucketWork(
1761 target_for(index), bucket_options, alignment=alignment,
1762 reference_regions=reference_regions,
1763 )
1764 if len(bucket_work[index].collect()) >= 16: 1764 ↛ 1765line 1764 didn't jump to line 1765 because the condition on line 1764 was never true
1765 tracker.prime_motion(index, target_for(index), guides)
1766 paint = auto_bucket.paint_cell(
1767 cells[index], guides, bucket_options, target=target_for(index), margin=margin,
1768 work=bucket_work[index],
1769 trace_guides=list(teachers.values()),
1770 trace_palette=trace_palette, part_guides=part_guides,
1771 trace_face_rules=plan.trace_face_rules,
1772 trace_contexts=trace_contexts,
1773 tracked_color=lambda filled, index=index, tracker=tracker,
1774 guide_for=tracked_guide: tracker.color(
1775 index, target_for(index), filled, guide_for,
1776 ),
1777 )
1778 else:
1779 paint = paint_cell(
1780 cells[index], guides, finishes=_finishes(learned), learned=learned,
1781 teacher=teacher, margin=margin, hint=hint, hand_guides=hand_guides,
1782 hand_first=hand_first,
1783 flow_for=(lambda guide_cell, index=index: flow_between(index, guide_cell))
1784 if hint is not None else None,
1785 target=target_for(index),
1786 )
1787 plan.cells[index] = paint
1788 part_paints["now"][index] = paint
1789 current[index] = apply_paint(cells[index], paint)
1790 undecided_now[index] = (paint.undecided, paint.unpainted)
1791 if tracker is not None:
1792 tracker.remember(index, paint)
1793 previous = current
1794 confident = {
1795 index for index, paint in plan.cells.items()
1796 if paint.unsure_share <= CONFIDENT_UNSURE_SHARE
1797 }
1798 added = confident - references
1799 references |= added
1800 if not added or len(references) == len(usable):
1801 break # 増やす見本が無い、またはみんな見本になった
1802 plan.references = sorted(references)
1803 plan.finishes = {
1804 (_opaque_rgb(line), _opaque_rgb(host)): _opaque_rgb(color)
1805 for (line, host), color in _finishes(learned).items()
1806 }
1807 if progress is not None:
1808 progress(total, total)
1809 if alignment is not None:
1810 plan.optimization_stats = dict(
1811 alignment_hits=alignment.hits, cpu_alignments=alignment.cpu_solves,
1812 alignment_batches=alignment.cpu_batches,
1813 batched_alignments=alignment.batched_solves,
1814 shared_motion_certified=alignment.motion_certified,
1815 shared_motion_regions=alignment.shared_support_hits,
1816 shared_motion_trials=alignment.motion_trials,
1817 shared_support_checks=alignment.support_checks,
1818 gpu_alignment_uploads=0 if alignment._resident is None else alignment._resident.uploads,
1819 gpu_alignment_return_bytes=0 if alignment._resident is None else alignment._resident.returned_bytes,
1820 gpu_region_samples=0 if alignment.regions is None else alignment.regions.sample_count,
1821 gpu_sample_batches=0 if alignment.regions is None else alignment.regions.sample_batches,
1822 gpu_guide_uploads=0 if alignment.regions is None else alignment.regions.guide_uploads,
1823 routed_cpu_samples=0 if alignment.regions is None else alignment.regions.cpu_samples,
1824 routed_cpu_indices=0 if alignment.regions is None else alignment.regions.cpu_indices,
1825 gpu_resident_shift_samples=0 if alignment.regions is None else alignment.regions.device_shift_samples,
1826 gpu_bucket_indices=0 if alignment.regions is None else alignment.regions.index_count,
1827 gpu_region_error=alignment.region_error if alignment.regions is None else alignment.regions.error,
1828 gpu_sample_seconds=0.0 if alignment.regions is None else alignment.regions.sample_seconds,
1829 gpu_bucket_seconds=0.0 if alignment.regions is None else alignment.regions.index_seconds,
1830 gpu_alignments=alignment.gpu_solves, gpu_error=alignment.gpu_error,
1831 reused_decisions=sum(item.reused_decisions for item in bucket_work.values()),
1832 color_decisions=sum(item.decision_solves for item in bucket_work.values()),
1833 prepared_cells=0 if preparation is None else preparation.prepared,
1834 preparation_workers=0 if preparation is None else preparation.workers,
1835 reference_cache_hits=0 if reference_regions is None else reference_regions.hits,
1836 reference_sessions=0 if reference_regions is None else reference_regions.created,
1837 reference_region_hits=0 if reference_regions is None else reference_regions.region_hits,
1838 )
1839 if tracker is not None: 1839 ↛ 1843line 1839 didn't jump to line 1843 because the condition on line 1839 was always true
1840 plan.optimization_stats.update(tracked_parts=tracker.matches,
1841 long_memory_parts=tracker.long_matches,
1842 deformation_pairs=tracker.flow_solves)
1843 return plan
1846__all__ = [
1847 "AutoPlan",
1848 "CellPaint",
1849 "apply_paint",
1850 "boundary_mask",
1851 "gap_mask",
1852 "line_only",
1853 "mark_mask",
1854 "paint_cell",
1855 "paint_from_guide",
1856 "plan_auto_paint",
1857 "primary_line_mask",
1858]