Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

api_scan.inc

v0.1.0-build.85 Latest Commit ede38f5

Size
44 KB
Lines
613
Language
cpp
SHA-256
a39dd0e22f71…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// Public C ABI: scan functions. Included after core_internal.inc.
2
3extern "C" {
4
5double ckey_calibration_score_gray8(const uint8_t *gray, size_t width, size_t height,
6 size_t stride, const double quad_xy[8]) {
7 if (!valid_image_args(gray, width, height, stride, quad_xy)) return -1.0;
8
9 constexpr double canonical[8] = {0,0,29,0,29,29,0,29};
10 double best = 0.0;
11 // Key polarity and orientation are intentionally unknown to localization.
12 // Score every cyclic winding exactly as the decoder does, but retain a
13 // smooth contrast/evidence measure rather than requiring a valid payload.
14 for (int reverse = 0; reverse < 2; ++reverse) for (int start = 0; start < 4; ++start) {
15 double dst[8];
16 for (int i = 0; i < 4; ++i) {
17 const int qi = (start + (reverse ? -i : i) + 8) % 4;
18 dst[2 * i] = quad_xy[2 * qi]; dst[2 * i + 1] = quad_xy[2 * qi + 1];
19 }
20 double h[8]; if (!solve_homography(canonical, dst, h)) continue;
21 std::vector<double> grid, ring, ray, finder, empty;
22 for (int r : {11,12}) for (int c : {2,3}) grid.push_back(gray_sample(gray,width,height,stride,h,c+.5,r+.5));
23 for (int slot : {18,19,21}) ring.push_back(polar_sample(gray,width,height,stride,h,6.0,slot));
24 ring.push_back(polar_sample(gray,width,height,stride,h,6.0,23.5));
25 ray.push_back(polar_sample(gray,width,height,stride,h,8.8,0));
26 ray.push_back(polar_sample(gray,width,height,stride,h,8.4,54));
27 ray.push_back(polar_sample(gray,width,height,stride,h,8.8,55));
28 // The one-arc rounded-corner finder is localization evidence. It is
29 // not an acceptance shortcut: keys, RS, and CRC remain authoritative.
30 constexpr double finder_inset=4.00;
31 const double diagonal=kTau/8.0;
32 for (double finder_radius : {2.30}) {
33 finder.push_back(gray_sample(gray,width,height,stride,h,29.0-(finder_inset+finder_radius*std::cos(diagonal)),finder_inset+finder_radius*std::sin(diagonal)));
34 finder.push_back(gray_sample(gray,width,height,stride,h,29.0-(finder_inset+finder_radius*std::cos(diagonal)),29.0-(finder_inset+finder_radius*std::sin(diagonal))));
35 finder.push_back(gray_sample(gray,width,height,stride,h,finder_inset+finder_radius*std::cos(diagonal),29.0-(finder_inset+finder_radius*std::sin(diagonal))));
36 const double first=kTau*18.0/360.0, second=kTau*72.0/360.0;
37 finder.push_back(gray_sample(gray,width,height,stride,h,finder_inset+finder_radius*std::cos(first),finder_inset+finder_radius*std::sin(first)));
38 finder.push_back(gray_sample(gray,width,height,stride,h,finder_inset+finder_radius*std::cos(second),finder_inset+finder_radius*std::sin(second)));
39 empty.push_back(gray_sample(gray,width,height,stride,h,finder_inset+finder_radius*std::cos(diagonal),finder_inset+finder_radius*std::sin(diagonal)));
40 }
41 empty.push_back(polar_sample(gray,width,height,stride,h,kLocatorApertureRadius,36));
42 empty.push_back(polar_sample(gray,width,height,stride,h,6.0,20));
43 empty.push_back(polar_sample(gray,width,height,stride,h,8.8,54));
44 std::vector<double> marks = grid;
45 marks.insert(marks.end(), ring.begin(), ring.end());
46 marks.insert(marks.end(), ray.begin(), ray.end());
47 marks.insert(marks.end(), finder.begin(), finder.end());
48 if (std::any_of(marks.begin(),marks.end(),[](double v){ return v < 0; }) ||
49 std::any_of(empty.begin(),empty.end(),[](double v){ return v < 0; })) continue;
50 const double mark_mean = std::accumulate(marks.begin(),marks.end(),0.0) / marks.size();
51 const double empty_mean = std::accumulate(empty.begin(),empty.end(),0.0) / empty.size();
52 const double contrast = std::abs(mark_mean - empty_mean);
53 const bool dark_marks = mark_mean < empty_mean;
54 const double threshold = (mark_mean + empty_mean) * .5;
55 const auto marked = [&](double v) { return dark_marks ? v < threshold : v > threshold; };
56 const size_t evidence = std::count_if(marks.begin(),marks.end(),marked) +
57 std::count_if(empty.begin(),empty.end(),[&](double v){ return !marked(v); });
58 // Contrast supplies a continuous hill for sub-pixel geometry search;
59 // expected key evidence prevents a bright non-CKey rectangle winning.
60 best = std::max(best, contrast * static_cast<double>(evidence) / static_cast<double>(marks.size() + empty.size()));
61 }
62 return best;
63}
64
65double ckey_calibration_score_text_gray8(const uint8_t *gray, size_t width, size_t height,
66 size_t stride, const double quad_xy[8]) {
67 if (!valid_image_args(gray, width, height, stride, quad_xy)) return -1.0;
68 constexpr double canonical[8] = {0,0,29,0,29,29,0,29};
69 double best = 0.0;
70 for (int reverse=0; reverse<2; ++reverse) for (int start=0; start<4; ++start) {
71 double dst[8];
72 for (int i=0; i<4; ++i) {
73 const int q=(start+(reverse ? -i : i)+8)%4;
74 dst[2*i]=quad_xy[2*q]; dst[2*i+1]=quad_xy[2*q+1];
75 }
76 double h[8]; if (!solve_homography(canonical,dst,h)) continue;
77 const KeyFit fit=text_key_fit(gray,width,height,stride,h);
78 if (fit.valid) best=std::max(best,fit.contrast*fit.evidence);
79 }
80 return best;
81}
82
83double ckey_calibration_score_extended_gray8(const uint8_t *gray, size_t width, size_t height,
84 size_t stride, const double quad_xy[8]) {
85 if (!valid_image_args(gray, width, height, stride, quad_xy)) return -1.0;
86 constexpr double canonical[8] = {0,0,116,0,116,116,0,116};
87 double best = 0.0;
88 for (int reverse=0; reverse<2; ++reverse) for (int start=0; start<4; ++start) {
89 double dst[8];
90 for (int i=0; i<4; ++i) { const int q=(start+(reverse ? -i : i)+8)%4; dst[2*i]=quad_xy[2*q]; dst[2*i+1]=quad_xy[2*q+1]; }
91 double h[8]; if (!solve_homography(canonical,dst,h)) continue;
92 const KeyFit fit=extended_key_fit(gray,width,height,stride,h);
93 if (fit.valid) best=std::max(best,fit.contrast*fit.evidence);
94 }
95 return best;
96}
97
98ckey_status ckey_decode_gray8(const uint8_t *gray, size_t width, size_t height, size_t stride,
99 const double quad_xy[8], uint8_t *payload, size_t out_capacity,
100 ckey_diagnostics *diag) {
101 if (!gray || !quad_xy || !payload || width<2 || height<2 || stride<width ||
102 out_capacity<CKEY_LOCATOR_PAYLOAD_BYTES || height>std::numeric_limits<size_t>::max()/stride) {
103 set_status(diag,CKEY_INVALID_ARGUMENT,"invalid grayscale image, quadrilateral, or output buffer"); return CKEY_INVALID_ARGUMENT;
104 }
105 for (int i=0;i<8;++i) if (!std::isfinite(quad_xy[i])) { set_status(diag,CKEY_INVALID_ARGUMENT,"quadrilateral coordinates must be finite"); return CKEY_INVALID_ARGUMENT; }
106 if (!valid_quad(quad_xy)) { set_status(diag,CKEY_INVALID_ARGUMENT,"quadrilateral must be a non-degenerate convex cyclic perimeter"); return CKEY_INVALID_ARGUMENT; }
107
108 std::array<uint8_t,CKEY_LOCATOR_PAYLOAD_BYTES> accepted{};
109 bool found=false, ambiguous=false; uint32_t accepted_corrections=0;
110 constexpr double canonical[8]={0,0,29,0,29,29,0,29};
111 // Try all cyclic starts and both winding directions; orientation is validated by keys and frame CRC.
112 for (int reverse=0;reverse<2;++reverse) for (int start=0;start<4;++start) {
113 double dst[8];
114 for (int i=0;i<4;++i) { int qi=(start+(reverse?-i:i)+8)%4; dst[2*i]=quad_xy[2*qi]; dst[2*i+1]=quad_xy[2*qi+1]; }
115 double h[8]; if (!solve_homography(canonical,dst,h)) continue;
116 // Require the full symbol footprint, including every sample patch, to be in the image.
117 ImagePoint corners[4]; bool inside=true;
118 for(int i=0;i<4;++i) { if(!map_point(h,canonical[2*i],canonical[2*i+1],corners[i]) || corners[i].x<0 || corners[i].y<0 || corners[i].x>=width || corners[i].y>=height) inside=false; }
119 if(!inside) continue;
120
121 std::vector<double> grid_marks, ring_marks, ray_marks, empties;
122 for (int r : {11,12}) for (int c : {2,3}) grid_marks.push_back(gray_sample(gray,width,height,stride,h,c+.5,r+.5));
123 for (int slot : {18,19,21}) ring_marks.push_back(polar_sample(gray,width,height,stride,h,6.0,slot));
124 ring_marks.push_back(polar_sample(gray,width,height,stride,h,6.0,23.5)); // center of the reserved continuous four-slot arc
125 ray_marks.push_back(polar_sample(gray,width,height,stride,h,8.8,0));
126 ray_marks.push_back(polar_sample(gray,width,height,stride,h,8.4,54));
127 ray_marks.push_back(polar_sample(gray,width,height,stride,h,8.8,55));
128 empties.push_back(polar_sample(gray,width,height,stride,h,kLocatorApertureRadius,36));
129 empties.push_back(polar_sample(gray,width,height,stride,h,6.0,20));
130 empties.push_back(polar_sample(gray,width,height,stride,h,8.8,54));
131 std::vector<double> marks=grid_marks;
132 marks.insert(marks.end(),ring_marks.begin(),ring_marks.end());
133 marks.insert(marks.end(),ray_marks.begin(),ray_marks.end());
134 if (std::any_of(marks.begin(),marks.end(),[](double v){return v<0;}) || std::any_of(empties.begin(),empties.end(),[](double v){return v<0;})) continue;
135 const double mark_mean=std::accumulate(marks.begin(),marks.end(),0.0)/marks.size();
136 const double empty_mean=std::accumulate(empties.begin(),empties.end(),0.0)/empties.size();
137 if (std::abs(mark_mean-empty_mean)<24.0) continue;
138 const bool dark_marks=mark_mean<empty_mean;
139 const double threshold=(mark_mean+empty_mean)*.5;
140 auto marked=[&](double v){return dark_marks?v<threshold:v>threshold;};
141 const size_t grid_valid=std::count_if(grid_marks.begin(),grid_marks.end(),marked);
142 const size_t ring_valid=std::count_if(ring_marks.begin(),ring_marks.end(),marked);
143 const size_t ray_valid=std::count_if(ray_marks.begin(),ray_marks.end(),marked);
144 const size_t empty_keys=std::count_if(empties.begin(),empties.end(),[&](double v){return !marked(v);});
145 if (grid_valid<3 || ring_valid<3 || ray_valid<2 || empty_keys<2) continue;
146
147 // Data sites are normalized between the solid Grid Key modules and the
148 // empty references, then each feature class (modules, ring dots, dial
149 // rays) is thresholded from its own samples. Dots and 0.15-unit rays
150 // use a support narrower than the stroke so it is not averaged away.
151 const double grid_mean=std::accumulate(grid_marks.begin(),grid_marks.end(),0.0)/grid_marks.size();
152 std::array<uint8_t,kCodewordBytes> cw{};
153 bool samples_ok=true;
154 const auto &nodes=data_nodes();
155 std::vector<double> level(kBits); std::array<std::vector<double>,3> classes;
156 const auto feature=[](const Node &n){ return n.dial ? 2 : n.polar ? 1 : 0; };
157 for (size_t bit=0;bit<kBits;++bit) {
158 const Node &n=nodes[bit]; double v;
159 if (n.dial) v=polar_sample(gray,width,height,stride,h,8.8,n.slot,kLocatorRaySpread); // outer band separates short from long ray
160 else if (n.polar) v=gray_sample(gray,width,height,stride,h,n.x,n.y,kLocatorDotSpread);
161 else v=gray_sample(gray,width,height,stride,h,n.x,n.y);
162 if (v<0) { samples_ok=false; break; }
163 level[bit]=(v-empty_mean)/(grid_mean-empty_mean);
164 classes[feature(n)].push_back(level[bit]);
165 }
166 if(!samples_ok) continue;
167 const std::array<double,3> cut={class_threshold(classes[0]),class_threshold(classes[1]),class_threshold(classes[2])};
168 for (size_t bit=0;bit<kBits;++bit)
169 if (level[bit]>cut[feature(nodes[bit])]) cw[bit/8]|=static_cast<uint8_t>(1u<<(7-(bit%8)));
170 std::array<uint8_t,CKEY_LOCATOR_PAYLOAD_BYTES> candidate{}; ckey_diagnostics local{};
171 const ckey_status status=ckey_decode_locator(cw.data(),cw.size(),candidate.data(),candidate.size(),&local);
172 if(status!=CKEY_OK) continue;
173 if(!found) {accepted=candidate; accepted_corrections=local.corrected_symbols; found=true;}
174 else if(candidate!=accepted) ambiguous=true;
175 }
176 if (!found || ambiguous) { set_status(diag,ambiguous?CKEY_INTEGRITY_FAILURE:CKEY_UNCORRECTABLE,ambiguous?"multiple orientations yielded different valid payloads":"no orientation passed calibration keys, Reed-Solomon, and CRC-32C"); return ambiguous?CKEY_INTEGRITY_FAILURE:CKEY_UNCORRECTABLE; }
177 std::memcpy(payload,accepted.data(),accepted.size());
178 set_status(diag,CKEY_OK,"decoded rectified locator image; caller supplied symbol quadrilateral");
179 if(diag) diag->corrected_symbols=accepted_corrections;
180 return CKEY_OK;
181}
182
183// The dense profiles deliberately keep the same projective contract as the
184// locator: callers locate a plausible outer boundary, while this core owns
185// orientation, sampling, ECC, and integrity. Their calibration samples use
186// actual payload-sized Grid Key marks, not a second oversized marker.
187ckey_status ckey_decode_text_gray8(const uint8_t *gray, size_t width, size_t height, size_t stride,
188 const double quad_xy[8], uint8_t *payload, size_t out_capacity,
189 size_t *out_payload_len, ckey_diagnostics *diag) {
190 if (!payload || !out_payload_len || !valid_image_args(gray, width, height, stride, quad_xy)) {
191 set_status(diag, CKEY_INVALID_ARGUMENT, "invalid Profile 2 image, quadrilateral, or output buffer"); return CKEY_INVALID_ARGUMENT;
192 }
193 constexpr double canonical[8]={0,0,29,0,29,29,0,29};
194 constexpr size_t bits=kTextCodewordBytes*8;
195 const auto &nodes=text_nodes(); const auto &white=text_whitening();
196 if (nodes.size() < bits) { set_status(diag,CKEY_INTERNAL_ERROR,"Profile 2 sampling layout unavailable"); return CKEY_INTERNAL_ERROR; }
197 bool found=false, ambiguous=false; std::vector<uint8_t> accepted; uint32_t accepted_corrections=0;
198 for (int reverse=0;reverse<2;++reverse) for (int start=0;start<4;++start) {
199 double dst[8],h[8];
200 for (int i=0;i<4;++i) { const int q=(start+(reverse?-i:i)+8)%4; dst[2*i]=quad_xy[2*q]; dst[2*i+1]=quad_xy[2*q+1]; }
201 if (!solve_homography(canonical,dst,h)) continue;
202 { const KeyFit fit=text_key_fit(gray,width,height,stride,h); if (!fit.valid || fit.evidence<kDenseKeyGate) continue; }
203 std::vector<double> marks, empties;
204 for (int r:{11,12}) for(int c:{2,3}) marks.push_back(gray_sample(gray,width,height,stride,h,c+.5,r+.5));
205 // Both points lie inside declared clearances, so they are never printed.
206 empties.push_back(gray_sample(gray,width,height,stride,h,1.5,11.5,kModuleSpread));
207 empties.push_back(gray_sample(gray,width,height,stride,h,5.5,14.5,kModuleSpread));
208 if (std::any_of(marks.begin(),marks.end(),[](double x){return x<0;}) || std::any_of(empties.begin(),empties.end(),[](double x){return x<0;})) continue;
209 const double mark_mean=std::accumulate(marks.begin(),marks.end(),0.0)/marks.size(), empty_mean=std::accumulate(empties.begin(),empties.end(),0.0)/empties.size();
210 if (std::abs(mark_mean-empty_mean)<18.0) continue;
211 const bool dark=mark_mean<empty_mean; const double threshold=(mark_mean+empty_mean)*.5;
212 const auto marked=[&](double x){return dark?x<threshold:x>threshold;};
213 if (std::count_if(marks.begin(),marks.end(),marked)<3 || std::count_if(empties.begin(),empties.end(),[&](double x){return !marked(x);})<1) continue;
214 (void)marked;
215 // Pass 1: normalized samples per feature class (Cartesian, dial, ring).
216 std::vector<double> level(bits); std::array<std::vector<double>,3> classes; bool usable=true;
217 const auto feature=[](const TextNode &n){ return n.cart ? 0 : n.dial ? 1 : 2; };
218 for (size_t bit=0;bit<bits;++bit) {
219 const auto &n=nodes[bit];
220 const double v=n.cart ? gray_sample(gray,width,height,stride,h,n.x,n.y,kModuleSpread)
221 : n.dial ? polar_sample_slots(gray,width,height,stride,h,kCenter,8.55,n.slot,144.0,kRaySpread)
222 : gray_sample(gray,width,height,stride,h,n.x,n.y,kDotSpread);
223 if (v<0) { usable=false; break; }
224 level[bit]=(v-empty_mean)/(mark_mean-empty_mean);
225 classes[feature(n)].push_back(level[bit]);
226 }
227 if (!usable) continue;
228 const std::array<double,3> cut={class_threshold(classes[0]),class_threshold(classes[1]),class_threshold(classes[2])};
229 // printed[i]: estimated printed value of site i. Fill sites are known.
230 std::vector<uint8_t> printed(nodes.size());
231 for (size_t i=0;i<nodes.size();++i) printed[i]=i<bits ? (level[i]>cut[feature(nodes[i])]) : white[i];
232 static const std::vector<int> ring_grid=[]{
233 std::vector<int> grid(kTextRings*kTextSlots,-1); const auto &all=text_nodes();
234 for (size_t i=0;i<all.size();++i) if (!all[i].cart && !all[i].dial) grid[static_cast<size_t>(all[i].ring)*kTextSlots+all[i].slot]=static_cast<int>(i);
235 return grid; }();
236 refine_ring_context(ring_grid,kTextRings,kTextSlots,bits,white,level,cut[2],printed);
237 std::array<uint8_t,kTextCodewordBytes> cw{};
238 for (size_t bit=0;bit<bits;++bit)
239 if ((printed[bit]!=0) != (white[bit]!=0)) cw[bit/8]|=static_cast<uint8_t>(1u<<(7-(bit%8)));
240 std::array<uint8_t,CKEY_TEXT_PAYLOAD_MAX_BYTES> candidate{}; size_t candidate_len=0; ckey_diagnostics local{};
241 if (ckey_decode_text(cw.data(),cw.size(),candidate.data(),candidate.size(),&candidate_len,&local)!=CKEY_OK) continue;
242 const std::vector<uint8_t> result(candidate.begin(),candidate.begin()+static_cast<std::ptrdiff_t>(candidate_len));
243 if (!found) { accepted=result; accepted_corrections=local.corrected_symbols; found=true; }
244 else if (result!=accepted) ambiguous=true;
245 }
246 if (!found || ambiguous) { set_status(diag,ambiguous?CKEY_INTEGRITY_FAILURE:CKEY_UNCORRECTABLE,ambiguous?"multiple Profile 2 orientations passed integrity":"no Profile 2 orientation passed calibration, Reed-Solomon, and CRC"); return ambiguous?CKEY_INTEGRITY_FAILURE:CKEY_UNCORRECTABLE; }
247 if (out_capacity<accepted.size()) { set_status(diag,CKEY_INSUFFICIENT_BUFFER,"Profile 2 output buffer is too small"); return CKEY_INSUFFICIENT_BUFFER; }
248 std::memcpy(payload,accepted.data(),accepted.size()); *out_payload_len=accepted.size();
249 set_status(diag,CKEY_OK,"decoded rectified Profile 2 image"); if (diag) diag->corrected_symbols=accepted_corrections;
250 return CKEY_OK;
251}
252
253ckey_status ckey_decode_extended_gray8(const uint8_t *gray, size_t width, size_t height, size_t stride,
254 const double quad_xy[8], uint8_t *payload, size_t out_capacity,
255 size_t *out_payload_len, ckey_diagnostics *diag) {
256 if (!payload || !out_payload_len || !valid_image_args(gray, width, height, stride, quad_xy)) {
257 set_status(diag, CKEY_INVALID_ARGUMENT, "invalid Profile 3 image, quadrilateral, or output buffer"); return CKEY_INVALID_ARGUMENT;
258 }
259 constexpr double canonical[8]={0,0,116,0,116,116,0,116};
260 const auto &nodes=extended_nodes(); const auto &white=extended_whitening();
261 if (nodes.size()<kExtendedBits) { set_status(diag,CKEY_INTERNAL_ERROR,"Profile 3 sampling layout unavailable"); return CKEY_INTERNAL_ERROR; }
262 bool found=false, ambiguous=false; std::vector<uint8_t> accepted; uint32_t accepted_corrections=0;
263 for (int reverse=0;reverse<2;++reverse) for (int start=0;start<4;++start) {
264 double dst[8],h[8];
265 for (int i=0;i<4;++i) { const int q=(start+(reverse?-i:i)+8)%4; dst[2*i]=quad_xy[2*q]; dst[2*i+1]=quad_xy[2*q+1]; }
266 if (!solve_homography(canonical,dst,h)) continue;
267 { const KeyFit fit=extended_key_fit(gray,width,height,stride,h); if (!fit.valid || fit.evidence<kDenseKeyGate) continue; }
268 std::vector<double> marks, empties;
269 for (int r=0;r<2;++r) for (int c=0;c<2;++c) marks.push_back(gray_sample(gray,width,height,stride,h,8.25+c*.5,45.25+r*.5,kModuleSpread));
270 empties.push_back(gray_sample(gray,width,height,stride,h,kExtendedGridClearX,kExtendedGridClearY,kModuleSpread));
271 empties.push_back(gray_sample(gray,width,height,stride,h,58.0,4.5,kModuleSpread));
272 if (std::any_of(marks.begin(),marks.end(),[](double x){return x<0;}) || std::any_of(empties.begin(),empties.end(),[](double x){return x<0;})) continue;
273 const double mark_mean=std::accumulate(marks.begin(),marks.end(),0.0)/marks.size(), empty_mean=std::accumulate(empties.begin(),empties.end(),0.0)/empties.size();
274 if (std::abs(mark_mean-empty_mean)<14.0) continue;
275 const bool dark=mark_mean<empty_mean; const double threshold=(mark_mean+empty_mean)*.5;
276 const auto marked=[&](double x){return dark?x<threshold:x>threshold;};
277 if (std::count_if(marks.begin(),marks.end(),marked)<3) continue;
278 (void)marked;
279 const auto normalized=[&](double v){ return (v-empty_mean)/(mark_mean-empty_mean); };
280 // Pass 1: normalized samples for Cartesian and ring sites; per-ray
281 // length probes and integrated width coverage.
282 std::vector<double> level(kExtendedBits, 0.0); std::vector<double> cart_values, ring_values, probe_values, width_values;
283 std::array<std::array<double,4>,kExtendedRaySlots> probes{}; std::array<double,kExtendedRaySlots> coverage{};
284 std::array<bool,kExtendedRaySlots> ray_ready{}; bool usable=true;
285 for (size_t bit=0;bit<kExtendedBits && usable;++bit) {
286 const auto &n=nodes[bit];
287 if (n.kind!=ExtendedKind::ray) {
288 const double v=gray_sample(gray,width,height,stride,h,n.x,n.y,n.kind==ExtendedKind::cart?kModuleSpread:kDotSpread);
289 if (v<0) { usable=false; break; }
290 level[bit]=normalized(v);
291 (n.kind==ExtendedKind::cart?cart_values:ring_values).push_back(level[bit]);
292 continue;
293 }
294 if (ray_ready[n.slot]) continue;
295 for (int l=0;l<4;++l) {
296 const double v=polar_sample_slots(gray,width,height,stride,h,58.0,26.30+.80+.85*l-.16,n.slot,576.0,kRaySpread);
297 if (v<0) { usable=false; break; }
298 probes[n.slot][l]=normalized(v); probe_values.push_back(probes[n.slot][l]);
299 }
300 // Width is integrated ink coverage across the ray, which blur
301 // conserves: thin (0.16) covers ~0.6 of the +/-0.12 window and
302 // wide (0.28) covers nearly all of it.
303 const double a=kTau*n.slot/576.0, r=26.75, ux=std::sin(a), uy=-std::cos(a);
304 double sum=0;
305 for (int k=-3;k<=3 && usable;++k) {
306 const double t=.04*k;
307 const double v=gray_sample(gray,width,height,stride,h,58.0+r*ux-t*uy,58.0+r*uy+t*ux,0.0);
308 if (v<0) { usable=false; break; }
309 sum+=std::clamp(normalized(v),0.0,1.0);
310 }
311 coverage[n.slot]=sum/7.0; width_values.push_back(coverage[n.slot]); ray_ready[n.slot]=true;
312 }
313 if (!usable) continue;
314 const double cart_cut=class_threshold(cart_values), ring_cut=class_threshold(ring_values);
315 const double probe_cut=class_threshold(probe_values), width_cut=class_threshold(width_values);
316 std::vector<uint8_t> ring_printed(nodes.size());
317 for (size_t i=0;i<nodes.size();++i)
318 ring_printed[i]=i<kExtendedBits ? (nodes[i].kind==ExtendedKind::ring && level[i]>ring_cut) : white[i];
319 static const std::vector<int> ring_grid=[]{
320 std::vector<int> grid(kExtendedRings*kExtendedRingSlots,-1); const auto &all=extended_nodes();
321 for (size_t i=0;i<all.size();++i) if (all[i].kind==ExtendedKind::ring) grid[static_cast<size_t>(all[i].ring)*kExtendedRingSlots+all[i].slot]=static_cast<int>(i);
322 return grid; }();
323 refine_ring_context(ring_grid,kExtendedRings,kExtendedRingSlots,kExtendedBits,white,level,ring_cut,ring_printed);
324 std::array<uint8_t,kExtendedCodewordBytes> cw{};
325 for (size_t bit=0;bit<kExtendedBits;++bit) {
326 const auto &n=nodes[bit];
327 bool printed;
328 if (n.kind==ExtendedKind::cart) printed=level[bit]>cart_cut;
329 else if (n.kind==ExtendedKind::ring) printed=ring_printed[bit]!=0;
330 else if (n.component==2) printed=coverage[n.slot]>width_cut;
331 else {
332 int length=0;
333 for (int l=0;l<4;++l) if (probes[n.slot][l]>probe_cut) length=l;
334 const int gray_code=length^(length>>1);
335 printed=n.component==0 ? ((gray_code>>1)&1)!=0 : (gray_code&1)!=0;
336 }
337 if (printed != (white[bit]!=0)) cw[bit/8]|=static_cast<uint8_t>(1u<<(7-(bit%8)));
338 }
339 std::array<uint8_t,CKEY_EXTENDED_PAYLOAD_MAX_BYTES> candidate{}; size_t candidate_len=0; ckey_diagnostics local{};
340 if (ckey_decode_extended(cw.data(),cw.size(),candidate.data(),candidate.size(),&candidate_len,&local)!=CKEY_OK) continue;
341 const std::vector<uint8_t> result(candidate.begin(),candidate.begin()+static_cast<std::ptrdiff_t>(candidate_len));
342 if (!found) { accepted=result; accepted_corrections=local.corrected_symbols; found=true; } else if (result!=accepted) ambiguous=true;
343 }
344 if (!found || ambiguous) { set_status(diag,ambiguous?CKEY_INTEGRITY_FAILURE:CKEY_UNCORRECTABLE,ambiguous?"multiple Profile 3 orientations passed integrity":"no Profile 3 orientation passed calibration, Reed-Solomon, and CRC"); return ambiguous?CKEY_INTEGRITY_FAILURE:CKEY_UNCORRECTABLE; }
345 if (out_capacity<accepted.size()) { set_status(diag,CKEY_INSUFFICIENT_BUFFER,"Profile 3 output buffer is too small"); return CKEY_INSUFFICIENT_BUFFER; }
346 std::memcpy(payload,accepted.data(),accepted.size()); *out_payload_len=accepted.size();
347 set_status(diag,CKEY_OK,"decoded rectified Profile 3 image"); if (diag) diag->corrected_symbols=accepted_corrections;
348 return CKEY_OK;
349}
350
351double ckey_calibration_score_scalable_gray8(const uint8_t *gray, size_t width, size_t height, size_t stride,
352 const double quad_xy[8], uint32_t *out_version) {
353 if (!valid_image_args(gray, width, height, stride, quad_xy)) return -1.0;
354 double score = 0; const int v = best_scalable_version(gray, width, height, stride, quad_xy, score);
355 if (out_version) *out_version = static_cast<uint32_t>(v);
356 return score;
357}
358
359ckey_status ckey_decode_scalable_gray8(const uint8_t *gray, size_t width, size_t height, size_t stride, const double quad_xy[8],
360 uint8_t *out_payload, size_t out_capacity, size_t *out_payload_len, uint32_t *out_version,
361 uint32_t *out_layout, ckey_diagnostics *diag) {
362 if (!out_payload || !out_payload_len || !valid_image_args(gray, width, height, stride, quad_xy)) {
363 set_status(diag, CKEY_INVALID_ARGUMENT, "invalid Profile 0 image, quadrilateral, or output buffer"); return CKEY_INVALID_ARGUMENT;
364 }
365 // 1. The skeleton gates orientation. 2. Each version's keys are tested at
366 // that orientation; only agreeing versions are fully sampled, best
367 // first. 3. RS, the frame's version byte, and CRC-32C decide.
368 struct Attempt { double score; int version, family; double h[8]; };
369 std::vector<Attempt> attempts;
370 for (int reverse = 0; reverse < 2; ++reverse) for (int start = 0; start < 4; ++start) {
371 double h[8]; bool ok; orientation_homography(quad_xy, reverse, start, h, ok); if (!ok) continue;
372 const KeyFit skeleton = scalable_skeleton_fit(gray, width, height, stride, h);
373 if (!skeleton.valid || skeleton.evidence < kDenseKeyGate || skeleton.contrast < 14.0) continue;
374 for (int f = 0; f < kScalableFamilies; ++f) for (int v = 1; v <= kScalableVersions; ++v) {
375 const KeyFit keys = scalable_version_fit(scalable_layout(v, f), gray, width, height, stride, h);
376 if (!keys.valid || keys.evidence < 0.85) continue;
377 Attempt a{keys.contrast * keys.evidence * keys.evidence, v, f, {}}; std::copy(h, h + 8, a.h);
378 attempts.push_back(a);
379 }
380 }
381 std::sort(attempts.begin(), attempts.end(), [](const Attempt &a, const Attempt &b) { return a.score > b.score; });
382 if (attempts.size() > 8) attempts.resize(8);
383 std::vector<uint8_t> candidate(65535);
384 for (const auto &attempt : attempts) {
385 const ScalableLayout &L = scalable_layout(attempt.version, attempt.family); const double *h = attempt.h; const double p = L.pitch;
386 // References: the Grid Key (printed) and two never-printed points.
387 std::vector<double> marks, empties;
388 for (int r = 0; r < 2; ++r) for (int c = 0; c < 2; ++c)
389 marks.push_back(gray_sample(gray, width, height, stride, h, (L.col0 + c + .5) * p, (L.row0 + r + .5) * p, 0.12 * p));
390 empties.push_back(gray_sample(gray, width, height, stride, h, (L.col0 - 1 + .5) * p, (L.row0 + .5) * p, 0.12 * p));
391 empties.push_back(gray_sample(gray, width, height, stride, h, 14.5, 1.0, 0.05));
392 if (std::any_of(marks.begin(), marks.end(), [](double x) { return x < 0; }) || std::any_of(empties.begin(), empties.end(), [](double x) { return x < 0; })) continue;
393 const double mark_mean = std::accumulate(marks.begin(), marks.end(), 0.0) / marks.size();
394 const double empty_mean = std::accumulate(empties.begin(), empties.end(), 0.0) / empties.size();
395 if (std::abs(mark_mean - empty_mean) < 14.0) continue;
396 const size_t bits = L.codeword_bytes * 8;
397 std::vector<double> level(bits); std::array<std::vector<double>, 3> classes; bool usable = true;
398 for (size_t i = 0; i < bits && usable; ++i) {
399 const auto &n = L.nodes[i];
400 const double v = n.kind == 0 ? gray_sample(gray, width, height, stride, h, n.x, n.y, 0.12 * p)
401 : n.kind == 1 ? polar_sample_slots(gray, width, height, stride, h, kCenter, kScalableDialInner + L.dial_sample, n.slot, L.dial_slots, 0.04 * p)
402 : gray_sample(gray, width, height, stride, h, n.x, n.y, 0.25 * L.dot);
403 if (v < 0) { usable = false; break; }
404 level[i] = (v - empty_mean) / (mark_mean - empty_mean);
405 classes[n.kind].push_back(level[i]);
406 }
407 if (!usable) continue;
408 const std::array<double, 3> cut = {class_threshold(classes[0]), class_threshold(classes[1]), class_threshold(classes[2])};
409 std::vector<uint8_t> printed(L.nodes.size());
410 for (size_t i = 0; i < L.nodes.size(); ++i) printed[i] = i < bits ? (level[i] > cut[L.nodes[i].kind]) : L.white[i];
411 refine_ring_context_rings(L.ring_grid, L.ring_slots, L.ring_stride, bits, L.white, level, cut[2], printed);
412 std::vector<uint8_t> cw(L.codeword_bytes, 0);
413 for (size_t i = 0; i < bits; ++i) if ((printed[i] != 0) != (L.white[i] != 0)) cw[i / 8] |= static_cast<uint8_t>(1u << (7 - i % 8));
414 size_t length = 0; ckey_diagnostics local{};
415 if (ckey_decode_scalable(cw.data(), cw.size(), static_cast<uint32_t>(attempt.version), static_cast<ckey_scalable_layout>(attempt.family),
416 candidate.data(), candidate.size(), &length, &local) != CKEY_OK) continue;
417 if (out_capacity < length) { set_status(diag, CKEY_INSUFFICIENT_BUFFER, "Profile 0 output buffer is too small"); return CKEY_INSUFFICIENT_BUFFER; }
418 std::memcpy(out_payload, candidate.data(), length); *out_payload_len = length;
419 if (out_version) *out_version = static_cast<uint32_t>(attempt.version);
420 if (out_layout) *out_layout = static_cast<uint32_t>(attempt.family);
421 set_status(diag, CKEY_OK, local.message); if (diag) diag->corrected_symbols = local.corrected_symbols;
422 return CKEY_OK;
423 }
424 set_status(diag, CKEY_UNCORRECTABLE, attempts.empty() ? "no Profile 0 version's keys matched" : "no Profile 0 version passed Reed-Solomon and CRC");
425 return CKEY_UNCORRECTABLE;
426}
427
428size_t ckey_find_borders_gray8(const uint8_t *gray, size_t width, size_t height, size_t stride,
429 double *out_quads, size_t max_quads) {
430 if (!gray || !out_quads || max_quads == 0 || width < 16 || height < 16 || stride < width ||
431 height > std::numeric_limits<size_t>::max() / stride) return 0;
432 // About 960 x 540 samples at any capture size (step 2 at 1080p, 4 at 4K).
433 const size_t step = std::max<size_t>(2, 2 * std::max(width, height) / 1920);
434 const size_t sw = (width + step - 1) / step, sh = (height + step - 1) / step;
435 const size_t min_side = std::min(width, height);
436 struct Candidate { std::array<double, 8> quad; double rank; };
437 std::vector<Candidate> candidates;
438 std::vector<uint8_t> visited(sw * sh);
439 std::vector<size_t> queue;
440 for (int polarity = 0; polarity < 2; ++polarity) {
441 const bool dark = polarity == 1;
442 const uint8_t t = frame_threshold(gray, width, height, stride, dark);
443 const auto is_mark = [&](size_t sx, size_t sy) {
444 const uint8_t v = gray[std::min(sy * step, height - 1) * stride + std::min(sx * step, width - 1)];
445 return dark ? v <= t : v >= t;
446 };
447 std::fill(visited.begin(), visited.end(), 0);
448 for (size_t sy = 0; sy < sh; ++sy) for (size_t sx = 0; sx < sw; ++sx) {
449 const size_t start = sy * sw + sx;
450 if (visited[start] || !is_mark(sx, sy)) continue;
451 visited[start] = 1; queue.assign(1, start);
452 size_t min_x = sx, max_x = sx, min_y = sy, max_y = sy;
453 for (size_t head = 0; head < queue.size(); ++head) {
454 const size_t x = queue[head] % sw, y = queue[head] / sw;
455 min_x = std::min(min_x, x); max_x = std::max(max_x, x); min_y = std::min(min_y, y); max_y = std::max(max_y, y);
456 for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) {
457 if (!dx && !dy) continue;
458 const long nx = static_cast<long>(x) + dx, ny = static_cast<long>(y) + dy;
459 if (nx < 0 || ny < 0 || nx >= static_cast<long>(sw) || ny >= static_cast<long>(sh)) continue;
460 const size_t n = static_cast<size_t>(ny) * sw + static_cast<size_t>(nx);
461 if (!visited[n] && is_mark(static_cast<size_t>(nx), static_cast<size_t>(ny))) { visited[n] = 1; queue.push_back(n); }
462 }
463 }
464 const size_t box_w = (max_x - min_x + 1) * step, box_h = (max_y - min_y + 1) * step, side = std::min(box_w, box_h);
465 // The whole border must be in view: a component touching the frame
466 // edge is clipped (or is a display bezel). This replaces a fixed
467 // fraction-of-frame cap that rejected large rotated symbols.
468 if (queue.size() < 24 || side < 20 * step || min_x == 0 || min_y == 0 || max_x + 1 >= sw || max_y + 1 >= sh) continue;
469 const double aspect = static_cast<double>(box_w) / box_h;
470 // A rotated square's axis-aligned box stays square; a filled
471 // panel or blob fails the fill test.
472 const double fill = static_cast<double>(queue.size()) / static_cast<double>((max_x - min_x + 1) * (max_y - min_y + 1));
473 if (aspect <= 0.72 || aspect >= 1.38 || fill >= 0.38) continue;
474 std::vector<Point2> points; points.reserve(queue.size());
475 for (size_t index : queue) points.push_back({static_cast<double>((index % sw) * step), static_cast<double>((index / sw) * step)});
476 Candidate c{};
477 if (!fit_border_quad(points, static_cast<double>(step), c.quad)) continue;
478 c.rank = border_rank(std::abs(std::log(aspect)), fill, static_cast<double>(side) / min_side);
479 candidates.push_back(c);
480 }
481 }
482 std::stable_sort(candidates.begin(), candidates.end(), [](const Candidate &a, const Candidate &b) { return a.rank > b.rank; });
483 // Every profile shares one skeleton in normalized coordinates (frame
484 // stroke, finder arcs, orientation split; Profile 3 is the same shape at
485 // 4x). Re-rank the leading shape candidates by how well that skeleton
486 // fits, so a symbol's own interior components (dense payload clusters,
487 // rings) cannot crowd its true border out of the returned set.
488 if (candidates.size() > 24) candidates.resize(24);
489 for (auto &c : candidates) {
490 double q[8]; double cx = 0, cy = 0;
491 for (int i = 0; i < 4; ++i) { cx += c.quad[2 * i] / 4; cy += c.quad[2 * i + 1] / 4; }
492 for (int i = 0; i < 4; ++i) { q[2 * i] = cx + (c.quad[2 * i] - cx) * kBorderToField; q[2 * i + 1] = cy + (c.quad[2 * i + 1] - cy) * kBorderToField; }
493 c.rank = valid_quad(q) ? scalable_skeleton_score(gray, width, height, stride, q) + 1e-3 * c.rank : -1;
494 }
495 std::stable_sort(candidates.begin(), candidates.end(), [](const Candidate &a, const Candidate &b) { return a.rank > b.rank; });
496 const size_t count = std::min(max_quads, candidates.size());
497 for (size_t i = 0; i < count; ++i) std::copy(candidates[i].quad.begin(), candidates[i].quad.end(), out_quads + 8 * i);
498 return count;
499}
500
501namespace {
502// Scores a refined quadrilateral by the field it claims to be. It calls the
503// scan library's calibration scores, so it lives here rather than in the
504// shared internals, which the codec and render libraries also compile.
505double field_score(int field, const uint8_t *gray, size_t width, size_t height, size_t stride, const double q[8]) {
506 if (field == CKEY_FIELD_SCALABLE) return frame_edge_score(gray, width, height, stride, q, 4.0);
507 if (field == CKEY_FIELD_LOCATOR) return ckey_calibration_score_gray8(gray, width, height, stride, q);
508 if (field == CKEY_FIELD_TEXT) return ckey_calibration_score_text_gray8(gray, width, height, stride, q);
509 return ckey_calibration_score_extended_gray8(gray, width, height, stride, q);
510}
511} // namespace
512
513double ckey_refine_quad_gray8(const uint8_t *gray, size_t width, size_t height, size_t stride,
514 const double border_quad[8], ckey_field field, double out_quad[8]) {
515 if (!out_quad || (field != CKEY_FIELD_SCALABLE && field != CKEY_FIELD_LOCATOR && field != CKEY_FIELD_TEXT && field != CKEY_FIELD_EXTENDED) ||
516 !valid_image_args(gray, width, height, stride, border_quad)) return -1.0;
517 double cx = 0, cy = 0; for (int i = 0; i < 4; ++i) { cx += border_quad[2 * i] / 4; cy += border_quad[2 * i + 1] / 4; }
518 const double side = (std::hypot(border_quad[2] - border_quad[0], border_quad[3] - border_quad[1]) +
519 std::hypot(border_quad[6] - border_quad[0], border_quad[7] - border_quad[1])) / 2;
520 const auto similar = [&](double scale, double angle, double ox, double oy, double q[8]) {
521 const double c = std::cos(angle), s = std::sin(angle);
522 for (int i = 0; i < 4; ++i) {
523 const double dx = border_quad[2 * i] - cx, dy = border_quad[2 * i + 1] - cy;
524 q[2 * i] = cx + ox + scale * (c * dx - s * dy); q[2 * i + 1] = cy + oy + scale * (s * dx + c * dy);
525 }
526 };
527 // Coordinate descent over (scale, rotation, offset), then per-corner
528 // projective moves. Steps halve each round, so it converges at any pixel
529 // scale without a symbol-size table.
530 // Objective at a given search step (pixels). Profile 0 aligns on the
531 // frame's edges with a sample separation that follows the step.
532 const auto objective = [&](const double q[8], double step_px) {
533 return field == CKEY_FIELD_SCALABLE ? frame_edge_score(gray, width, height, stride, q, std::clamp(step_px, 0.5, 6.0))
534 : field_score(field, gray, width, height, stride, q);
535 };
536 double scale = kBorderToField, angle = 0, ox = 0, oy = 0;
537 double best[8]; similar(scale, angle, ox, oy, best);
538 double scale_step = 0.03, angle_step = 2.0 * kTau / 360.0, offset_step = side * 0.02;
539 double best_score = objective(best, offset_step);
540 for (int round = 0; round < 6; ++round) {
541 best_score = objective(best, offset_step); // re-evaluate at this step's resolution
542 bool improved = true;
543 while (improved) {
544 improved = false;
545 const double moves[8][4] = {{scale_step, 0, 0, 0}, {-scale_step, 0, 0, 0}, {0, angle_step, 0, 0}, {0, -angle_step, 0, 0},
546 {0, 0, offset_step, 0}, {0, 0, -offset_step, 0}, {0, 0, 0, offset_step}, {0, 0, 0, -offset_step}};
547 for (const auto &m : moves) {
548 double q[8]; similar(scale + m[0], angle + m[1], ox + m[2], oy + m[3], q);
549 const double score = objective(q, offset_step);
550 if (score > best_score) { best_score = score; scale += m[0]; angle += m[1]; ox += m[2]; oy += m[3]; std::copy(q, q + 8, best); improved = true; }
551 }
552 }
553 scale_step /= 2; angle_step /= 2; offset_step /= 2;
554 }
555 if (field == CKEY_FIELD_SCALABLE) {
556 // Profile 0: stop here unless the CKey skeleton (frame, finders,
557 // orientation split) agrees at some orientation, so a non-symbol
558 // candidate costs only the similarity alignment above.
559 bool skeleton_agrees = false;
560 for (int reverse = 0; reverse < 2 && !skeleton_agrees; ++reverse) for (int start = 0; start < 4 && !skeleton_agrees; ++start) {
561 double h[8]; bool ok; orientation_homography(best, reverse, start, h, ok); if (!ok) continue;
562 const KeyFit fit = scalable_skeleton_fit(gray, width, height, stride, h);
563 skeleton_agrees = fit.valid && fit.evidence >= kDenseKeyGate && fit.contrast >= 14.0;
564 }
565 if (!skeleton_agrees) { std::copy(best, best + 8, out_quad); return 0.0; }
566 }
567 // Per-corner projective moves: repeat at each step while they improve
568 // (bounded), down to sub-pixel steps, so keystone displacements of a few
569 // percent of the side are fully recovered.
570 for (double corner_step = side * 0.02; corner_step > 0.3; corner_step /= 2) {
571 best_score = objective(best, corner_step);
572 for (int pass = 0; pass < 8; ++pass) {
573 bool improved = false;
574 for (int corner = 0; corner < 4; ++corner) for (const auto &[dx, dy] : {std::pair{1.0, 0.0}, {-1.0, 0.0}, {0.0, 1.0}, {0.0, -1.0}}) {
575 double q[8]; std::copy(best, best + 8, q);
576 q[2 * corner] += dx * corner_step; q[2 * corner + 1] += dy * corner_step;
577 const double score = objective(q, corner_step);
578 if (score > best_score) { best_score = score; std::copy(q, q + 8, best); improved = true; }
579 }
580 if (!improved) break;
581 }
582 }
583 if (field == CKEY_FIELD_SCALABLE) {
584 // Profile 0: the skeleton aligned the field; now pick the version whose
585 // keys fit best and fine-tune against them. They sit at the version's
586 // own module and slot pitch, so they pin the scale far more tightly
587 // than the frame and finders can at high densities.
588 double version_score = 0; int family = 0;
589 const int v = best_scalable_version(gray, width, height, stride, best, version_score, &family);
590 if (v > 0) {
591 const ScalableLayout &L = scalable_layout(v, family);
592 const double pitch_px = side / L.grid;
593 double score = scalable_version_score(L, gray, width, height, stride, best);
594 for (double step = pitch_px * 0.5; step > pitch_px * 0.02; step /= 2) {
595 bool improved = true;
596 while (improved) {
597 improved = false;
598 for (int corner = 0; corner < 4; ++corner) for (const auto &[dx, dy] : {std::pair{1.0, 0.0}, {-1.0, 0.0}, {0.0, 1.0}, {0.0, -1.0}}) {
599 double q[8]; std::copy(best, best + 8, q);
600 q[2 * corner] += dx * step; q[2 * corner + 1] += dy * step;
601 const double candidate = scalable_version_score(L, gray, width, height, stride, q);
602 if (candidate > score) { score = candidate; std::copy(q, q + 8, best); improved = true; }
603 }
604 }
605 }
606 best_score = score;
607 }
608 }
609 std::copy(best, best + 8, out_quad);
610 return best_score;
611}
612
613} // extern "C"