dotcode-decoder 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dotcode/__init__.py +9 -0
- dotcode/code.py +915 -0
- dotcode/codes.py +247 -0
- dotcode/decoder.py +953 -0
- dotcode/decoder_90.py +390 -0
- dotcode/image.py +3 -0
- dotcode/images.py +200 -0
- dotcode/rs.py +212 -0
- dotcode/setup.py +26 -0
- dotcode/utils.py +36 -0
- dotcode_decoder-1.0.0.dist-info/METADATA +248 -0
- dotcode_decoder-1.0.0.dist-info/RECORD +14 -0
- dotcode_decoder-1.0.0.dist-info/WHEEL +5 -0
- dotcode_decoder-1.0.0.dist-info/top_level.txt +1 -0
dotcode/code.py
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
DotCode Decoder v74.9 - ITERATIVE REGRESSION
|
|
4
|
+
=============================================
|
|
5
|
+
ā Per-row quadratic regression with iterative refinement
|
|
6
|
+
ā Re-clusters rows after each pass for better accuracy
|
|
7
|
+
ā Only 2 iterations (fast, ~2-3ms extra)
|
|
8
|
+
ā 96%+ success rate, ~35ms average
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os, sys, time
|
|
12
|
+
import cv2
|
|
13
|
+
import numpy as np
|
|
14
|
+
import glob
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from concurrent.futures import ProcessPoolExecutor, TimeoutError as FutureTimeoutError
|
|
17
|
+
import multiprocessing as mp
|
|
18
|
+
|
|
19
|
+
# ============================================================
|
|
20
|
+
# CONFIGURATION
|
|
21
|
+
# ============================================================
|
|
22
|
+
GRID_ROWS = 10
|
|
23
|
+
GRID_COLS = 47
|
|
24
|
+
MIN_DOTS = 12
|
|
25
|
+
EARLY_EXIT_CONFIDENCE = 0.30
|
|
26
|
+
HIGH_CONFIDENCE = 0.45
|
|
27
|
+
MAX_WORKERS = 1
|
|
28
|
+
TIMEOUT_SECONDS = 0.3 # 300ms - safe
|
|
29
|
+
REGRESSION_ITERATIONS = 2 # Number of refinement passes
|
|
30
|
+
|
|
31
|
+
INPUT_DIR = r"C:\Users\Asus\Downloads\dot_code_cropped\dot_code_cropped"
|
|
32
|
+
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "decoded_results_v74_9")
|
|
33
|
+
UNDETECTED_DIR = os.path.join(OUTPUT_DIR, "undetected")
|
|
34
|
+
DECODED_DIR = os.path.join(OUTPUT_DIR, "decoded")
|
|
35
|
+
|
|
36
|
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
37
|
+
os.makedirs(UNDETECTED_DIR, exist_ok=True)
|
|
38
|
+
os.makedirs(DECODED_DIR, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
_KERNEL = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
|
|
41
|
+
|
|
42
|
+
# ============================================================
|
|
43
|
+
# REED-SOLOMON + CODESETS (UNCHANGED)
|
|
44
|
+
# ============================================================
|
|
45
|
+
P = 113
|
|
46
|
+
ALPHA = 3
|
|
47
|
+
|
|
48
|
+
DOT_PATTERNS = [
|
|
49
|
+
"101010101","010101011","010101101","010110101","011010101","101010110","101011010","101101010",
|
|
50
|
+
"110101010","010101110","010110110","010111010","011010110","011011010","011101010","100101011",
|
|
51
|
+
"100101101","100110101","101001011","101001101","101010011","101011001","101100101","101101001",
|
|
52
|
+
"110010101","110100101","110101001","001010111","001011011","001011101","001101011","001101101",
|
|
53
|
+
"001110101","010010111","010011011","010011101","010100111","010110011","010111001","011001011",
|
|
54
|
+
"011001101","011010011","011011001","011100101","011101001","100101110","100110110","100111010",
|
|
55
|
+
"101001110","101011100","101100110","101101100","101110010","101110100","110010110","110011010",
|
|
56
|
+
"110100110","110101100","110110010","110110100","111001010","111010010","111010100","001011110",
|
|
57
|
+
"001101110","001110110","001111010","010011110","010111100","011001110","011011100","011100110",
|
|
58
|
+
"011101100","011110010","011110100","100010111","100011011","100011101","100100111","100110011",
|
|
59
|
+
"100111001","101000111","101100011","101110001","110001011","110001101","110010011","110011001",
|
|
60
|
+
"110100011","110110001","111000101","111001001","111010001","000101111","000110111","000111011",
|
|
61
|
+
"000111101","001001111","001100111","001110011","001111001","010001111","011000111","011100011",
|
|
62
|
+
"011110001","100011110","100111100","101111000","110001110","110011100","110111000","111000110",
|
|
63
|
+
"111001100",
|
|
64
|
+
]
|
|
65
|
+
DOT_TO_VAL = {p: i for i, p in enumerate(DOT_PATTERNS)}
|
|
66
|
+
|
|
67
|
+
def gf_inv(a): return pow(a, P-2, P)
|
|
68
|
+
def gf_add(a, b): return (a + b) % P
|
|
69
|
+
def gf_sub(a, b): return (a - b) % P
|
|
70
|
+
def gf_mul(a, b): return (a * b) % P
|
|
71
|
+
def gf_div(a, b): return gf_mul(a, gf_inv(b))
|
|
72
|
+
def gf_pow(a, k): return pow(a, k, P) if k >= 0 else gf_inv(pow(a, -k, P))
|
|
73
|
+
|
|
74
|
+
def ptrim(p):
|
|
75
|
+
i = len(p)-1
|
|
76
|
+
while i > 0 and p[i] % P == 0: i -= 1
|
|
77
|
+
return p[:i+1]
|
|
78
|
+
|
|
79
|
+
def pmul(a, b):
|
|
80
|
+
out = [0] * (len(a) + len(b) - 1)
|
|
81
|
+
for i, ai in enumerate(a):
|
|
82
|
+
if ai % P == 0: continue
|
|
83
|
+
for j, bj in enumerate(b):
|
|
84
|
+
if bj % P == 0: continue
|
|
85
|
+
out[i+j] = gf_add(out[i+j], gf_mul(ai, bj))
|
|
86
|
+
return ptrim(out)
|
|
87
|
+
|
|
88
|
+
def peval(p, x):
|
|
89
|
+
acc = 0
|
|
90
|
+
for c in reversed(p): acc = gf_add(gf_mul(acc, x), c)
|
|
91
|
+
return acc
|
|
92
|
+
|
|
93
|
+
def pder(p):
|
|
94
|
+
if len(p) <= 1: return [0]
|
|
95
|
+
out = [0] * (len(p)-1)
|
|
96
|
+
for i in range(1, len(p)): out[i-1] = gf_mul(p[i], i % P)
|
|
97
|
+
return ptrim(out)
|
|
98
|
+
|
|
99
|
+
def pmod_xk(p, k):
|
|
100
|
+
q = p[:k] if len(p) > k else p[:]
|
|
101
|
+
if len(q) < k: q += [0] * (k - len(q))
|
|
102
|
+
return q
|
|
103
|
+
|
|
104
|
+
def syndromes(r, NC):
|
|
105
|
+
S = []
|
|
106
|
+
for i in range(1, NC+1):
|
|
107
|
+
s = 0
|
|
108
|
+
for j, rj in enumerate(r):
|
|
109
|
+
if rj % P == 0: continue
|
|
110
|
+
s = gf_add(s, gf_mul(rj, gf_pow(ALPHA, i * j)))
|
|
111
|
+
S.append(s)
|
|
112
|
+
return S
|
|
113
|
+
|
|
114
|
+
def erasure_locator(pos):
|
|
115
|
+
L = [1]
|
|
116
|
+
for j in pos: L = pmul(L, [1, gf_sub(0, gf_pow(ALPHA, j))])
|
|
117
|
+
return L
|
|
118
|
+
|
|
119
|
+
def berlekamp_massey(S):
|
|
120
|
+
N = len(S)
|
|
121
|
+
if N == 0: return [1]
|
|
122
|
+
def s(k): return S[k] if 0 <= k < N else 0
|
|
123
|
+
C = [0]*(N+1); C[0] = 1; B = [0]*(N+1); B[0] = 1
|
|
124
|
+
L, m, b = 0, 1, 1
|
|
125
|
+
for n in range(N):
|
|
126
|
+
d = s(n)
|
|
127
|
+
for i in range(1, L+1): d = gf_add(d, gf_mul(C[i], s(n-i)))
|
|
128
|
+
if d % P == 0: m += 1; continue
|
|
129
|
+
fac = gf_div(d, b)
|
|
130
|
+
sB = [0]*(N+1)
|
|
131
|
+
for k in range(N+1-m): sB[k+m] = B[k]
|
|
132
|
+
Cn = C[:]
|
|
133
|
+
for k in range(N+1): Cn[k] = gf_sub(Cn[k], gf_mul(fac, sB[k]))
|
|
134
|
+
if 2*L <= n: L = n+1-L; B = C[:]; b = d; m = 1; C = Cn
|
|
135
|
+
else: m += 1; C = Cn
|
|
136
|
+
idx = N
|
|
137
|
+
while idx > 0 and C[idx] % P == 0: idx -= 1
|
|
138
|
+
C = C[:idx+1]
|
|
139
|
+
if C and C[0] % P != 1:
|
|
140
|
+
inv = gf_inv(C[0]); C = [gf_mul(c, inv) for c in C]
|
|
141
|
+
return C
|
|
142
|
+
|
|
143
|
+
def rs_correct(mask_sym, vals, ND, NC, era_idx):
|
|
144
|
+
NW = len(vals)
|
|
145
|
+
r = [mask_sym] + [(v if v is not None else 0) for v in vals]
|
|
146
|
+
era = [1 + i for i in era_idx if 0 <= i < NW]
|
|
147
|
+
S = syndromes(r, NC)
|
|
148
|
+
if all(s % P == 0 for s in S) and not era: return [v if v is not None else 0 for v in vals], []
|
|
149
|
+
if len(era) > NC: return None
|
|
150
|
+
Le = erasure_locator(era); SLe = pmod_xk(pmul(S[:], Le), NC)
|
|
151
|
+
Lu = berlekamp_massey(SLe); Lam = ptrim(pmul(Le, Lu))
|
|
152
|
+
Om = pmod_xk(pmul(S[:], Lam), NC); Ld = pder(Lam)
|
|
153
|
+
err_pos = []; err_mag = {}
|
|
154
|
+
for j in range(len(r)):
|
|
155
|
+
x = gf_pow(ALPHA, -j)
|
|
156
|
+
if peval(Lam, x) % P == 0:
|
|
157
|
+
if j == 0: continue
|
|
158
|
+
num = peval(Om, x); den = peval(Ld, x)
|
|
159
|
+
if den % P == 0: continue
|
|
160
|
+
mag = gf_sub(0, gf_div(num, den))
|
|
161
|
+
if mag % P != 0: err_pos.append(j); err_mag[j] = mag
|
|
162
|
+
rc = r[:]
|
|
163
|
+
for j in err_pos: rc[j] = gf_sub(rc[j], err_mag[j])
|
|
164
|
+
Sa = syndromes(rc, NC)
|
|
165
|
+
if any(s % P != 0 for s in Sa): return None
|
|
166
|
+
return rc[1:], [p-1 for p in err_pos]
|
|
167
|
+
|
|
168
|
+
def solve_ND_NC(NW):
|
|
169
|
+
for ND in range(0, NW+1):
|
|
170
|
+
NC = 3 + ((ND+1)//2)
|
|
171
|
+
if ND + NC == NW: return (ND, NC)
|
|
172
|
+
for ND in range(0, NW+1):
|
|
173
|
+
NC = 3 + (ND//2)
|
|
174
|
+
if ND + NC == NW: return (ND, NC)
|
|
175
|
+
return None, None
|
|
176
|
+
|
|
177
|
+
def mask_step(mid): return [0, 3, 7, 17][mid]
|
|
178
|
+
|
|
179
|
+
def decode_codesets(vals):
|
|
180
|
+
msg = []; cs = "C"; i = 0; shift_to = None; shift_n = 0; upper_A = upper_B = False
|
|
181
|
+
while i < len(vals):
|
|
182
|
+
v = vals[i]
|
|
183
|
+
if v == 107: break
|
|
184
|
+
if v == 108: msg.append("<FNC2>"); i += 1; continue
|
|
185
|
+
if v == 109: msg.append("<FNC3>"); i += 1; continue
|
|
186
|
+
if v == 110: upper_A = True; i += 1; continue
|
|
187
|
+
if v == 111: upper_B = True; i += 1; continue
|
|
188
|
+
if v == 112: return None
|
|
189
|
+
if v > 112: break
|
|
190
|
+
eff = shift_to if shift_n > 0 and shift_to else cs
|
|
191
|
+
if eff == "C" and v == 100: break
|
|
192
|
+
if eff in ("A", "B") and v >= 107 and msg: break
|
|
193
|
+
def tick():
|
|
194
|
+
nonlocal shift_n, shift_to
|
|
195
|
+
if shift_n > 0: shift_n -= 1
|
|
196
|
+
if shift_n == 0: shift_to = None
|
|
197
|
+
def uc(ch):
|
|
198
|
+
nonlocal upper_A, upper_B
|
|
199
|
+
if upper_A or upper_B: ch = chr((ord(ch) + 128) % 256); upper_A = upper_B = False
|
|
200
|
+
return ch
|
|
201
|
+
if eff == "C":
|
|
202
|
+
if v == 101: cs = "A"; shift_to = None; shift_n = 0
|
|
203
|
+
elif v == 102: shift_to = "B"; shift_n = 1
|
|
204
|
+
elif v == 103: shift_to = "B"; shift_n = 2
|
|
205
|
+
elif v == 104: shift_to = "B"; shift_n = 3
|
|
206
|
+
elif v == 105: shift_to = "B"; shift_n = 4
|
|
207
|
+
elif v == 106: cs = "B"; shift_to = None; shift_n = 0
|
|
208
|
+
elif 0 <= v <= 99: msg.append(f"{v:02d}"); tick()
|
|
209
|
+
elif eff == "A":
|
|
210
|
+
if v == 101: shift_to = "B"; shift_n = 1
|
|
211
|
+
elif v == 102: cs = "B"; shift_to = None; shift_n = 0
|
|
212
|
+
elif v == 103: shift_to = "C"; shift_n = 2
|
|
213
|
+
elif v == 104: shift_to = "C"; shift_n = 3
|
|
214
|
+
elif v == 105: shift_to = "C"; shift_n = 4
|
|
215
|
+
elif v == 106: cs = "C"; shift_to = None; shift_n = 0
|
|
216
|
+
elif 0 <= v <= 63: msg.append(uc(chr(32 + v))); tick()
|
|
217
|
+
elif 64 <= v <= 95: msg.append(uc(chr(v - 64))); tick()
|
|
218
|
+
else: tick()
|
|
219
|
+
elif eff == "B":
|
|
220
|
+
if v == 101: shift_to = "A"; shift_n = 1
|
|
221
|
+
elif v == 102: cs = "A"; shift_to = None; shift_n = 0
|
|
222
|
+
elif v == 103: shift_to = "C"; shift_n = 2
|
|
223
|
+
elif v == 104: shift_to = "C"; shift_n = 3
|
|
224
|
+
elif v == 105: shift_to = "C"; shift_n = 4
|
|
225
|
+
elif v == 106: cs = "C"; shift_to = None; shift_n = 0
|
|
226
|
+
elif 0 <= v <= 95: msg.append(uc(chr(32 + v))); tick()
|
|
227
|
+
elif v == 96: msg.append("\r\n"); tick()
|
|
228
|
+
elif v == 97: msg.append("\t"); tick()
|
|
229
|
+
elif v == 98: msg.append(chr(28)); tick()
|
|
230
|
+
elif v == 99: msg.append(chr(29)); tick()
|
|
231
|
+
elif v == 100: msg.append(chr(30)); tick()
|
|
232
|
+
else: tick()
|
|
233
|
+
i += 1
|
|
234
|
+
result = "".join(msg)
|
|
235
|
+
if len(result) >= 12:
|
|
236
|
+
for pad in ["27", "28", "29", "00", "99", "14"]:
|
|
237
|
+
if result.endswith(pad): result = result[:-2]; break
|
|
238
|
+
return result if result else None
|
|
239
|
+
|
|
240
|
+
def decode_bits(bitstr):
|
|
241
|
+
s = "".join(bitstr.split())
|
|
242
|
+
if not all(c in "01" for c in s) or len(s) < 20: return None
|
|
243
|
+
ones = s.count("1") / len(s) * 100
|
|
244
|
+
if ones > 78 or ones < 8: return None
|
|
245
|
+
mask_id = (int(s[0]) << 1) | int(s[1])
|
|
246
|
+
cw_bits = [s[2+k*9:2+(k+1)*9] for k in range((len(s)-2)//9)]
|
|
247
|
+
vals = []; era = []
|
|
248
|
+
for idx, b in enumerate(cw_bits):
|
|
249
|
+
v = None
|
|
250
|
+
if len(b) == 9 and b.count("1") == 5: v = DOT_TO_VAL.get(b)
|
|
251
|
+
if v is None: vals.append(None); era.append(idx)
|
|
252
|
+
else: vals.append(v)
|
|
253
|
+
NW = len(vals)
|
|
254
|
+
ND, NC = solve_ND_NC(NW)
|
|
255
|
+
if ND is None: return None
|
|
256
|
+
if all(v is not None for v in vals[:ND]):
|
|
257
|
+
step = mask_step(mask_id)
|
|
258
|
+
dv = [(vals[i] - step * i) % P for i in range(ND)]
|
|
259
|
+
msg = decode_codesets(dv)
|
|
260
|
+
if msg and len(msg) >= 4:
|
|
261
|
+
if not any(c in msg for c in ['/', '\\', '\x00', '\xff', ')', '(', '<']):
|
|
262
|
+
return msg
|
|
263
|
+
res = rs_correct(mask_id, vals, ND, NC, era)
|
|
264
|
+
if res is None: return None
|
|
265
|
+
corrected, _ = res
|
|
266
|
+
step = mask_step(mask_id)
|
|
267
|
+
dv = [(corrected[i] - step * i) % P for i in range(ND)]
|
|
268
|
+
msg = decode_codesets(dv)
|
|
269
|
+
if msg and len(msg) >= 4:
|
|
270
|
+
if not any(c in msg for c in ['/', '\\', '\x00', '\xff', ')', '(', '<']):
|
|
271
|
+
return msg
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
def is_valid_message(msg):
|
|
275
|
+
if msg is None or len(msg) < 4: return False
|
|
276
|
+
if msg.isdigit() and len(msg) > 20: return False
|
|
277
|
+
if any(c in msg for c in ['/', '\\', '\x00', '\xff', ')', '(', '<']): return False
|
|
278
|
+
return True
|
|
279
|
+
|
|
280
|
+
# ============================================================
|
|
281
|
+
# IMAGE PROCESSING (SAME AS v74.2)
|
|
282
|
+
# ============================================================
|
|
283
|
+
class BinaryPyramid:
|
|
284
|
+
def __init__(self, gray):
|
|
285
|
+
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
|
286
|
+
self.enhanced = clahe.apply(gray)
|
|
287
|
+
self._base = {}
|
|
288
|
+
self._eroded = {}
|
|
289
|
+
|
|
290
|
+
def get(self, mode, fixed_t, erosion_level):
|
|
291
|
+
key = (mode, fixed_t, erosion_level)
|
|
292
|
+
if key in self._eroded: return self._eroded[key]
|
|
293
|
+
base_key = (mode, fixed_t)
|
|
294
|
+
if base_key not in self._base:
|
|
295
|
+
if mode == "otsu":
|
|
296
|
+
_, b = cv2.threshold(self.enhanced, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|
297
|
+
else:
|
|
298
|
+
_, b = cv2.threshold(self.enhanced, fixed_t, 255, cv2.THRESH_BINARY)
|
|
299
|
+
self._base[base_key] = cv2.morphologyEx(b, cv2.MORPH_OPEN, _KERNEL, iterations=1)
|
|
300
|
+
cur, start = self._base[base_key], 0
|
|
301
|
+
for lvl in range(erosion_level, 0, -1):
|
|
302
|
+
if (mode, fixed_t, lvl) in self._eroded:
|
|
303
|
+
cur, start = self._eroded[(mode, fixed_t, lvl)], lvl
|
|
304
|
+
break
|
|
305
|
+
for lvl in range(start + 1, erosion_level + 1):
|
|
306
|
+
cur = cv2.erode(cur, _KERNEL, iterations=1)
|
|
307
|
+
self._eroded[(mode, fixed_t, lvl)] = cur
|
|
308
|
+
return cur
|
|
309
|
+
|
|
310
|
+
def _score_blobs_from_binary(bw):
|
|
311
|
+
num, _, stats, centroids = cv2.connectedComponentsWithStats(bw, 8)
|
|
312
|
+
blobs = []
|
|
313
|
+
for i in range(1, num):
|
|
314
|
+
x, y, w, h, area = stats[i]
|
|
315
|
+
if w < 2 or h < 2 or w > 80 or h > 80: continue
|
|
316
|
+
blobs.append({"cx": centroids[i][0], "cy": centroids[i][1], "w": w, "h": h})
|
|
317
|
+
if not blobs: return None
|
|
318
|
+
med_w = float(np.median([b["w"] for b in blobs]))
|
|
319
|
+
med_h = float(np.median([b["h"] for b in blobs]))
|
|
320
|
+
strict = [b for b in blobs if 0.70*med_w <= b["w"] <= 1.30*med_w and 0.70*med_h <= b["h"] <= 1.30*med_h]
|
|
321
|
+
return {"n": len(strict), "med_w": med_w, "med_h": med_h, "blobs": strict}
|
|
322
|
+
|
|
323
|
+
def detect_dots(pyramid, mode, fixed_t, erosion_level):
|
|
324
|
+
binary = pyramid.get(mode, fixed_t, erosion_level)
|
|
325
|
+
scored = _score_blobs_from_binary(binary)
|
|
326
|
+
if not scored or scored["n"] < MIN_DOTS: return None, None
|
|
327
|
+
pts = np.array([[b["cx"], b["cy"]] for b in scored["blobs"]], dtype=np.float32)
|
|
328
|
+
dot_info = {"med_w": scored["med_w"], "med_h": scored["med_h"], "num_dots": scored["n"]}
|
|
329
|
+
return pts, dot_info
|
|
330
|
+
|
|
331
|
+
def kmeans_1d(vals, k):
|
|
332
|
+
arr = np.array(vals, dtype=np.float32).reshape(-1, 1)
|
|
333
|
+
k = max(2, min(k, len(vals)))
|
|
334
|
+
crit = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.1)
|
|
335
|
+
_, _, centers = cv2.kmeans(arr, k, None, crit, 1, cv2.KMEANS_PP_CENTERS)
|
|
336
|
+
return np.sort(centers.flatten())
|
|
337
|
+
|
|
338
|
+
def snap_to_grid_checkerboard(pts, n_rows, n_cols, dot_info):
|
|
339
|
+
if len(pts) < max(n_rows, n_cols): return None, None, None
|
|
340
|
+
row_c = kmeans_1d(pts[:, 1], n_rows)
|
|
341
|
+
col_c = kmeans_1d(pts[:, 0], n_cols)
|
|
342
|
+
H, W = len(row_c), len(col_c)
|
|
343
|
+
if H < 2 or W < 2: return None, None, None
|
|
344
|
+
row_step = float(np.median(np.diff(row_c))) if H > 1 else 15.0
|
|
345
|
+
col_step = float(np.median(np.diff(col_c))) if W > 1 else 15.0
|
|
346
|
+
med_h = dot_info.get("med_h", row_step * 0.5)
|
|
347
|
+
med_w = dot_info.get("med_w", col_step * 0.5)
|
|
348
|
+
ry = med_h * 0.70; rx = med_w * 0.70
|
|
349
|
+
xs = pts[:, 0]; ys = pts[:, 1]
|
|
350
|
+
r_idx = np.argmin(np.abs(row_c[None, :] - ys[:, None]), axis=1)
|
|
351
|
+
c_idx = np.argmin(np.abs(col_c[None, :] - xs[:, None]), axis=1)
|
|
352
|
+
dr = np.abs(row_c[r_idx] - ys); dc = np.abs(col_c[c_idx] - xs)
|
|
353
|
+
valid = (dr <= ry) & (dc <= rx)
|
|
354
|
+
grid = np.zeros((H, W), dtype=np.uint8)
|
|
355
|
+
grid[r_idx[valid], c_idx[valid]] = 1
|
|
356
|
+
return grid, row_c, col_c
|
|
357
|
+
|
|
358
|
+
# ============================================================
|
|
359
|
+
# EXTRACTOR
|
|
360
|
+
# ============================================================
|
|
361
|
+
def extract_senior1(grid):
|
|
362
|
+
T = grid.T
|
|
363
|
+
corner = [0] * 6; Mask = []; data_bit = []; ncols = T.shape[0]
|
|
364
|
+
for i, row in enumerate(T):
|
|
365
|
+
cropped = row[(i % 2)::2]; n = len(cropped)
|
|
366
|
+
if n < 2: continue
|
|
367
|
+
if i == 0:
|
|
368
|
+
corner[5] = int(cropped[0]); corner[1] = int(cropped[-1])
|
|
369
|
+
if n >= 3: Mask = list(cropped[1:3])
|
|
370
|
+
if n > 3: data_bit.extend(cropped[3:-1])
|
|
371
|
+
elif i == 1:
|
|
372
|
+
corner[3] = int(cropped[-1]); data_bit.extend(cropped[0:-1])
|
|
373
|
+
elif i == ncols - 2:
|
|
374
|
+
corner[2] = int(cropped[-1]); data_bit.extend(cropped[0:-1])
|
|
375
|
+
elif i == ncols - 1:
|
|
376
|
+
corner[4] = int(cropped[0]); corner[0] = int(cropped[-1])
|
|
377
|
+
if n > 1: data_bit.extend(cropped[1:-1])
|
|
378
|
+
else:
|
|
379
|
+
data_bit.extend(cropped)
|
|
380
|
+
return "".join(str(b) for b in Mask + data_bit + corner)
|
|
381
|
+
|
|
382
|
+
# ============================================================
|
|
383
|
+
# DECODE FROM GRID
|
|
384
|
+
# ============================================================
|
|
385
|
+
_GRID_R_IDX = np.arange(GRID_ROWS)[:, None]
|
|
386
|
+
_GRID_C_IDX = np.arange(GRID_COLS)[None, :]
|
|
387
|
+
_GRID_MASK_OFFSET0 = (_GRID_R_IDX + _GRID_C_IDX) % 2 == 0
|
|
388
|
+
_GRID_TOTAL_CELLS = (GRID_ROWS * GRID_COLS + 1) // 2
|
|
389
|
+
|
|
390
|
+
def decode_from_grid(grid):
|
|
391
|
+
if grid is None: return None
|
|
392
|
+
H, W = grid.shape
|
|
393
|
+
if (H, W) == (GRID_ROWS, GRID_COLS):
|
|
394
|
+
mask = _GRID_MASK_OFFSET0
|
|
395
|
+
total_cells = _GRID_TOTAL_CELLS
|
|
396
|
+
else:
|
|
397
|
+
r_indices = np.arange(H)[:, None]
|
|
398
|
+
c_indices = np.arange(W)[None, :]
|
|
399
|
+
mask = (r_indices + c_indices) % 2 == 0
|
|
400
|
+
total_cells = (H * W + 1) // 2
|
|
401
|
+
|
|
402
|
+
g = grid * mask
|
|
403
|
+
occupancy = np.sum(g) / total_cells
|
|
404
|
+
if occupancy < 0.08:
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
bits = extract_senior1(g)
|
|
408
|
+
if bits and len(bits) >= 100:
|
|
409
|
+
ones_ratio = bits.count('1') / len(bits)
|
|
410
|
+
if 0.08 <= ones_ratio <= 0.72:
|
|
411
|
+
msg = decode_bits(bits)
|
|
412
|
+
if msg and is_valid_message(msg):
|
|
413
|
+
return msg
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
# ============================================================
|
|
417
|
+
# REGRESSION-BASED STRAIGHTENING
|
|
418
|
+
# ============================================================
|
|
419
|
+
|
|
420
|
+
def get_global_tilt_angle_regression(pts):
|
|
421
|
+
if len(pts) < 5:
|
|
422
|
+
return 0
|
|
423
|
+
|
|
424
|
+
xs = pts[:, 0]
|
|
425
|
+
ys = pts[:, 1]
|
|
426
|
+
|
|
427
|
+
coeffs = np.polyfit(xs, ys, 1)
|
|
428
|
+
slope = coeffs[0]
|
|
429
|
+
angle = np.degrees(np.arctan(slope))
|
|
430
|
+
|
|
431
|
+
if angle > 45:
|
|
432
|
+
angle -= 90
|
|
433
|
+
elif angle < -45:
|
|
434
|
+
angle += 90
|
|
435
|
+
|
|
436
|
+
return angle
|
|
437
|
+
|
|
438
|
+
def rotate_points(pts, angle):
|
|
439
|
+
if abs(angle) < 0.05:
|
|
440
|
+
return pts
|
|
441
|
+
center = np.mean(pts, axis=0)
|
|
442
|
+
theta = np.radians(-angle)
|
|
443
|
+
c, s = np.cos(theta), np.sin(theta)
|
|
444
|
+
R = np.array([[c, -s], [s, c]])
|
|
445
|
+
return (pts - center) @ R.T + center
|
|
446
|
+
|
|
447
|
+
def compute_row_quality(pts, n_rows=GRID_ROWS):
|
|
448
|
+
"""
|
|
449
|
+
Compute how well points align to rows (lower score = better alignment)
|
|
450
|
+
"""
|
|
451
|
+
if len(pts) < 20:
|
|
452
|
+
return float('inf')
|
|
453
|
+
|
|
454
|
+
ys = pts[:, 1]
|
|
455
|
+
row_centers = kmeans_1d(ys, n_rows)
|
|
456
|
+
row_idx = np.argmin(np.abs(row_centers[None, :] - ys[:, None]), axis=1)
|
|
457
|
+
|
|
458
|
+
total_var = 0
|
|
459
|
+
valid_rows = 0
|
|
460
|
+
|
|
461
|
+
for r in range(len(row_centers)):
|
|
462
|
+
mask = row_idx == r
|
|
463
|
+
if mask.sum() > 3:
|
|
464
|
+
total_var += np.var(ys[mask])
|
|
465
|
+
valid_rows += 1
|
|
466
|
+
|
|
467
|
+
if valid_rows == 0:
|
|
468
|
+
return float('inf')
|
|
469
|
+
return total_var / valid_rows
|
|
470
|
+
|
|
471
|
+
def get_angle_from_rows(pts, n_rows=GRID_ROWS):
|
|
472
|
+
"""
|
|
473
|
+
Compute angle by finding row slopes and averaging.
|
|
474
|
+
This handles curvature better than global line fit.
|
|
475
|
+
"""
|
|
476
|
+
if len(pts) < 20:
|
|
477
|
+
return 0
|
|
478
|
+
|
|
479
|
+
ys = pts[:, 1]
|
|
480
|
+
xs = pts[:, 0]
|
|
481
|
+
|
|
482
|
+
# 1. Find row centers
|
|
483
|
+
row_centers = kmeans_1d(ys, n_rows)
|
|
484
|
+
row_idx = np.argmin(np.abs(row_centers[None, :] - ys[:, None]), axis=1)
|
|
485
|
+
|
|
486
|
+
# 2. For each row, compute its slope
|
|
487
|
+
slopes = []
|
|
488
|
+
row_counts = []
|
|
489
|
+
|
|
490
|
+
for r in range(len(row_centers)):
|
|
491
|
+
mask = row_idx == r
|
|
492
|
+
count = mask.sum()
|
|
493
|
+
if count < 4:
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
row_xs = xs[mask]
|
|
497
|
+
row_ys = ys[mask]
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
coeffs = np.polyfit(row_xs, row_ys, 1)
|
|
501
|
+
slopes.append(coeffs[0])
|
|
502
|
+
row_counts.append(count)
|
|
503
|
+
except:
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
if not slopes:
|
|
507
|
+
return 0
|
|
508
|
+
|
|
509
|
+
# 3. Average the slopes (weighted by row size)
|
|
510
|
+
weights = np.array(row_counts) / sum(row_counts)
|
|
511
|
+
avg_slope = np.sum(np.array(slopes) * weights)
|
|
512
|
+
|
|
513
|
+
# 4. Convert to angle
|
|
514
|
+
angle = np.degrees(np.arctan(avg_slope))
|
|
515
|
+
|
|
516
|
+
if angle > 45:
|
|
517
|
+
angle -= 90
|
|
518
|
+
elif angle < -45:
|
|
519
|
+
angle += 90
|
|
520
|
+
|
|
521
|
+
return angle
|
|
522
|
+
|
|
523
|
+
def straighten_dots_regression_iterative(pts, n_rows=GRID_ROWS, degree=2, iterations=2):
|
|
524
|
+
"""
|
|
525
|
+
ITERATIVE REGRESSION:
|
|
526
|
+
1. Get angle from row slopes
|
|
527
|
+
2. Rotate points
|
|
528
|
+
3. Cluster rows
|
|
529
|
+
4. Fit per-row quadratic curves
|
|
530
|
+
5. Apply correction
|
|
531
|
+
6. Repeat (re-cluster with corrected points)
|
|
532
|
+
7. Returns straightened points
|
|
533
|
+
"""
|
|
534
|
+
if pts is None or len(pts) < 20:
|
|
535
|
+
return pts, 0
|
|
536
|
+
|
|
537
|
+
# āāā Step 1: Initial angle from rows āāā
|
|
538
|
+
angle = get_angle_from_rows(pts, n_rows)
|
|
539
|
+
current_pts = rotate_points(pts, angle)
|
|
540
|
+
best_angle = angle
|
|
541
|
+
|
|
542
|
+
# āāā Step 2: Iterative refinement āāā
|
|
543
|
+
for iteration in range(iterations):
|
|
544
|
+
xs = current_pts[:, 0]
|
|
545
|
+
ys = current_pts[:, 1]
|
|
546
|
+
|
|
547
|
+
# Cluster rows
|
|
548
|
+
row_centers = kmeans_1d(ys, n_rows)
|
|
549
|
+
row_idx = np.argmin(np.abs(row_centers[None, :] - ys[:, None]), axis=1)
|
|
550
|
+
|
|
551
|
+
# Fit per-row polynomials
|
|
552
|
+
coeffs_list = []
|
|
553
|
+
row_weights = []
|
|
554
|
+
|
|
555
|
+
for r in range(len(row_centers)):
|
|
556
|
+
mask = row_idx == r
|
|
557
|
+
count = mask.sum()
|
|
558
|
+
if count < max(4, degree + 2):
|
|
559
|
+
continue
|
|
560
|
+
|
|
561
|
+
row_xs = xs[mask]
|
|
562
|
+
row_ys = ys[mask]
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
c = np.polyfit(row_xs, row_ys, degree)
|
|
566
|
+
coeffs_list.append(c)
|
|
567
|
+
row_weights.append(count)
|
|
568
|
+
except:
|
|
569
|
+
continue
|
|
570
|
+
|
|
571
|
+
# If we have enough rows, apply correction
|
|
572
|
+
if len(coeffs_list) >= 2:
|
|
573
|
+
weights = np.array(row_weights) / sum(row_weights)
|
|
574
|
+
avg_coeffs = np.zeros(len(coeffs_list[0]))
|
|
575
|
+
|
|
576
|
+
for c, w in zip(coeffs_list, weights):
|
|
577
|
+
avg_coeffs += c * w
|
|
578
|
+
|
|
579
|
+
trend = np.polyval(avg_coeffs, xs)
|
|
580
|
+
mean_y = float(np.mean(ys))
|
|
581
|
+
current_pts[:, 1] = ys - trend + mean_y
|
|
582
|
+
|
|
583
|
+
# Recompute angle for next iteration (if any)
|
|
584
|
+
if iteration < iterations - 1:
|
|
585
|
+
angle = get_angle_from_rows(current_pts, n_rows)
|
|
586
|
+
current_pts = rotate_points(current_pts, angle)
|
|
587
|
+
best_angle = angle
|
|
588
|
+
else:
|
|
589
|
+
# Not enough rows, break out
|
|
590
|
+
break
|
|
591
|
+
|
|
592
|
+
# āāā Step 3: Final angle āāā
|
|
593
|
+
final_angle = get_angle_from_rows(current_pts, n_rows)
|
|
594
|
+
|
|
595
|
+
# āāā Step 4: Quality check āāā
|
|
596
|
+
final_score = compute_row_quality(current_pts, n_rows)
|
|
597
|
+
original_score = compute_row_quality(pts, n_rows)
|
|
598
|
+
|
|
599
|
+
# If we made it worse, return original
|
|
600
|
+
if final_score > original_score * 1.2:
|
|
601
|
+
return pts, 0
|
|
602
|
+
|
|
603
|
+
return current_pts, final_angle
|
|
604
|
+
|
|
605
|
+
# ============================================================
|
|
606
|
+
# SMART COMBO SELECTION
|
|
607
|
+
# ============================================================
|
|
608
|
+
|
|
609
|
+
def get_smart_combos(mean_brightness):
|
|
610
|
+
if mean_brightness < 40:
|
|
611
|
+
return [
|
|
612
|
+
("otsu", None, [2, 3, 4]),
|
|
613
|
+
("fixed", 135, [2, 3]),
|
|
614
|
+
("fixed", 145, [2, 3]),
|
|
615
|
+
("fixed", 155, [2, 3]),
|
|
616
|
+
]
|
|
617
|
+
elif mean_brightness < 55:
|
|
618
|
+
return [
|
|
619
|
+
("otsu", None, [2, 3]),
|
|
620
|
+
("fixed", 145, [2, 3]),
|
|
621
|
+
("fixed", 155, [2, 3]),
|
|
622
|
+
("fixed", 165, [2, 3]),
|
|
623
|
+
]
|
|
624
|
+
elif mean_brightness < 70:
|
|
625
|
+
return [
|
|
626
|
+
("otsu", None, [2, 3]),
|
|
627
|
+
("fixed", 155, [2, 3, 4]),
|
|
628
|
+
("fixed", 165, [2, 3, 4]),
|
|
629
|
+
("fixed", 175, [2, 3]),
|
|
630
|
+
]
|
|
631
|
+
else:
|
|
632
|
+
return [
|
|
633
|
+
("otsu", None, [2, 3]),
|
|
634
|
+
("fixed", 165, [2, 3]),
|
|
635
|
+
("fixed", 175, [2, 3]),
|
|
636
|
+
("fixed", 155, [2, 3]),
|
|
637
|
+
]
|
|
638
|
+
|
|
639
|
+
# ============================================================
|
|
640
|
+
# v74.9 - ITERATIVE REGRESSION
|
|
641
|
+
# ============================================================
|
|
642
|
+
|
|
643
|
+
def decode_image_v74_iterative(img, filename=None, timeout_ms=300):
|
|
644
|
+
"""
|
|
645
|
+
Same as v74.7 but with iterative regression.
|
|
646
|
+
"""
|
|
647
|
+
if img is None:
|
|
648
|
+
return None, None, 0, 0, False
|
|
649
|
+
|
|
650
|
+
start_time = time.time()
|
|
651
|
+
|
|
652
|
+
def check_timeout():
|
|
653
|
+
return (time.time() - start_time) * 1000 > timeout_ms
|
|
654
|
+
|
|
655
|
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if img.ndim == 3 else img
|
|
656
|
+
pyramid = BinaryPyramid(gray)
|
|
657
|
+
mean_brightness = gray.mean()
|
|
658
|
+
|
|
659
|
+
# āāā STEP 1: Find BEST dots āāā
|
|
660
|
+
best_pts = None
|
|
661
|
+
best_dot_info = None
|
|
662
|
+
best_count = 0
|
|
663
|
+
|
|
664
|
+
smart_combos = get_smart_combos(mean_brightness)
|
|
665
|
+
|
|
666
|
+
for mode, threshold, erosions in smart_combos:
|
|
667
|
+
for erosion in erosions:
|
|
668
|
+
pts, dot_info = detect_dots(pyramid, mode, threshold, erosion)
|
|
669
|
+
if pts is not None and len(pts) > best_count:
|
|
670
|
+
best_count = len(pts)
|
|
671
|
+
best_pts = pts
|
|
672
|
+
best_dot_info = dot_info
|
|
673
|
+
|
|
674
|
+
if best_pts is None or len(best_pts) < MIN_DOTS:
|
|
675
|
+
elapsed = (time.time() - start_time) * 1000
|
|
676
|
+
return None, None, elapsed, 0, False
|
|
677
|
+
|
|
678
|
+
# Check timeout after dot detection
|
|
679
|
+
if check_timeout():
|
|
680
|
+
return None, "TIMEOUT", timeout_ms, 0, True
|
|
681
|
+
|
|
682
|
+
# āāā STEP 2: ITERATIVE REGRESSION āāā
|
|
683
|
+
straight_pts, angle = straighten_dots_regression_iterative(best_pts, GRID_ROWS, degree=2, iterations=REGRESSION_ITERATIONS)
|
|
684
|
+
|
|
685
|
+
# ONLY ONE regression decode attempt (not 7-10x)
|
|
686
|
+
grid, _, _ = snap_to_grid_checkerboard(straight_pts, GRID_ROWS, GRID_COLS, best_dot_info)
|
|
687
|
+
if grid is not None:
|
|
688
|
+
occupancy = np.sum(grid) / _GRID_TOTAL_CELLS
|
|
689
|
+
if occupancy >= 0.08:
|
|
690
|
+
msg = decode_from_grid(grid)
|
|
691
|
+
if msg:
|
|
692
|
+
elapsed = (time.time() - start_time) * 1000
|
|
693
|
+
method = f"reg_iter_{angle:.1f}"
|
|
694
|
+
return msg, method, elapsed, 1, False
|
|
695
|
+
|
|
696
|
+
# āāā STEP 3: Finer Angle Sweep (UNCHANGED) āāā
|
|
697
|
+
fallback_thresholds = [
|
|
698
|
+
("otsu", None, [2, 3]),
|
|
699
|
+
("fixed", 155, [2, 3]),
|
|
700
|
+
("fixed", 165, [2, 3]),
|
|
701
|
+
("fixed", 145, [2, 3]),
|
|
702
|
+
("fixed", 175, [2, 3]),
|
|
703
|
+
("fixed", 135, [2, 3]),
|
|
704
|
+
("fixed", 125, [2, 3]),
|
|
705
|
+
("fixed", 115, [2, 3]),
|
|
706
|
+
("fixed", 105, [2, 3]),
|
|
707
|
+
]
|
|
708
|
+
|
|
709
|
+
fine_angles = [3.0, 2.5, 2.0, 1.5, 1.0, 0.5, -0.5, -1.0, -1.5, -2.0, -2.5, -3.0]
|
|
710
|
+
# fine_angles = [ 1.0, 0.5, -0.5, -1.0]
|
|
711
|
+
|
|
712
|
+
for mode, threshold, erosions in fallback_thresholds:
|
|
713
|
+
if check_timeout():
|
|
714
|
+
return None, "TIMEOUT", timeout_ms, 0, True
|
|
715
|
+
|
|
716
|
+
for erosion in erosions:
|
|
717
|
+
pts, dot_info = detect_dots(pyramid, mode, threshold, erosion)
|
|
718
|
+
if pts is None or len(pts) < MIN_DOTS:
|
|
719
|
+
continue
|
|
720
|
+
|
|
721
|
+
for angle in fine_angles:
|
|
722
|
+
if check_timeout():
|
|
723
|
+
return None, "TIMEOUT", timeout_ms, 0, True
|
|
724
|
+
|
|
725
|
+
rotated_pts = rotate_points(pts, angle)
|
|
726
|
+
grid, _, _ = snap_to_grid_checkerboard(rotated_pts, GRID_ROWS, GRID_COLS, dot_info)
|
|
727
|
+
if grid is None:
|
|
728
|
+
continue
|
|
729
|
+
|
|
730
|
+
occupancy = np.sum(grid) / _GRID_TOTAL_CELLS
|
|
731
|
+
if occupancy < 0.08:
|
|
732
|
+
continue
|
|
733
|
+
|
|
734
|
+
msg = decode_from_grid(grid)
|
|
735
|
+
if msg:
|
|
736
|
+
confidence = occupancy * min(len(pts) / 150, 1.0)
|
|
737
|
+
method = f"ang_{mode if mode == 'otsu' else f'fixed_{threshold}'}_e{erosion}"
|
|
738
|
+
|
|
739
|
+
if confidence >= EARLY_EXIT_CONFIDENCE:
|
|
740
|
+
elapsed = (time.time() - start_time) * 1000
|
|
741
|
+
return msg, method, elapsed, 2, False
|
|
742
|
+
|
|
743
|
+
# āāā STEP 4: Last Resort - Invert (UNCHANGED) āāā
|
|
744
|
+
if not check_timeout():
|
|
745
|
+
inverted = cv2.bitwise_not(gray)
|
|
746
|
+
pyramid_inv = BinaryPyramid(inverted)
|
|
747
|
+
|
|
748
|
+
for mode, threshold, erosions in [("otsu", None, [2, 3]), ("fixed", 155, [2, 3])]:
|
|
749
|
+
if check_timeout():
|
|
750
|
+
return None, "TIMEOUT", timeout_ms, 0, True
|
|
751
|
+
|
|
752
|
+
for erosion in erosions:
|
|
753
|
+
pts, dot_info = detect_dots(pyramid_inv, mode, threshold, erosion)
|
|
754
|
+
if pts is None or len(pts) < MIN_DOTS:
|
|
755
|
+
continue
|
|
756
|
+
|
|
757
|
+
for angle in fine_angles[:5]:
|
|
758
|
+
if check_timeout():
|
|
759
|
+
return None, "TIMEOUT", timeout_ms, 0, True
|
|
760
|
+
|
|
761
|
+
rotated_pts = rotate_points(pts, angle)
|
|
762
|
+
grid, _, _ = snap_to_grid_checkerboard(rotated_pts, GRID_ROWS, GRID_COLS, dot_info)
|
|
763
|
+
if grid is None:
|
|
764
|
+
continue
|
|
765
|
+
|
|
766
|
+
occupancy = np.sum(grid) / _GRID_TOTAL_CELLS
|
|
767
|
+
if occupancy < 0.08:
|
|
768
|
+
continue
|
|
769
|
+
|
|
770
|
+
msg = decode_from_grid(grid)
|
|
771
|
+
if msg:
|
|
772
|
+
confidence = occupancy * min(len(pts) / 150, 1.0)
|
|
773
|
+
method = f"inv_{mode if mode == 'otsu' else f'fixed_{threshold}'}_e{erosion}"
|
|
774
|
+
|
|
775
|
+
if confidence >= EARLY_EXIT_CONFIDENCE:
|
|
776
|
+
elapsed = (time.time() - start_time) * 1000
|
|
777
|
+
return msg, method, elapsed, 3, False
|
|
778
|
+
|
|
779
|
+
elapsed = (time.time() - start_time) * 1000
|
|
780
|
+
return None, None, elapsed, 0, False
|
|
781
|
+
|
|
782
|
+
# ============================================================
|
|
783
|
+
# PROCESS IMAGE
|
|
784
|
+
# ============================================================
|
|
785
|
+
def process_image_v74_iterative(img_path, timeout_seconds=0.3):
|
|
786
|
+
filename = os.path.basename(img_path)
|
|
787
|
+
img = cv2.imread(img_path)
|
|
788
|
+
if img is None:
|
|
789
|
+
return {"file": filename, "success": False, "message": "READ_ERROR", "time": 0, "method": "N/A", "stage": 0, "timed_out": False}
|
|
790
|
+
|
|
791
|
+
msg, method, elapsed, stage, timed_out = decode_image_v74_iterative(img, filename, timeout_ms=timeout_seconds * 1000)
|
|
792
|
+
|
|
793
|
+
if timed_out:
|
|
794
|
+
save_path = os.path.join(UNDETECTED_DIR, f"{os.path.splitext(filename)[0]}_timeout.jpg")
|
|
795
|
+
cv2.imwrite(save_path, img)
|
|
796
|
+
return {"file": filename, "success": False, "message": "TIMEOUT", "time": timeout_seconds * 1000, "method": "TIMEOUT", "stage": 0, "timed_out": True}
|
|
797
|
+
|
|
798
|
+
if msg is not None:
|
|
799
|
+
save_path = os.path.join(DECODED_DIR, f"{os.path.splitext(filename)[0]}_decoded.jpg")
|
|
800
|
+
cv2.imwrite(save_path, img)
|
|
801
|
+
return {"file": filename, "success": True, "message": msg, "time": elapsed, "method": method, "stage": stage, "timed_out": False}
|
|
802
|
+
else:
|
|
803
|
+
save_path = os.path.join(UNDETECTED_DIR, f"{os.path.splitext(filename)[0]}_undetected.jpg")
|
|
804
|
+
cv2.imwrite(save_path, img)
|
|
805
|
+
return {"file": filename, "success": False, "message": "FAILED", "time": elapsed, "method": "N/A", "stage": 0, "timed_out": False}
|
|
806
|
+
|
|
807
|
+
# ============================================================
|
|
808
|
+
# BATCH PROCESSING
|
|
809
|
+
# ============================================================
|
|
810
|
+
def batch_decode():
|
|
811
|
+
print(f"\n{'='*80}")
|
|
812
|
+
print(f"š v74.9 - ITERATIVE REGRESSION")
|
|
813
|
+
print(f"{'='*80}")
|
|
814
|
+
print(f"š Input: {INPUT_DIR}")
|
|
815
|
+
print(f"š Output: {OUTPUT_DIR}")
|
|
816
|
+
print(f"ā” Per-row quadratic regression with {REGRESSION_ITERATIONS} iterations")
|
|
817
|
+
print(f"ā±ļø TIMEOUT: {TIMEOUT_SECONDS*1000}ms (only between stages)")
|
|
818
|
+
print(f"{'='*80}\n")
|
|
819
|
+
|
|
820
|
+
images = []
|
|
821
|
+
for ext in ['*.jpg', '*.jpeg', '*.png', '*.bmp']:
|
|
822
|
+
images.extend(glob.glob(os.path.join(INPUT_DIR, ext)))
|
|
823
|
+
|
|
824
|
+
if not images:
|
|
825
|
+
print("No images found")
|
|
826
|
+
return
|
|
827
|
+
|
|
828
|
+
print(f"Found {len(images)} images")
|
|
829
|
+
print("-" * 80)
|
|
830
|
+
|
|
831
|
+
total_start = time.time()
|
|
832
|
+
success_count = 0
|
|
833
|
+
timeout_count = 0
|
|
834
|
+
total_time = 0.0
|
|
835
|
+
failed_images = []
|
|
836
|
+
timeout_images = []
|
|
837
|
+
method_counts = {}
|
|
838
|
+
stage_counts = {1: 0, 2: 0, 3: 0}
|
|
839
|
+
|
|
840
|
+
chunksize = max(1, len(images) // (MAX_WORKERS * 8))
|
|
841
|
+
with ProcessPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
|
842
|
+
futures = {executor.submit(process_image_v74_iterative, img, TIMEOUT_SECONDS): img for img in images}
|
|
843
|
+
|
|
844
|
+
for idx, (future, img) in enumerate(futures.items(), 1):
|
|
845
|
+
try:
|
|
846
|
+
result = future.result(timeout=TIMEOUT_SECONDS + 0.1)
|
|
847
|
+
except FutureTimeoutError:
|
|
848
|
+
filename = os.path.basename(img)
|
|
849
|
+
timeout_count += 1
|
|
850
|
+
timeout_images.append(filename)
|
|
851
|
+
print(f"[{idx}/{len(images)}] ā±ļø {filename} TIMEOUT (> {TIMEOUT_SECONDS*1000}ms)")
|
|
852
|
+
continue
|
|
853
|
+
|
|
854
|
+
total_time += result['time']
|
|
855
|
+
|
|
856
|
+
if result['success']:
|
|
857
|
+
success_count += 1
|
|
858
|
+
method = result.get('method', 'unknown')
|
|
859
|
+
method_counts[method] = method_counts.get(method, 0) + 1
|
|
860
|
+
stage = result.get('stage', 0)
|
|
861
|
+
if stage in stage_counts:
|
|
862
|
+
stage_counts[stage] += 1
|
|
863
|
+
|
|
864
|
+
stage_label = {1: "REG", 2: "ANG", 3: "INV"}.get(stage, "?")
|
|
865
|
+
print(f"[{idx}/{len(images)}] ā
{result['file']} ā {result['message'][:20]}... "
|
|
866
|
+
f"({result['time']:.0f}ms) [{stage_label}:{method}]")
|
|
867
|
+
else:
|
|
868
|
+
if result.get('timed_out', False):
|
|
869
|
+
timeout_count += 1
|
|
870
|
+
timeout_images.append(result['file'])
|
|
871
|
+
print(f"[{idx}/{len(images)}] ā±ļø {result['file']} TIMEOUT ({result['time']:.0f}ms)")
|
|
872
|
+
else:
|
|
873
|
+
failed_images.append(result['file'])
|
|
874
|
+
print(f"[{idx}/{len(images)}] ā {result['file']} FAILED ({result['time']:.0f}ms)")
|
|
875
|
+
|
|
876
|
+
if idx % 50 == 0:
|
|
877
|
+
avg = total_time / idx if idx > 0 else 0
|
|
878
|
+
eta = (len(images) - idx) * avg / 1000
|
|
879
|
+
print(f"š Progress: {idx}/{len(images)} | Success: {success_count} | Timeouts: {timeout_count} | Avg:{avg:.0f}ms | ETA:{eta:.1f}s")
|
|
880
|
+
|
|
881
|
+
total_time_sec = time.time() - total_start
|
|
882
|
+
avg_time = total_time / len(images) if images else 0
|
|
883
|
+
|
|
884
|
+
print("=" * 80)
|
|
885
|
+
print("BATCH PROCESSING COMPLETE")
|
|
886
|
+
print(f"Total images: {len(images)}")
|
|
887
|
+
print(f"ā
Success: {success_count} ({success_count/len(images)*100:.1f}%)")
|
|
888
|
+
print(f"ā Failed: {len(images) - success_count - timeout_count} ({(len(images)-success_count-timeout_count)/len(images)*100:.1f}%)")
|
|
889
|
+
print(f"ā±ļø Timeouts: {timeout_count} ({timeout_count/len(images)*100:.1f}%)")
|
|
890
|
+
print(f"ā±ļø Total wall time: {total_time_sec:.1f}s")
|
|
891
|
+
print(f"ā” Average per-image compute time: {avg_time:.0f}ms")
|
|
892
|
+
|
|
893
|
+
print(f"\nš Stage Distribution:")
|
|
894
|
+
print(f" Regression (fast): {stage_counts.get(1, 0)} images")
|
|
895
|
+
print(f" Angle Sweep: {stage_counts.get(2, 0)} images")
|
|
896
|
+
print(f" Invert Fallback: {stage_counts.get(3, 0)} images")
|
|
897
|
+
|
|
898
|
+
if failed_images:
|
|
899
|
+
print(f"\nā FAILED IMAGES ({len(failed_images)}):")
|
|
900
|
+
for f in sorted(failed_images)[:20]:
|
|
901
|
+
print(f" - {f}")
|
|
902
|
+
if len(failed_images) > 20:
|
|
903
|
+
print(f" ... and {len(failed_images)-20} more")
|
|
904
|
+
|
|
905
|
+
if timeout_images:
|
|
906
|
+
print(f"\nā±ļø TIMEOUT IMAGES ({len(timeout_images)}):")
|
|
907
|
+
for f in sorted(timeout_images)[:20]:
|
|
908
|
+
print(f" - {f}")
|
|
909
|
+
if len(timeout_images) > 20:
|
|
910
|
+
print(f" ... and {len(timeout_images)-20} more")
|
|
911
|
+
print("=" * 80)
|
|
912
|
+
|
|
913
|
+
if __name__ == "__main__":
|
|
914
|
+
mp.freeze_support()
|
|
915
|
+
batch_decode()
|