File size: 6,084 Bytes
e880876
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8b65440
e880876
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
---
license: other
tags:
  - change-detection
  - object-detection
  - document-analysis
  - centernet
  - siamese
library_name: pytorch
---

# CyberAI Cup 2026 β€” Task 1: Packaging Material Difference Mining

Complete working state of the solution: trained weights, all source code, documentation, and the raw
evidence behind every reported number. Private repository β€” this is a working handover, not a release.

## Current result

Fold-0 out-of-fold **global F1 0.9049** (precision 0.9225, recall 0.8881, score threshold 0.31),
measured on 40 held-out validation pairs with the competition's own metric: TP/FP/FN accumulated
across all images before computing precision and recall, boxes matched at IoU β‰₯ 0.5.

For scale, the classical image-difference baseline scores **0.029** on the same split.

Folds 1–4 are untrained and no submission has been generated. `HANDOVER.md` section 5 holds the
prioritized queue for whoever continues.

## What is in here

| Path | Contents |
|---|---|
| `checkpoints/stage1_fold0_convnext_tiny/best.pt` | The model β€” 128 MB, epoch 19, the 0.9049 checkpoint |
| `checkpoints/stage1_fold0_convnext_tiny/last.pt` | 384 MB, epoch 35, with optimizer and scheduler state for resuming |
| `checkpoints/oof/` | Out-of-fold candidate boxes and the threshold sweep |
| `checkpoints/stage2_ab/` | Verifier from the A/B test β€” reference only, its measured delta was +0.0009 |
| `src/pmdm/` | 14 modules, ~1500 lines: preprocessing, model, losses, decoding, training, inference, metric |
| `modal_app.py` | Every Modal function and entrypoint used to produce these results |
| `scripts/` | CPU smoke test, local training without Modal, dataset analysis, error analysis |
| `runs/` | Raw training logs, per-epoch history, and analysis output from every run |
| `Task1/` | The competition dataset: 200 training pairs with 1407 annotated boxes, 100 test pairs. **Uploading separately** β€” 2.5 GB of page-scale PNGs takes hours on a home uplink, so it lands after the code and weights (`scripts/push_dataset_to_hf.py`) |
| `HANDOVER.md` | Start here β€” architecture, results, error breakdown, mistakes, next steps |
| `RUNBOOK.md` | Every command with measured timings |
| `IMPLEMENTATION_PLAN.md` | Original design rationale |

## Architecture

A siamese CenterNet. Both images pass through a shared ConvNeXt-tiny encoder taking 4 channels
(BGR plus a local-background-normalized "ink map" that makes the features invariant to the photo's
shadows). Features fuse per scale as `conv(concat(a, b, |a βˆ’ b|))`, decode through a U-Net upsampling
path, and emit four heads at **output stride 2** β€” a Gaussian focal heatmap for difference centers,
width/height, a sub-pixel offset, and an auxiliary change-mask segmentation.

Stride 2 is the load-bearing choice: 83 of the ground-truth boxes are 8Γ—8 pixels, which vanish
entirely at the stride 4 a standard CenterNet uses.

Three things about the data drove the rest of the design:

1. **Pairs are already sub-pixel aligned.** Of 300 pairs, 284 need no registration at all and 16 need
   a pure translation. No warping is applied by default.
2. **Blur, not geometry, is the dominant false-positive source.** The photo is softer than the
   template, so the template is blur-matched to it with a Οƒ grid search minimizing the
   Laplacian-variance gap before any comparison.
3. **Nothing is ever deleted.** Of 1404 analysed boxes, 842 are additions and 558 are modifications
   of existing content β€” and exactly 0 are deletions. That licenses a polarity filter discarding any
   candidate that is inked in the template and blank in the photo.

Inference tiles each page at 768 px with 25% overlap, then merges candidates with Weighted Boxes
Fusion rather than NMS β€” with boxes this small, averaging the coordinates beats picking one.

## Loading the model

```python
import torch
from pmdm.model import SiamCenterNet

model = SiamCenterNet(backbone="convnext_tiny")
ck = torch.load("checkpoints/stage1_fold0_convnext_tiny/best.pt", map_location="cpu")
model.load_state_dict(ck["model"])   # all keys match
model.eval()
```

`ck["metric"]` carries the evaluation that selected this checkpoint, including the 0.31 threshold.
Full tiled inference over a page is `pmdm.infer`; scoring is `pmdm.metric.global_f1`.

## Reproducing

```bash
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e . torch torchvision timm \
    opencv-python-headless "numpy<2" pandas

# CPU pre-flight over the whole pipeline, ~2 min
PMDM_DATA=Task1/PackagingMaterialDifferenceMiningDataset \
PMDM_WORK=/tmp/pmdm_work PMDM_CKPT=/tmp/pmdm_ckpt \
.venv/bin/python scripts/local_smoke.py

# every dataset claim above
PMDM_DATA=Task1/PackagingMaterialDifferenceMiningDataset \
.venv/bin/python scripts/analyze_dataset.py

# the error breakdown, from the archived out-of-fold predictions
.venv/bin/python scripts/error_analysis.py checkpoints/oof/fold0_convnext_tiny.npz
```

Training runs either on Modal (`modal run --detach modal_app.py::train --fold 0 --epochs 40`,
~45 min on an A100-40GB) or on any GPU box with no Modal account
(`python scripts/train_local.py --fold 0 --epochs 40`). Same loop, same checkpoints.

## Where the remaining errors are

Of 268 ground-truth boxes across the validation fold: 252 were proposed and matched, 5 were proposed
but localized too loosely to clear IoU 0.5, and 11 were never proposed at all β€” a recall ceiling of
0.959 for the current proposal stage.

By box size, the picture is lopsided: boxes above 24 px match 97.5% of the time, 12–24 px match
97.9%, and boxes under 12 px match 82.8%. **Ten of the eleven complete misses are sub-12px marks.**
Everything else is solved. That is why the next step is synthetic data weighted toward tiny marks
(`src/pmdm/synth.py`, written and smoke-tested but never run at scale) rather than more box
refinement or a stronger verifier.

## License

The dataset in `Task1/` belongs to the CyberAI Cup 2026 organizers and is included here only so this
private repository is self-contained for the team. Do not redistribute it.