Coverage for paintmaskanimator/canvas_skewer_fill.py: 75%

321 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 04:50 +0000

1"""串刺し塗り(バケツの全コマ一括適用)を PaintCanvas に足す mixin。 

2 

3串刺し塗りは、クリックした図形を選択レイヤーの各コマで追いかけて塗る。 

4コマごとに絵は動くので、同じ座標を種にし続けると途中で図形の外へ出て背景を 

5塗り始める。そこで現在コマから前後へ1コマずつ進み、直前のコマで塗った領域と 

6重なる図形を選び直す(:func:`bucket_fill.track_fill_region`)。 

7 

8追跡の信用度が下がったコマでは勝手に塗らずに止まり、そのコマを表示して 

9ユーザーに塗る場所をクリックしてもらう(Esc でそのコマを飛ばす)。 

10すべてのコマが決まってから一度に塗るので、Undo は1回で全コマ戻る。 

11 

12既存のセルにだけ塗る。保持セルを新しいキーに分けたり、空セルに絵を 

13作ったりはしない。 

14""" 

15from __future__ import annotations 

16 

17from dataclasses import dataclass, field 

18from typing import Any, Dict, List, Optional, Tuple 

19 

20import numpy as np 

21from PySide6.QtCore import QRectF, Qt 

22from PySide6.QtGui import QColor, QImage, QPainter 

23 

24from . import bucket_fill, frame_scope, imaging 

25from ._canvas_members import CanvasMembers 

26from .i18n import tr 

27from .time_display import frame_position 

28from .logging_setup import get_logger 

29from .progress import close_counter, create_counter, update_counter 

30 

31log = get_logger(__name__) 

32 

33# 追跡の重なり(IoU)がこれ未満のコマは、自動では塗らずにユーザーに指定してもらう。 

34TRACK_CONFIDENT_IOU = 0.5 

35 

36# 手動指定を待っているコマで、前のコマの塗り範囲を示す色。 

37_GUIDE_RGBA = (255, 64, 160, 90) 

38# 追跡が「たぶんここ」と推測した範囲を示す色(Enter で採用できる)。 

39_GUESS_RGBA = (40, 140, 255, 110) 

40 

41Cell = Tuple[int, int] 

42 

43 

44@dataclass 

45class SkewerFillSession: 

46 """串刺し塗り1回分の途中経過。手動指定で止まっている間も保持する。""" 

47 

48 scope: Any 

49 reference: Optional[Tuple[int, int, int]] 

50 options: bucket_fill.BucketOptions 

51 selection_mask: Optional[np.ndarray] 

52 replacement: Any 

53 # 塗り始めたときに表示していたコマ。終わったらここへ戻す。 

54 origin_frame: int 

55 # これから見るセル。前方向と後ろ方向の2本。 

56 queues: List[List[Cell]] 

57 # 方向ごとの「直前に塗れたコマの領域」。 

58 previous: List[np.ndarray] 

59 # 方向ごとの「直前に塗れたコマの画素」。コマ間のずれの推定に使う。 

60 previous_pixels: List[Optional[np.ndarray]] 

61 # 走査対象のセル一覧。途中でタイムラインが変わっていないかの確認用。 

62 cells: List[Cell] 

63 direction: int = 0 

64 # ``id(image)`` → ``(外接矩形, 切り出したマスク, ((置換色, 切り出した島), ...))``。 

65 regions: Dict[ 

66 int, 

67 Tuple[ 

68 Tuple[int, int, int, int], 

69 np.ndarray, 

70 Tuple[Tuple[Tuple[int, int, int], np.ndarray], ...], 

71 ], 

72 ] = field(default_factory=dict) 

73 # ``id(image)`` → ``(image, 計画したときの cacheKey)``。途中で描き換えられて 

74 # いないかの確認用。 

75 images: Dict[int, Tuple[QImage, int]] = field(default_factory=dict) 

76 skipped: int = 0 

77 # 手動指定を待っているセル ``(frame, layer)``。 

78 pending: Optional[Cell] = None 

79 # 手動指定を待つコマに重ねる、推測(無ければ前のコマの塗り範囲)。 

80 guide: Optional[QImage] = None 

81 # 自信はないが追跡が推測した範囲と、そのコマの画素。Enter で採用する。 

82 guess: Optional[bucket_fill.FillResult] = None 

83 guess_pixels: Optional[np.ndarray] = None 

84 

85 

86class SkewerFillMixin(CanvasMembers): 

87 _skewer_session: Optional[SkewerFillSession] = None 

88 

89 # ------------------------------------------------------------------ 

90 # 入口 

91 # ------------------------------------------------------------------ 

92 def flood_fill_over_frames(self, p): 

93 """串刺し塗りのクリック。手動指定待ちなら、その指定として扱う。""" 

94 if self._skewer_session is not None: 

95 self._skewer_resume_with_click(p) 

96 return 

97 start_x, start_y = int(p.x()), int(p.y()) 

98 if not self._seed_in_bounds(start_x, start_y): 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true

99 return 

100 window: Any = self.window() 

101 scope_controller = getattr(window, "scope", None) 

102 if scope_controller is None: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true

103 return 

104 scope = scope_controller.current_frame(True, selection_only=False) 

105 cells = frame_scope.resolve_cells(self, scope) 

106 layer_index = int(self.active_layer_index) 

107 key_frame = self.resolve_key_frame(self.current_frame, layer_index) 

108 if key_frame is None or (key_frame, layer_index) not in cells: 108 ↛ 110line 108 didn't jump to line 110 because the condition on line 108 was never true

109 # 空セルには塗らない(新しいセルを作らない)。 

110 self.status_message.emit( 

111 tr("串刺し塗り:このコマには塗れる絵がありません。") 

112 ) 

113 return 

114 pivot_image = self.frames[key_frame].layers[layer_index].image 

115 

116 options = self.bucket_options() 

117 selection_mask = self.selection_mask_bool( 

118 pivot_image.width(), pivot_image.height() 

119 ) 

120 _converted, layer_pixels = self._layer_fill_pixels(pivot_image) 

121 # 判定は参照レイヤーの絵で行う(塗る先は変わらない)。 

122 _reference, pixels = self.bucket_reference_pixels( 

123 options, key_frame, layer_index, _converted, layer_pixels 

124 ) 

125 start_result = bucket_fill.compute_fill_region( 

126 pixels, start_x, start_y, options, selection_mask 

127 ) 

128 if not start_result.ok: 

129 message = self._bucket_failure_message(start_result.reason) 

130 self.status_message.emit( 

131 message or tr("串刺し塗りの対象になるコマがありませんでした。") 

132 ) 

133 return 

134 

135 pivot = cells.index((key_frame, layer_index)) 

136 session = SkewerFillSession( 

137 scope=scope, 

138 reference=bucket_fill.seed_category(pixels, start_x, start_y), 

139 options=options, 

140 selection_mask=selection_mask, 

141 replacement=self.paint_source_color(), 

142 origin_frame=int(self.current_frame), 

143 queues=[cells[pivot + 1:], list(reversed(cells[:pivot]))], 

144 previous=[start_result.mask, start_result.mask], 

145 previous_pixels=[pixels.copy()] * 2, 

146 cells=list(cells), 

147 ) 

148 self._skewer_remember(session, pivot_image, start_result) 

149 self._skewer_session = session 

150 self._skewer_advance() 

151 

152 def skewer_fill_pending(self): 

153 """手動指定を待っているか。""" 

154 return bool( 

155 self._skewer_session is not None 

156 and self._skewer_session.pending is not None 

157 ) 

158 

159 def skip_skewer_fill_frame(self): 

160 """手動指定を待っているコマを塗らずに飛ばし、続きを追いかける。""" 

161 session = self._skewer_session 

162 if session is None or session.pending is None: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true

163 return False 

164 if not self._skewer_session_valid(session): 164 ↛ 165line 164 didn't jump to line 165 because the condition on line 164 was never true

165 self.abort_skewer_fill(tr("絵が変わったため、串刺し塗りを取り消しました。")) 

166 return True 

167 session.skipped += 1 

168 self._skewer_clear_pending(session) 

169 self._skewer_advance() 

170 return True 

171 

172 def skip_skewer_fill_direction(self): 

173 """手動指定を待っているコマと、同じ向きの残りのコマを塗らずに飛ばす(Shift+Esc)。 

174 

175 図形が画面から消えたあとは、どのコマも前に塗れたコマと比べることになり、 

176 1コマずつ飛ばしても止まり続ける。そのときに残りをまとめて飛ばす。 

177 """ 

178 session = self._skewer_session 

179 if session is None or session.pending is None: 

180 return False 

181 if not self._skewer_session_valid(session): 181 ↛ 182line 181 didn't jump to line 182 because the condition on line 181 was never true

182 self.abort_skewer_fill(tr("絵が変わったため、串刺し塗りを取り消しました。")) 

183 return True 

184 queue = session.queues[session.direction] 

185 session.skipped += 1 + len(queue) 

186 queue.clear() 

187 self._skewer_clear_pending(session) 

188 self._skewer_advance() 

189 return True 

190 

191 def abort_skewer_fill(self, message=None): 

192 """途中の串刺し塗りを、何も塗らずに取り消す。""" 

193 if self._skewer_session is None: 

194 return 

195 self._skewer_session = None 

196 self.update() 

197 self.status_message.emit(message or tr("串刺し塗りを取り消しました。")) 

198 

199 # ------------------------------------------------------------------ 

200 # 追跡 

201 # ------------------------------------------------------------------ 

202 def _skewer_advance(self): 

203 """次のコマから追跡を続ける。信用度が低いコマに来たら止まって待つ。""" 

204 session = self._skewer_session 

205 if session is None: 205 ↛ 206line 205 didn't jump to line 206 because the condition on line 205 was never true

206 return 

207 total = sum(len(queue) for queue in session.queues) 

208 dialog = ( 

209 create_counter(self, tr("串刺し塗り"), total, cancellable=True) 

210 if total 

211 else None 

212 ) 

213 done = 0 

214 try: 

215 while session.direction < len(session.queues): 

216 queue = session.queues[session.direction] 

217 while queue: 

218 if dialog is not None and dialog.wasCanceled(): 218 ↛ 219line 218 didn't jump to line 219 because the condition on line 218 was never true

219 self.abort_skewer_fill(tr("串刺し塗りを中止しました。")) 

220 return 

221 frame_index, layer_index = queue[0] 

222 update_counter( 

223 dialog, done, total, 

224 tr("{position}の塗る範囲を探しています").format( 

225 position=frame_position(frame_index) 

226 ), 

227 ) 

228 done += 1 

229 image = self.frames[frame_index].layers[layer_index].image 

230 if id(image) in session.regions: 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true

231 queue.pop(0) 

232 continue 

233 result, pixels = self._skewer_locate( 

234 session, 

235 self._skewer_reference_image( 

236 session, frame_index, layer_index, image 

237 ), 

238 ) 

239 if result is not None and result.ok and ( 

240 result.score is None 

241 or result.score >= TRACK_CONFIDENT_IOU 

242 ): 

243 self._skewer_remember(session, image, result) 

244 session.previous[session.direction] = result.mask 

245 session.previous_pixels[session.direction] = pixels 

246 queue.pop(0) 

247 continue 

248 if result is None: 248 ↛ 250line 248 didn't jump to line 250 because the condition on line 248 was never true

249 # 追跡しない設定(「隣接」OFF)で塗れなかったコマは飛ばす。 

250 session.skipped += 1 

251 queue.pop(0) 

252 continue 

253 # 自信がないので、このコマはユーザーに指定してもらう。 

254 # 推測があれば見せて、Enter だけで採用できるようにする。 

255 queue.pop(0) 

256 if result.ok: 

257 session.guess = result 

258 session.guess_pixels = pixels 

259 self._skewer_wait_for(session, (frame_index, layer_index)) 

260 return 

261 session.direction += 1 

262 finally: 

263 close_counter(dialog) 

264 self._skewer_apply() 

265 

266 def _skewer_reference_image(self, session, frame_index, layer_index, image): 

267 """``frame_index`` のコマで判定に使う絵。参照レイヤーの設定に従う。""" 

268 return self._bucket_reference_image( 

269 session.options, frame_index, layer_index, image 

270 ) 

271 

272 def _skewer_locate(self, session, image): 

273 """追跡で塗る範囲を探す。``(結果, そのコマの画素)`` を返す。 

274 

275 結果が ``None`` なのは「追跡しない設定で塗れなかった」とき。 

276 """ 

277 _converted, view = self._layer_fill_pixels(image) 

278 # 次のコマとのずれの推定に使うので、画像に縛られない複製にしておく。 

279 pixels = view.copy() 

280 previous = session.previous[session.direction] 

281 if pixels.shape[:2] != previous.shape: 281 ↛ 282line 281 didn't jump to line 282 because the condition on line 281 was never true

282 return None, pixels 

283 if session.options.adjacent: 283 ↛ 290line 283 didn't jump to line 290 because the condition on line 283 was always true

284 return bucket_fill.track_fill_region( 

285 pixels, previous, session.reference, session.options, 

286 session.selection_mask, 

287 previous_pixels=session.previous_pixels[session.direction], 

288 ), pixels 

289 # 「隣接」OFF はレイヤー内の同色を一括で塗るので、位置を追う必要がない。 

290 reference = session.reference 

291 if reference is None: 

292 target = bucket_fill.background_mask(pixels) 

293 else: 

294 target = np.all(pixels[:, :, :3] == reference, axis=2) 

295 target &= pixels[:, :, 3] > 0 

296 points = np.argwhere(target) 

297 if not points.size: 

298 return None, pixels 

299 start_y, start_x = (int(value) for value in points[0]) 

300 result = bucket_fill.compute_fill_region( 

301 pixels, start_x, start_y, session.options, session.selection_mask 

302 ) 

303 return (result if result.ok else None), pixels 

304 

305 def _skewer_remember(self, session, image, result): 

306 assert result.bounds is not None # ok なら必ず外接矩形がある 

307 x0, y0, x1, y1 = result.bounds 

308 session.regions[id(image)] = ( 

309 result.bounds, 

310 result.mask[y0:y1, x0:x1].copy(), 

311 tuple( 

312 (rgb, island[y0:y1, x0:x1].copy()) for rgb, island in result.islands 

313 ), 

314 ) 

315 session.images[id(image)] = (image, int(image.cacheKey())) 

316 

317 def _skewer_wait_for(self, session, cell): 

318 frame_index, layer_index = cell 

319 session.pending = cell 

320 if session.guess is not None: 

321 session.guide = self._skewer_guide_image(session.guess.mask, _GUESS_RGBA) 

322 else: 

323 session.guide = self._skewer_guide_image( 

324 session.previous[session.direction] 

325 ) 

326 self.current_frame = int(frame_index) 

327 self.active_layer_index = int(layer_index) 

328 self.selectionChanged.emit() 

329 self.update() 

330 self.status_message.emit(self._skewer_pending_message(session)) 

331 

332 @staticmethod 

333 def _skewer_pending_message(session): 

334 assert session.pending is not None 

335 if session.guess is not None: 

336 return tr( 

337 "串刺し塗り:{position}は自信がありません。推測した範囲を青で" 

338 "表示しています(Enter:この範囲で塗る/クリック:塗る場所を指定" 

339 "/Esc:このコマを飛ばす/Shift+Esc:この向きの残りを飛ばす)。" 

340 ).format(position=frame_position(session.pending[0])) 

341 return tr( 

342 "串刺し塗り:{position}は追いかけきれませんでした。" 

343 "塗る場所をクリックしてください(Esc:このコマを飛ばす" 

344 "/Shift+Esc:この向きの残りを飛ばす)。" 

345 ).format(position=frame_position(session.pending[0])) 

346 

347 def skewer_fill_has_guess(self): 

348 session = self._skewer_session 

349 return bool(session is not None and session.pending is not None and session.guess) 

350 

351 def accept_skewer_guess(self): 

352 """手動指定を待っているコマで、追跡の推測をそのまま採用する(Enter)。""" 

353 session = self._skewer_session 

354 if session is None or session.pending is None or session.guess is None: 354 ↛ 355line 354 didn't jump to line 355 because the condition on line 354 was never true

355 return False 

356 if not self._skewer_session_valid(session): 356 ↛ 357line 356 didn't jump to line 357 because the condition on line 356 was never true

357 self.abort_skewer_fill(tr("絵が変わったため、串刺し塗りを取り消しました。")) 

358 return True 

359 frame_index, layer_index = session.pending 

360 image = self.frames[frame_index].layers[layer_index].image 

361 self._skewer_remember(session, image, session.guess) 

362 session.previous[session.direction] = session.guess.mask 

363 session.previous_pixels[session.direction] = session.guess_pixels 

364 self._skewer_clear_pending(session) 

365 self._skewer_advance() 

366 return True 

367 

368 @staticmethod 

369 def _skewer_clear_pending(session): 

370 session.pending = None 

371 session.guide = None 

372 session.guess = None 

373 session.guess_pixels = None 

374 

375 def _skewer_resume_with_click(self, p): 

376 session = self._skewer_session 

377 assert session is not None 

378 if not self._skewer_session_valid(session): 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true

379 self.abort_skewer_fill(tr("絵が変わったため、串刺し塗りを取り消しました。")) 

380 return 

381 if session.pending is None: 381 ↛ 382line 381 didn't jump to line 382 because the condition on line 381 was never true

382 return 

383 frame_index, layer_index = session.pending 

384 pending_image = self.frames[frame_index].layers[layer_index].image 

385 key_frame = self.resolve_key_frame(self.current_frame, layer_index) 

386 if ( 386 ↛ 391line 386 didn't jump to line 391 because the condition on line 386 was never true

387 key_frame is None 

388 or self.frames[key_frame].layers[layer_index].image is not pending_image 

389 ): 

390 # 別のコマを見ているときは、待っているコマへ戻す。 

391 self._skewer_wait_for(session, session.pending) 

392 return 

393 start_x, start_y = int(p.x()), int(p.y()) 

394 if not self._seed_in_bounds(start_x, start_y, pending_image): 394 ↛ 395line 394 didn't jump to line 395 because the condition on line 394 was never true

395 return 

396 _converted, view = self._layer_fill_pixels( 

397 self._skewer_reference_image( 

398 session, frame_index, layer_index, pending_image 

399 ) 

400 ) 

401 pixels = view.copy() 

402 result = bucket_fill.compute_fill_region( 

403 pixels, start_x, start_y, session.options, session.selection_mask 

404 ) 

405 if not result.ok: 405 ↛ 406line 405 didn't jump to line 406 because the condition on line 405 was never true

406 message = self._bucket_failure_message(result.reason) 

407 if message: 

408 self.status_message.emit(message) 

409 return 

410 self._skewer_remember(session, pending_image, result) 

411 session.previous[session.direction] = result.mask 

412 session.previous_pixels[session.direction] = pixels 

413 self._skewer_clear_pending(session) 

414 self._skewer_advance() 

415 

416 def _skewer_session_valid(self, session): 

417 """待っている間にタイムラインや絵が変わっていないか。""" 

418 try: 

419 cells = frame_scope.resolve_cells(self, session.scope) 

420 except (IndexError, AttributeError): 

421 return False 

422 if cells != session.cells: 422 ↛ 423line 422 didn't jump to line 423 because the condition on line 422 was never true

423 return False 

424 for image, cache_key in session.images.values(): 

425 if int(image.cacheKey()) != cache_key: 425 ↛ 426line 425 didn't jump to line 426 because the condition on line 425 was never true

426 return False 

427 live = { 

428 id(self.frames[frame_index].layers[layer_index].image) 

429 for frame_index, layer_index in cells 

430 } 

431 return all(image_id in live for image_id in session.images) 

432 

433 # ------------------------------------------------------------------ 

434 # 書き込み 

435 # ------------------------------------------------------------------ 

436 def _skewer_apply(self): 

437 session = self._skewer_session 

438 self._skewer_session = None 

439 if session is None: 439 ↛ 440line 439 didn't jump to line 440 because the condition on line 439 was never true

440 return 

441 window: Any = self.window() 

442 scope_controller = getattr(window, "scope", None) 

443 if scope_controller is None or not self._skewer_session_valid(session): 443 ↛ 444line 443 didn't jump to line 444 because the condition on line 443 was never true

444 self.update() 

445 self.status_message.emit( 

446 tr("絵が変わったため、串刺し塗りを取り消しました。") 

447 ) 

448 return 

449 written_colors = set() 

450 

451 def op(context): 

452 planned = session.regions.get(id(context.layer.image)) 

453 if planned is None: 

454 return None 

455 bounds, mask, islands = planned 

456 image, pixels = self._layer_fill_pixels(context.layer.image) 

457 x0, y0, x1, y1 = bounds 

458 patch = pixels[y0:y1, x0:x1] 

459 written_colors.update(self._fill_patch(patch, mask, session.replacement)) 

460 for rgb, island in islands: 460 ↛ 461line 460 didn't jump to line 461 because the loop on line 460 never started

461 written_colors.update(self._fill_patch(patch, island, QColor(*rgb))) 

462 return image.convertToFormat(QImage.Format.Format_ARGB32_Premultiplied) 

463 

464 if 0 <= session.origin_frame < len(self.frames): 464 ↛ 467line 464 didn't jump to line 467 because the condition on line 464 was always true

465 self.current_frame = session.origin_frame 

466 self.selectionChanged.emit() 

467 result = scope_controller.run_over( 

468 session.scope, 

469 op, 

470 label=tr("串刺し塗り"), 

471 cancellable=True, 

472 ) 

473 self.update() 

474 if result.cancelled: 474 ↛ 475line 474 didn't jump to line 475 because the condition on line 474 was never true

475 self.status_message.emit(tr("串刺し塗りを中止しました。")) 

476 return 

477 if not result.changed: 477 ↛ 478line 477 didn't jump to line 478 because the condition on line 477 was never true

478 self.status_message.emit( 

479 tr("串刺し塗りの対象になるコマがありませんでした。") 

480 ) 

481 return 

482 if written_colors: 

483 self._emit_actual_paint_colors(tuple(sorted(written_colors))) 

484 if session.skipped: 

485 self.status_message.emit( 

486 tr("串刺し塗り:{filled} コマを塗り、{skipped} コマは対象外でした。").format( 

487 filled=result.changed_cells, skipped=session.skipped 

488 ) 

489 ) 

490 else: 

491 self.status_message.emit( 

492 tr("串刺し塗り:{filled} コマを塗りました。").format( 

493 filled=result.changed_cells 

494 ) 

495 ) 

496 

497 # ------------------------------------------------------------------ 

498 # 表示 

499 # ------------------------------------------------------------------ 

500 @staticmethod 

501 def _skewer_guide_image(mask, color=_GUIDE_RGBA): 

502 rgba = np.zeros(mask.shape + (4,), dtype=np.uint8) 

503 rgba[mask] = color 

504 return imaging.rgba_array_to_qimage(rgba) 

505 

506 def paint_skewer_guide(self, painter: QPainter, work_rect): 

507 """手動指定を待つコマに、前のコマの塗り範囲を重ねる(キャンバス座標系)。""" 

508 session = self._skewer_session 

509 if session is None or session.pending is None or session.guide is None: 509 ↛ 511line 509 didn't jump to line 511 because the condition on line 509 was always true

510 return 

511 painter.drawImage(work_rect, session.guide) 

512 

513 def paint_skewer_banner(self, painter: QPainter): 

514 """手動指定を待っている間、何をすればよいかをキャンバス上部に出す。""" 

515 session = self._skewer_session 

516 if session is None or session.pending is None: 516 ↛ 518line 516 didn't jump to line 518 because the condition on line 516 was always true

517 return 

518 text = self._skewer_pending_message(session) 

519 metrics = painter.fontMetrics() 

520 margin = 8 

521 width = min(self.width() - margin * 2, metrics.horizontalAdvance(text) + 20) 

522 rect = QRectF(margin, margin, max(40, width), metrics.height() + 12) 

523 painter.save() 

524 painter.setPen(Qt.PenStyle.NoPen) 

525 painter.setBrush(QColor(20, 20, 20, 210)) 

526 painter.drawRoundedRect(rect, 6, 6) 

527 painter.setPen(QColor(255, 255, 255)) 

528 painter.drawText( 

529 rect.adjusted(10, 0, -10, 0), 

530 Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, 

531 metrics.elidedText(text, Qt.TextElideMode.ElideRight, int(rect.width()) - 20), 

532 ) 

533 painter.restore() 

534 

535 

536__all__ = ["SkewerFillMixin", "SkewerFillSession", "TRACK_CONFIDENT_IOU"]