StrikeNet β strike/clean word classifier
StrikeNet is the 79k-parameter CNN that pdf-strikethrough-detect
uses to resolve pixel-ambiguous strikethrough cases on scanned document pages. A thin strike
over an ascender-less word is pixel-identical to an ordinary glyph chain; geometry alone cannot
separate them, so a learned model casts the deciding vote. On born-digital PDFs the package reads
vector strokes and annotations directly and never invokes this model.
- Input: one standardized grey word crop,
32Γ160(ink-positive, height-normalized). Produced bypdf_strikethrough.cnn.std_crop; the exact geometry is recorded instrike_verdict_cnn.meta.jsonand enforced at load time (cnn._check_geometry) so training and inference preprocessing cannot drift. - Output: a single logit β sigmoid probability that the word is struck.
- Decision thresholds (in the meta):
p_hi = 0.85,p_lo = 0.15. A word scoringβ₯ p_hiis struck,β€ p_lois clean, and between them it is "unsure". Of the two thresholds, onlyp_hidecides what is reported: a candidate the CNN does not confirm atp_hiis not reported (a geometry-confident one whose crop is too small to score is kept), andp_loonly labels the rest clean or unsure. How the shipped values were chosen is not recorded (see Provenance); the training script sets a retrained model's thresholds from data:p_hias a split-conformal threshold on held-out struck-word probabilities (a recall floor of1 β alphafor crops like the validation set; seetraining/README.md),p_lomirrored on the clean class. Override per call withScanConfig.recall_first(cnn_p_hi=β¦)/precision_first(cnn_p_hi=β¦).
How it fits the pipeline
Scanned page β OCR words + morphological stroke geometry (lines.py) β StrikeNet adjudicates the
auto/review tier crops β struck words, char-level partial resolution, struck-aware markdown.
Full architecture: the project README.
Evaluation
Measured on the project's reproducible 10-document public regulatory-redline corpus (US
Copyright Office, FDIC, CEQ Γ2, EPA Γ3, California CCPA/CPPA, Gretna LA development code; 55.2k
struck words, each sha256-pinned). Reproduce with benchmarks/scanned_recovery.py.
| Metric | Value | Source |
|---|---|---|
| Scanned-path strike recovery (RapidOCR) | 96% | scanned_recovery.py, 3 docs / 24 pages / 2,170 known strikes |
| Scanned-path strike recovery (Azure DI) | 96% with confidence_free(); 91% with the default DI calibration |
same harness, Azure Document Intelligence words |
| Native vector detections independently confirmed by the flag signal | 99.8% | confirmation_rate.py (context; native path, not this model) |
The two scanned-path figures measure the whole pipeline (stroke geometry, OCR words and this model together), not the model on its own. Whether these documents overlap the model's training data is not recorded (see Provenance). No held-out precision/recall figure for the model itself exists yet: it needs labeled crops the model cannot have seen, such as from documents published after July 2026, or a retrain on a recorded labeled set.
Usage
The package runs on ONNX Runtime with no torch dependency. Download is digest-verified before the graph is ever loaded β a tampered host cannot swap the model.
import pdf_strikethrough as st
st.ensure_model(
"https://huggingface.co/niles-liu/strikenet/resolve/main/strike_verdict_cnn.onnx",
"fac2c51baaa75ee782196bdfe7452638cb48c7deddb21163b1ac6a0a72ae4457",
meta_url="https://huggingface.co/niles-liu/strikenet/resolve/main/strike_verdict_cnn.meta.json",
meta_sha256="4388b14715bfb1f51b56bb6c463d8f5c0847533316297890e70ac0b2234405d4",
)
assert "p_hi" in st.get_model_meta() # thresholds + crop geometry now loaded
result = st.detect_pdf("scanned-redline.pdf", ocr=st.rapidocr_backend())
Provenance and training
The shipped weights cannot be reproduced from the package's repository. The ONNX the package
ships (version: v3-corpus, also hosted at niles-liu/strikenet) is byte-identical in every
release since the first (0.4.0, July 2026). It predates training/train_strikenet.py, which did not
produce it. Its training set, train/validation split and training settings are not recorded in the
package's repository or any release, and neither is how its thresholds (0.85 / 0.15) were chosen:
the first release's code calls them guardrails, nothing more.
Training a replacement. training/train_strikenet.py trains StrikeNet on a labeled crop set the
detector exports, sets p_hi / p_lo from held-out data, and records in the meta, under
training, what the model was trained on: a digest of the labeled crops and their labels, the class
counts, the validation size, the hyperparameters (the seed that draws the split among them) and the
package and torch versions. The digest identifies the labeled set but cannot restore it, so keep
the labeled directory. The failing-page β better-model loop is one command per step:
pdf-strikethrough detect scan.pdf --ocr rapidocr --dump-crops crops_out/ # export scored crops
# label crops_out/crops.jsonl: set each row's "label" to "struck" or "clean"
python training/train_strikenet.py crops_out/ -o model_out/ --epochs 40 # train + calibrate + ONNX
Full loop and label format: training/README.md.
Limitations
- Handwritten / freehand strikes are the main known gap: untested, and nothing records that the model saw any in training, so a wavy pen scribble or heavy cross-out may be missed.
- Horizontal, left-to-right text only. Vertical writing modes (CJK/Mongolian) and RTL strike axes are out of scope.
- The model only sees word crops the geometry stage flags as candidates; it does not itself find words on the page.
License
MIT, matching the parent package.