Skip to content

Commit 9b6e4b0

Browse files
kalwaltclaude
andcommitted
feat(diagnostic): add dump_patt + diff_patt for issue #103
Adds a diagnostic harness to compare ar_patt_get_image output between the Rust port and the WebARKitLib C reference, byte-for-byte. - benchmarks/c_benchmark/dump_patt.c: sibling C dumper (main.c untouched), runs detection once and writes ext_patt + meta to benchmarks/data/c_ext_patt.{bin,meta.txt}. - crates/core/examples/dump_patt.rs: Rust counterpart, same shape, writes rs_ext_patt.{bin,meta.txt}. - crates/core/examples/diff_patt.rs: byte-comparison reporter with divergent indices, max abs delta, and zero-variance warnings. - benchmarks/data/README.md: end-to-end recipe. - docs/issue-103-fix-plan.md: full design — understanding lock, decision log (11 entries), risks, and the smoke-run finding that pivoted PR-B (the bug is in simple.rs's frame contract, not in the library; Rust dumper produces cf=0.892 on the same inputs). The library extraction path is verified correct on the active MONO-source x COLOR-detection path; PR-B will fix the example and add an early-failure check in ar_detect_marker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 65bb756 commit 9b6e4b0

7 files changed

Lines changed: 1091 additions & 0 deletions

File tree

benchmarks/c_benchmark/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,18 @@ elseif(NOT WIN32)
187187
target_link_libraries(c_benchmark m)
188188
endif()
189189

190+
# ── Pattern-extraction diagnostic dumper (issue #103) ──
191+
# Sibling of c_benchmark; runs detection once, re-extracts the matched patch
192+
# via arPattGetImage, and dumps it for byte-comparison with the Rust port.
193+
# See ../data/README.md and ../../docs/issue-103-fix-plan.md.
194+
add_executable(dump_patt ${AR_CORE_SOURCES} "dump_patt.c")
195+
target_compile_definitions(dump_patt PRIVATE ${COMMON_DEFS})
196+
if(WIN32 AND MSVC)
197+
target_compile_options(dump_patt PRIVATE /W3 /Z7)
198+
elseif(NOT WIN32)
199+
target_link_libraries(dump_patt m)
200+
endif()
201+
190202
# ── KPM targets (fixture generator + benchmark) ──
191203
# These require libjpeg-9f (bootstrapped) and are NOT built by default.
192204
# Build manually with: cmake --build . --target kpm_dump_fixtures

benchmarks/c_benchmark/dump_patt.c

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/*
2+
* dump_patt.c
3+
* WebARKitLib-rs
4+
*
5+
* This file is part of WebARKitLib-rs - WebARKit.
6+
*
7+
* WebARKitLib-rs is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Lesser General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* WebARKitLib-rs is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Lesser General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with WebARKitLib-rs. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
* As a special exception, the copyright holders of this library give you
21+
* permission to link this library with independent modules to produce an
22+
* executable, regardless of the license terms of these independent modules,
23+
* and to copy and distribute the resulting executable under terms of your
24+
* choice, provided that you also meet, for each linked independent module,
25+
* the terms and conditions of the license of that module. An independent
26+
* module is a module which is neither derived from nor based on this
27+
* library. If you modify this library, you may extend this exception to
28+
* your version of the library, but you are not obligated to do so. If you
29+
* do not wish to do so, delete this exception statement from your version.
30+
*
31+
* Copyright 2026 WebARKit.
32+
*
33+
* Author(s): Walter Perdan @kalwalt https://github.com/kalwalt
34+
*
35+
*/
36+
37+
/*
38+
* Diagnostic harness for issue #103 (low CF from template matching).
39+
*
40+
* Runs `arDetectMarker` once on a luma raw image, then re-extracts the same
41+
* pattern patch via `arPattGetImage` using the contour data preserved in
42+
* `arHandle->markerInfo2[0]`. The extracted patch is dumped to disk as raw
43+
* bytes for byte-level comparison with the Rust port.
44+
*
45+
* NOTE: this is a sibling of `main.c`; it does NOT modify the timing
46+
* benchmark. Outputs:
47+
* - <out_dir>/c_ext_patt.bin raw ext_patt bytes (patt_size^2 * channels)
48+
* - <out_dir>/c_ext_patt_meta.txt patt_size, channels, mode, vertices, cf, id, dir
49+
*/
50+
51+
#include <AR/ar.h>
52+
#include <stdio.h>
53+
#include <stdlib.h>
54+
#include <string.h>
55+
56+
/* Compatibility helpers required by some WebARKitLib builds. */
57+
char *cat(const char *file, size_t *bufSize_p) {
58+
FILE *fp;
59+
size_t len;
60+
char *s;
61+
if (!file)
62+
return NULL;
63+
fp = fopen(file, "rb");
64+
if (!fp)
65+
return NULL;
66+
fseek(fp, 0, SEEK_END);
67+
len = ftell(fp);
68+
fseek(fp, 0, SEEK_SET);
69+
if (!(s = (char *)malloc(len + 1))) {
70+
fclose(fp);
71+
return NULL;
72+
}
73+
if (fread(s, len, 1, fp) < 1 && len > 0) {
74+
free(s);
75+
fclose(fp);
76+
return NULL;
77+
}
78+
s[len] = '\0';
79+
fclose(fp);
80+
if (bufSize_p)
81+
*bufSize_p = len + 1;
82+
return s;
83+
}
84+
int test_d(const char *dir) { (void)dir; return 0; }
85+
int mkdir_p(const char *path) { (void)path; return 0; }
86+
87+
static const char *pixfmt_name(AR_PIXEL_FORMAT f) {
88+
switch (f) {
89+
case AR_PIXEL_FORMAT_RGB: return "AR_PIXEL_FORMAT_RGB";
90+
case AR_PIXEL_FORMAT_BGR: return "AR_PIXEL_FORMAT_BGR";
91+
case AR_PIXEL_FORMAT_RGBA: return "AR_PIXEL_FORMAT_RGBA";
92+
case AR_PIXEL_FORMAT_BGRA: return "AR_PIXEL_FORMAT_BGRA";
93+
case AR_PIXEL_FORMAT_ABGR: return "AR_PIXEL_FORMAT_ABGR";
94+
case AR_PIXEL_FORMAT_ARGB: return "AR_PIXEL_FORMAT_ARGB";
95+
case AR_PIXEL_FORMAT_MONO: return "AR_PIXEL_FORMAT_MONO";
96+
default: return "<unknown>";
97+
}
98+
}
99+
100+
static const char *mode_name(int m) {
101+
switch (m) {
102+
case AR_TEMPLATE_MATCHING_COLOR: return "AR_TEMPLATE_MATCHING_COLOR";
103+
case AR_TEMPLATE_MATCHING_MONO: return "AR_TEMPLATE_MATCHING_MONO";
104+
case AR_MATRIX_CODE_DETECTION: return "AR_MATRIX_CODE_DETECTION";
105+
case AR_TEMPLATE_MATCHING_COLOR_AND_MATRIX: return "AR_TEMPLATE_MATCHING_COLOR_AND_MATRIX";
106+
case AR_TEMPLATE_MATCHING_MONO_AND_MATRIX: return "AR_TEMPLATE_MATCHING_MONO_AND_MATRIX";
107+
default: return "<unknown>";
108+
}
109+
}
110+
111+
int main(int argc, char *argv[]) {
112+
if (argc < 5) {
113+
printf("Usage: %s <camera_para.dat> <patt.hiro> <luma.raw> <out_dir> [width] [height]\n",
114+
argv[0]);
115+
printf(" out_dir defaults expected: ../data\n");
116+
return 1;
117+
}
118+
119+
const char *cparam_name = argv[1];
120+
const char *patt_name = argv[2];
121+
const char *luma_name = argv[3];
122+
const char *out_dir = argv[4];
123+
int width = (argc > 5) ? atoi(argv[5]) : 429;
124+
int height = (argc > 6) ? atoi(argv[6]) : 317;
125+
126+
/* ---- ARParam + ARHandle setup, mirrors main.c ---- */
127+
ARParam cparam;
128+
if (arParamLoad(cparam_name, 1, &cparam) < 0) {
129+
fprintf(stderr, "ERROR: arParamLoad failed for %s\n", cparam_name);
130+
return 1;
131+
}
132+
arParamChangeSize(&cparam, width, height, &cparam);
133+
ARParamLT *cparamLT = arParamLTCreate(&cparam, AR_PARAM_LT_DEFAULT_OFFSET);
134+
135+
ARHandle *arHandle = arCreateHandle(cparamLT);
136+
if (!arHandle) {
137+
fprintf(stderr, "ERROR: arCreateHandle failed\n");
138+
return 1;
139+
}
140+
arHandle->arPixelFormat = AR_PIXEL_FORMAT_MONO;
141+
/* arPatternDetectionMode is left at the default (AR_TEMPLATE_MATCHING_COLOR), which
142+
matches the simple Rust example; do not override. */
143+
144+
ARPattHandle *pattHandle = arPattCreateHandle();
145+
if (!pattHandle || arPattLoad(pattHandle, patt_name) < 0) {
146+
fprintf(stderr, "ERROR: arPattLoad failed for %s\n", patt_name);
147+
return 1;
148+
}
149+
arPattAttach(arHandle, pattHandle);
150+
151+
/* ---- Load raw luma image ---- */
152+
size_t data_size = (size_t)width * (size_t)height;
153+
unsigned char *luma_buffer = (unsigned char *)malloc(data_size);
154+
FILE *f = fopen(luma_name, "rb");
155+
if (!f || fread(luma_buffer, 1, data_size, f) != data_size) {
156+
fprintf(stderr, "ERROR: could not read %zu bytes from %s\n", data_size, luma_name);
157+
if (f) fclose(f);
158+
free(luma_buffer);
159+
return 1;
160+
}
161+
fclose(f);
162+
163+
AR2VideoBufferT vbuf;
164+
memset(&vbuf, 0, sizeof(vbuf));
165+
vbuf.buff = luma_buffer;
166+
vbuf.buffLuma = luma_buffer;
167+
vbuf.fillFlag = 1;
168+
169+
/* ---- Run detection once ---- */
170+
if (arDetectMarker(arHandle, &vbuf) < 0) {
171+
fprintf(stderr, "ERROR: arDetectMarker failed\n");
172+
return 1;
173+
}
174+
175+
int marker_num = arGetMarkerNum(arHandle);
176+
if (marker_num <= 0) {
177+
fprintf(stderr, "ERROR: no markers detected (marker_num=%d)\n", marker_num);
178+
return 1;
179+
}
180+
ARMarkerInfo *minfo = arGetMarker(arHandle);
181+
/* Pick the first marker with id >= 0; otherwise fall back to index 0. */
182+
int sel = 0;
183+
for (int i = 0; i < marker_num; i++) {
184+
if (minfo[i].id >= 0) { sel = i; break; }
185+
}
186+
187+
/* The candidate's contour data lives in markerInfo2[sel] (assumes marker_info[i]
188+
corresponds to markerInfo2[i] for our single-detection case — true when no
189+
candidates were dropped between detection stages). */
190+
ARMarkerInfo2 *m2 = &arHandle->markerInfo2[sel];
191+
192+
int patt_size = pattHandle->pattSize;
193+
int patt_max_size = patt_size * 2;
194+
int channels = (arHandle->arPatternDetectionMode == AR_TEMPLATE_MATCHING_COLOR) ? 3 : 1;
195+
size_t ext_len = (size_t)patt_size * (size_t)patt_size * (size_t)channels;
196+
unsigned char *ext_patt = (unsigned char *)malloc(ext_len);
197+
memset(ext_patt, 0, ext_len);
198+
199+
/* Re-extract using the same contour data the matcher used internally.
200+
If the prototype differs in your AR/ar.h variant, adjust here. */
201+
int rv = arPattGetImage(arHandle->arImageProcMode, arHandle->arPatternDetectionMode,
202+
patt_size, patt_max_size,
203+
luma_buffer, width, height, arHandle->arPixelFormat,
204+
m2->x_coord, m2->y_coord, m2->vertex,
205+
arHandle->pattRatio, ext_patt);
206+
if (rv < 0) {
207+
fprintf(stderr, "ERROR: arPattGetImage returned %d\n", rv);
208+
free(ext_patt);
209+
return 1;
210+
}
211+
212+
/* ---- Write outputs ---- */
213+
char path_bin[1024], path_meta[1024];
214+
snprintf(path_bin, sizeof(path_bin), "%s/c_ext_patt.bin", out_dir);
215+
snprintf(path_meta, sizeof(path_meta), "%s/c_ext_patt_meta.txt", out_dir);
216+
217+
FILE *fbin = fopen(path_bin, "wb");
218+
if (!fbin) {
219+
fprintf(stderr, "ERROR: could not open %s for writing\n", path_bin);
220+
return 1;
221+
}
222+
fwrite(ext_patt, 1, ext_len, fbin);
223+
fclose(fbin);
224+
225+
FILE *fmeta = fopen(path_meta, "w");
226+
if (!fmeta) {
227+
fprintf(stderr, "ERROR: could not open %s for writing\n", path_meta);
228+
return 1;
229+
}
230+
fprintf(fmeta, "patt_size = %d\n", patt_size);
231+
fprintf(fmeta, "channels = %d\n", channels);
232+
fprintf(fmeta, "mode = %s\n", mode_name(arHandle->arPatternDetectionMode));
233+
fprintf(fmeta, "pixfmt = %s\n", pixfmt_name(arHandle->arPixelFormat));
234+
fprintf(fmeta, "xsize = %d\n", width);
235+
fprintf(fmeta, "ysize = %d\n", height);
236+
fprintf(fmeta, "patt_ratio = %.6f\n", arHandle->pattRatio);
237+
fprintf(fmeta, "selected = %d (of %d markers)\n", sel, marker_num);
238+
/* The 4 contour vertices used by arPattGetImage. */
239+
for (int i = 0; i < 4; i++) {
240+
int idx = m2->vertex[i];
241+
fprintf(fmeta, "vertex[%d] = (%d, %d)\n", i, m2->x_coord[idx], m2->y_coord[idx]);
242+
}
243+
/* Ideal-space marker corners (post-distortion-correction). */
244+
for (int i = 0; i < 4; i++) {
245+
fprintf(fmeta, "marker_vertex[%d] = (%.4f, %.4f)\n", i, minfo[sel].vertex[i][0], minfo[sel].vertex[i][1]);
246+
}
247+
fprintf(fmeta, "id = %d\n", minfo[sel].id);
248+
fprintf(fmeta, "id_patt = %d\n", minfo[sel].idPatt);
249+
fprintf(fmeta, "cf = %.6f\n", minfo[sel].cf);
250+
fprintf(fmeta, "cf_patt = %.6f\n", minfo[sel].cfPatt);
251+
fprintf(fmeta, "dir = %d\n", minfo[sel].dir);
252+
fclose(fmeta);
253+
254+
printf("Wrote %s (%zu bytes)\n", path_bin, ext_len);
255+
printf("Wrote %s\n", path_meta);
256+
printf("cf=%.4f id=%d dir=%d\n", minfo[sel].cf, minfo[sel].id, minfo[sel].dir);
257+
258+
/* Cleanup */
259+
free(ext_patt);
260+
free(luma_buffer);
261+
arPattDetach(arHandle);
262+
arPattDeleteHandle(pattHandle);
263+
arDeleteHandle(arHandle);
264+
arParamLTFree(&cparamLT);
265+
266+
return 0;
267+
}

benchmarks/data/README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Pattern Extraction Diagnostic — Issue #103
2+
3+
This directory holds the test fixtures and diagnostic outputs used to compare
4+
the Rust port of `ar_patt_get_image` against the C reference (WebARKitLib's
5+
`arPattGetID.c`).
6+
7+
See [docs/issue-103-fix-plan.md](../../docs/issue-103-fix-plan.md) for the full
8+
plan.
9+
10+
## Inputs
11+
12+
| File | Description |
13+
|---|---|
14+
| `camera_para.dat` | ARToolKit camera-parameter file. |
15+
| `patt.hiro` | Standard hiro template (16×16 BGR, ARToolKit5 format). |
16+
| `img.jpg` | Reference scene containing a clearly visible hiro marker. |
17+
| `hiro.raw` | Raw 8-bit luma extracted from `img.jpg` by either dumper (auto-generated). |
18+
19+
## Diagnostic outputs
20+
21+
| File | Producer | Description |
22+
|---|---|---|
23+
| `c_ext_patt.bin` | `dump_patt` (C) | Raw `ext_patt` bytes after `arPattGetImage`. |
24+
| `c_ext_patt_meta.txt` | `dump_patt` (C) | patt_size, channels, mode, vertices, cf, id, dir. |
25+
| `rs_ext_patt.bin` | `dump_patt` (Rust) | Same shape, produced by Rust port. |
26+
| `rs_ext_patt_meta.txt` | `dump_patt` (Rust) | Same. |
27+
28+
## Recipe
29+
30+
### 1. Build and run the C dumper
31+
32+
```bash
33+
cd benchmarks/c_benchmark
34+
cmake -B build
35+
cmake --build build --target dump_patt
36+
./build/dump_patt \
37+
../data/camera_para.dat \
38+
../data/patt.hiro \
39+
../data/hiro.raw \
40+
../data \
41+
429 317
42+
```
43+
44+
Note: `hiro.raw` is auto-generated by either dumper from `img.jpg`. Run the
45+
Rust dumper first if `hiro.raw` doesn't yet exist.
46+
47+
### 2. Run the Rust dumper
48+
49+
```bash
50+
cargo run --release --example dump_patt --features log-helpers
51+
```
52+
53+
Output:
54+
55+
```
56+
[info] Wrote benchmarks/data/rs_ext_patt.bin (768 bytes)
57+
[info] Wrote benchmarks/data/rs_ext_patt_meta.txt
58+
[info] cf=0.0965 id=-1 dir=3
59+
```
60+
61+
### 3. Diff the two patches
62+
63+
```bash
64+
cargo run --release --example diff_patt --features log-helpers
65+
```
66+
67+
Output (when patches diverge):
68+
69+
```
70+
[info] c side: benchmarks/data/c_ext_patt.bin (768 bytes)
71+
[info] rs side: benchmarks/data/rs_ext_patt.bin (768 bytes)
72+
[info] identical: 12/768 bytes
73+
[info] divergent: 756 / 768 (98.44%)
74+
[info] first 8 divergences: [(0, c=0xfe, rs=0x12), (1, c=0xfd, rs=0x10), …]
75+
[info] max abs delta: 244
76+
```
77+
78+
Output (when patches match):
79+
80+
```
81+
[info] identical: 768/768 bytes
82+
[info] PATCHES ARE BYTE-EQUAL ✓
83+
```
84+
85+
## Vertex synchronisation (R-A2 mitigation)
86+
87+
Both dumpers print the contour vertices used by `arPattGetImage`. If the C
88+
and Rust meta files report different vertex coordinates, the byte diff
89+
includes geometric drift on top of any extraction divergence — interpret with
90+
care. To eliminate that confounder, copy the C vertices into the Rust dumper
91+
(or vice-versa) before re-running.
92+
93+
This is a future enhancement; for now compare the `vertex[i] = (X, Y)` lines
94+
in the two `*_meta.txt` files manually before running `diff_patt`.
95+
96+
## Interpreting the diff
97+
98+
| Signal | Likely root | Next step |
99+
|---|---|---|
100+
| Patches byte-equal | Bug is in `pattern_match` arithmetic or `ar_patt_load` (out of scope #4) | Re-scope; not a sampling bug |
101+
| Statistical drift (similar histogram, small per-byte deltas) | Wrong averaging (luma weighting, total_div, integer truncation) | Fix `ar_patt_get_image` arithmetic |
102+
| Structural drift (shifted/transposed/rotated) | Sampling-grid bug | Fix `ar_patt_get_image` sampling loop |
103+
| Channel-permuted (R↔B swapped) | BGR/RGB convention mismatch | One-line swap in color branch writes |
104+
| Catastrophic (zeros / garbage) | Severe index or perspective bug | Likely full re-port of the active path |

0 commit comments

Comments
 (0)