Coverage for paintmaskanimator/auto_motion.py: 93%
83 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
1"""セル間で共有する動きの候補と、全探索を省略できる場合の厳密な上界検査。"""
2from collections import OrderedDict
4import numpy as np
7class LocalShift(tuple):
8 gap: float
10 def __new__(cls, dy, dx, gap):
11 result = super().__new__(cls, (int(dy), int(dx)))
12 result.gap = float(gap)
13 return result
16class MotionMemory:
17 def __init__(self, capacity=64):
18 self.fields = OrderedDict()
19 self.samples = OrderedDict()
20 self.capacity = capacity
22 def register(self, key, flow, x0, y0):
23 self.fields[key] = flow, x0, y0
24 self.fields.move_to_end(key)
25 while len(self.fields) > self.capacity:
26 self.fields.popitem(last=False)
28 def learn(self, key, bounds, result):
29 shift, score = result
30 if score < 0.95:
31 return
32 x0, y0, x1, y1 = bounds
33 grid = ((x0 + x1) // 256, (y0 + y1) // 256)
34 cells = self.samples.setdefault(key, {})
35 cells[grid] = tuple(shift)
36 if len(cells) > 512: 36 ↛ 37line 36 didn't jump to line 37 because the condition on line 36 was never true
37 cells.pop(next(iter(cells)))
38 self.samples.move_to_end(key)
39 while len(self.samples) > self.capacity:
40 self.samples.popitem(last=False)
42 def predict(self, key, bounds):
43 x0, y0, x1, y1 = bounds
44 x, y = (x0 + x1) // 2, (y0 + y1) // 2
45 if key in self.fields:
46 flow, ox, oy = self.fields[key]
47 if float(flow.score_at(y - oy, x - ox)) >= 0.8:
48 dy, dx = flow.at(y - oy, x - ox)
49 return int(np.rint(dy)), int(np.rint(dx))
50 cells = self.samples.get(key)
51 if cells:
52 grid = (x // 128, y // 128)
53 nearby = min(cells, key=lambda at: abs(at[0] - grid[0]) + abs(at[1] - grid[1]))
54 if abs(nearby[0] - grid[0]) + abs(nearby[1] - grid[1]) <= 2:
55 return cells[nearby]
56 return None
59def certify(problem, hint):
60 """未検査候補の理論上限を超えた場合だけ、FFTと同じ最良位置を返す。"""
61 from . import auto_paint as ap
62 if hint is None: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 return None
64 before, after = problem['before'], problem['after']
65 step, total = problem['step'], problem['total']
66 if not hint or total < 4 or not np.all((before == 0) | (before == 1)):
67 return None
68 ys, xs = np.nonzero(before)
69 dys, dxs = problem['dys'], problem['dxs']
70 dy, dx = (int(np.rint(value / step)) for value in hint)
71 if dy not in dys or dx not in dxs: 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 return None
73 rows, cols = problem['size']
75 def overlaps(shifts):
76 shifts = np.asarray(shifts, dtype=np.int64)
77 y = (ys[None] + shifts[:, 0, None]) % rows
78 x = (xs[None] + shifts[:, 1, None]) % cols
79 valid = (y < after.shape[0]) & (x < after.shape[1])
80 values = after[np.minimum(y, after.shape[0] - 1), np.minimum(x, after.shape[1] - 1)]
81 return np.where(valid, values, 0).sum(axis=1, dtype=np.float64) / total
83 penalty = ap.MOTION_PENALTY * step
84 score = float(overlaps([(dy, dx)])[0]) - penalty * (abs(dy) + abs(dx))
85 # 大きな証明表が必要なら、従来のFFTを使う方が速い。
86 radius = int(np.floor((float(after.max()) - score + 2e-5) / penalty))
87 if radius < 0 or radius > 8:
88 return None
89 shifts = [(int(y), int(x)) for y in dys for x in dxs if abs(y) + abs(x) <= radius]
90 if not shifts or len(shifts) * len(ys) > 32768:
91 return None
92 near = overlaps(shifts)
93 scores = near - penalty * np.array([abs(y) + abs(x) for y, x in shifts])
94 best = int(np.argmax(scores))
95 runner = np.partition(scores, -2)[-2] if len(scores) > 1 else -np.inf
96 unseen = float(after.max()) - penalty * (radius + 1)
97 if scores[best] - max(runner, unseen) <= 2e-5: 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true
98 return None
99 return LocalShift(shifts[best][0] * step, shifts[best][1] * step,
100 scores[best] - max(runner, unseen)), float(near[best])