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

1"""色分けの線を手がかりに、見本のセルから同じレイヤーのほかのセルを自動で塗る。 

2 

3原画では、黒の主線と、原色の赤・緑・青の色トレス線(範囲線・斜線・影の境目など) 

4が色分けの境界になる。黄・水色・赤紫(原色2つを重ねた色)の線も同じに扱う。 

5そこで対象セルを、これらの線で区切られた「部分」に分け、 

6部分ごとに、もう塗り終えた隣のセルの同じ場所の色を写す。 

7 

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 線」として知らせる。 

31 

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 

38 

39import numpy as np 

40 

41from . import bucket_fill, fill_tracking, line_flow, reference_check 

42 

43Rgb = Tuple[int, int, int] 

44Bounds = Tuple[int, int, int, int] 

45 

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 

165 

166 

167@dataclass 

168class CellPaint: 

169 """1セル分の塗り替え。``(色, 外接矩形, 矩形の中のマスク)`` の並び。""" 

170 

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) 

191 

192 @property 

193 def unsure_share(self) -> float: 

194 return self.unsure / self.checked if self.checked else 0.0 

195 

196 

197@dataclass 

198class AutoPlan: 

199 """自動塗りの計画。セルの番号は呼び出し側が渡した並びの位置。""" 

200 

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) 

213 

214 @property 

215 def painted_cells(self) -> int: 

216 return sum(1 for cell in self.cells.values() if cell.paints) 

217 

218 

219def primary_line_mask(pixels, tolerance=PRIMARY_TOLERANCE, 

220 low_tolerance=PRIMARY_LOW_TOLERANCE) -> np.ndarray: 

221 """色トレス線の画素:原色の赤・緑・青と、2つを重ねた黄・水色・赤紫。 

222 

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 

241 

242 

243def _is_primary(rgb) -> bool: 

244 pixel = np.array([[[*rgb, 255]]], dtype=np.uint8) 

245 return bool(primary_line_mask(pixel)[0, 0]) 

246 

247 

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 

256 

257 

258def boundary_mask(pixels) -> np.ndarray: 

259 """色分けの境界:黒の主線と、色トレス線。""" 

260 return fill_tracking.line_art(pixels) | primary_line_mask(pixels) 

261 

262 

263def mark_mask(pixels, black=None, primary=None, max_share=MARK_MAX_SHARE, 

264 thickness=MARK_THICKNESS) -> np.ndarray: 

265 """原画のしるしの色の画素:原色ではないのに、細い線としてだけ置かれている色。 

266 

267 頭のチャームのピンクのように、原色でない色で描き足されたしるしがある。 

268 わざわざ違う色で描いてあるのは「まわりと色が違う」という指示なので、まわりの色で 

269 塗りつぶしてはいけない。色トレス線と同じに扱い、手本で同じ場所が何色かを見て決める。 

270 

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 

296 

297 

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) 

302 

303 

304class _TargetRegions(reference_check.CellRegions): 

305 """対象の線だけで範囲を固定する。既存の塗り色は境界にしない。""" 

306 

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 = {} 

313 

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] 

324 

325 

326class _Cell: 

327 """1セル分の、部分分けと境界。""" 

328 

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 

366 

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 

373 

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 

380 

381 

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) 

394 

395 

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 

404 

405 

406def _sample(target: _Cell, guide: _Cell, local, bounds, shift): 

407 """``local``(``bounds`` の中のマスク)をずらして手本へ重ね、一番多い色を返す。 

408 

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]]) 

417 

418 

419def _similar_size(target: _Cell, guide: _Cell, index, local, bounds, shift) -> bool: 

420 """部分を手本へずらして重ねた先の部分が、大きさの近い部分か。 

421 

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 

434 

435 

436def _line_paints(target: _Cell, guide: _Cell, local, bounds, shift, painted, value, 

437 finishes, learned): 

438 """色トレス線1本を、手本で同じ場所の色にする塗り。手本でも線なら空。 

439 

440 線の形は対象から決める。手本の色がそろわなければ未決定として残し、 

441 手本の境界で線を画素単位に切り分けない。 

442 

443 使う色は、このセルで線のとなりにある色(``painted`` は部分を塗ったあとの色の 

444 値)か、手本で線の幅ほどの小さな部分の色(斜線の仕上げ色など)に限る。 

445 ずれて重なった先の、となりに無い大きな部分の色(髪など)では塗らない。 

446 

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())] 

478 

479 

480def _finish_paints(target: _Cell, local, bounds, painted, value, finishes): 

481 """覚えた仕上げ色 ``(線の値, まわりの値) -> 色の値`` が当てはまれば、その塗り。 

482 

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)) 

495 

496 

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 

510 

511 

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 

527 

528 

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 

539 

540 

541def _local_shift(target: _Cell, guide: _Cell, bounds, context=CONTEXT, margin=SEARCH_MARGIN): 

542 """部分のまわり(``bounds`` を ``context`` だけ広げた矩形)の境界線が、 

543 手本でどれだけずれた所にあるか。 

544 

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) 

550 

551 

552class _ShiftProblem(dict): 

553 """GPUで作る見本の窓は、CPUへ戻す必要が生じたときだけ構築する。""" 

554 

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) 

565 

566 

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 

594 

595 

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 

608 

609 

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])] 

617 

618 

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) 

629 

630 

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 

641 

642 

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 

653 

654 

655def _refine(target: _Cell, guide: _Cell, local, bounds, shift): 

656 """``shift`` のまわり ``REFINE_RADIUS`` 画素で、色が一番そろうずれ。 

657 

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 

671 

672 

673def _is_new_part(target: _Cell, guide: _Cell, index, local, bounds, shift) -> bool: 

674 """手本に無い形か(開いた口、新しく見えてきたフリルなど)。 

675 

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 

701 

702 

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()) 

709 

710 

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 

722 

723 

724def _hand_color(target: _Cell, hint, local, bounds) -> Optional[Rgb]: 

725 """元の線画の部分を、今の絵で人が一色に塗ってあれば、その色(確かな答え)。 

726 

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 

739 

740 

741def _hand_paint(pixels, hint, *, target=None) -> CellPaint: 

742 """元の線画 ``pixels`` のうち、今の絵 ``hint`` で人が塗ってある部分。 

743 

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 

765 

766 

767def _shape_match(target: _Cell, guide: _Cell, index, local, bounds) -> Optional[Rgb]: 

768 """手本の近くで、大きさと形がよく似た塗ってある部分の色。無ければ None。 

769 

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] 

812 

813 

814def _tiled_outcome(target: _Cell, guide: _Cell, local, bounds, margin): 

815 """大きな部分を ``TILE`` 画素の区画に分け、区画ごとにずれを求めて色を数える。 

816 

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 

850 

851 

852def _flow_votes(target: _Cell, guide: _Cell, flow: line_flow.Flow, local, bounds): 

853 """透明な部分1つを、区画ごとの動き ``flow`` で手本へ重ねたときの色の割合。 

854 

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) 

886 

887 

888def _flow_decision(target: _Cell, votes, index, local, bounds, near=True): 

889 """手本ごとの色の割合 ``votes`` ``[(手本, 動き, 割合, 重み, 座標)]`` を重みで足して決める。 

890 

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 

936 

937 

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 

949 

950 

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()) 

961 

962 

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 

969 

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()) 

974 

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 

987 

988 

989def _flow_outside(target: _Cell, guide: _Cell, flow: line_flow.Flow, local, bounds): 

990 """画面端に接する、対象セルの線で区切った領域を一色で塗る。 

991 

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)] 

1013 

1014 

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) 

1018 

1019 

1020 

1021def _region_outcome(target: _Cell, guide: _Cell, index, value, rgb, local, bounds, margin): 

1022 """部分1つを ``guide`` で決める。``(種類, 色)``。 

1023 

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 

1067 

1068 

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 

1081 

1082 

1083def _neighbour_color(target: _Cell, painted, local, window): 

1084 """穴に接している色の値のうち一番多いもの、その割合、それが線の色か。 

1085 

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 

1101 

1102 

1103def _tiny_holes(target: _Cell, painted): 

1104 """塗り終えたあとに残った透明の画素を、となりの色で埋める塗り。 

1105 

1106 線のきわや頬の中には、数画素だけ透明の穴が残る。部分として扱うには小さすぎて 

1107 手本と比べられなかった画素や、部分分けのために閉じた切れ目の画素などで、 

1108 仕上げでは背景が透けて見えてしまう。埋めるのは次の2つ。 

1109 

1110 * ``TINY_HOLE_PIXELS`` までの小さな穴。``TINY_HOLE_ANY_PIXELS`` 以下なら、 

1111 となりで一番多い色がどれだけ少なくても埋める(1画素の点は、どちらの塗りの 

1112 色にしても見た目は変わらないが、透けると必ず目立つ)。 

1113 * 部分分けのために閉じた色トレス線の切れ目(``_Cell.gaps``)。こちらで足した 

1114 画素で、絵の一部ではないので、大きさによらず埋める。 

1115 

1116 大きな塗り残しは埋めず、これまでどおり「確かめたい所」で知らせる。 

1117 

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 

1159 

1160 

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 

1231 

1232 

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`` を塗る範囲と色を決める。 

1238 

1239 部分ごとに ``guides`` を順に試し、最初に決まった手本に従う。仕上げ色を覚える 

1240 (``learned`` へ足す)のは、人が塗った見本 ``teacher`` が一番目の手本のときだけ。 

1241 

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 

1433 

1434 

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) 

1438 

1439 

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 

1448 

1449 

1450def _opaque_rgb(value) -> Rgb: 

1451 rgb = reference_check._to_rgb(value) 

1452 assert rgb is not None, "仕上げ色の集計に透明は入らない" 

1453 return rgb 

1454 

1455 

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 

1464 

1465 

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 ] 

1479 

1480 

1481@overload 

1482def plan_auto_paint( 

1483 cells: Sequence[np.ndarray], reference_index: int, *, 

1484 progress: None = None, **options: Any, 

1485) -> AutoPlan: ... 

1486 

1487 

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]: ... 

1493 

1494 

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 ) 

1505 

1506 

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セルずつ塗り進める。 

1525 

1526 ``reference_indices`` の見本も確定見本として保ち、近い見本から 

1527 1セルずつ塗り進める。見本自身は ``plan.cells`` に含めず、塗り替えない。 

1528 

1529 一通り塗ったら、迷った部分の少ないセルを見本に加えて、見本以外のセルを 

1530 もう一度塗る(``max_rounds`` 回まで、加えるセルが無くなったら終わり)。 

1531 加えた見本は、前の回の塗りを手本として使う。隣のセルで決められない部分も、 

1532 少し離れた見本や、反対側の隣(前の回の塗り)から決められることがある。 

1533 

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) 

1575 

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] 

1587 

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 

1591 

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 

1602 

1603 preparation = _resources.enter_context(Preparation( 

1604 (index for index, _toward in work), prepare_target, preparation_workers, 

1605 )) 

1606 preparation.schedule(bucket_work) 

1607 

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] 

1613 

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] 

1621 

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 

1635 

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)) 

1647 

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 

1654 

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) 

1659 

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)} 

1674 

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] 

1704 

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 

1748 

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 

1844 

1845 

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]