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
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 | |
| 3 | extern "C" { |
| 4 | |
| 5 | double 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 | |
| 65 | double 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 | |
| 83 | double 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 | |
| 98 | ckey_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. |
| 187 | ckey_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 | |
| 253 | ckey_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 | |
| 351 | double 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 | |
| 359 | ckey_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 | |
| 428 | size_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 | |
| 501 | namespace { |
| 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. |
| 505 | double 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 | |
| 513 | double 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" |