Red-ink removal
Removes examiner red ink from exam-script scans while preserving the student's black handwriting. Two learned stages: a U-Net segmenter finds the red, a fine-tuned DeepEraser removes it and reconstructs what was underneath.
Everything needed is in this folder; paths are relative, so it runs from anywhere.
Before / After
Real exam scripts with red examiner ink (ticks, crosses, circled deductions). The model removes the red strokes and reconstructs the student's black handwriting underneath.
red_removal_bundle/
βββ README.md
βββ requirements.txt
βββ config.json
βββ hubconf.py β torch.hub entry point
βββ models/
β βββ redseg_best.safetensors # segmenter (U-Net), val IoU 0.956
β βββ deeperaser_ft_real.safetensors # eraser β PRODUCTION (real+synthetic fine-tune)
β βββ deeperaser_ft.safetensors # eraser β synthetic-only (reference)
β βββ deeperaser.safetensors # DeepEraser upstream pretrained (zero-shot baseline)
βββ deeperaser/ # upstream model code (Feng et al., IEEE TMM 2024)
β βββ __init__.py
β βββ model.py # DeepEraser class
β βββ update.py # BasicUpdateBlock
β βββ extractor.py # BasicEncoder
βββ code/
βββ __init__.py
βββ train_redseg.py # defines the UNet class
βββ remove_red.py # β the script you run
Use from PyTorch Hub
import torch
remover = torch.hub.load("torr20/exam-red-remover", "red_remover")
clean_img, red_pct = remover("page.jpg")
# clean_img is a numpy array (H, W, 3) uint8
Setup (local)
pip install torch numpy Pillow safetensors
A CUDA GPU is used automatically if present (~2 GB for a normal page); otherwise pass
--cpu (slower).
Use (local)
# one image
python code/remove_red.py page.jpg
# several images / a whole folder, custom output dir
python code/remove_red.py scans/ --out cleaned/
# force CPU
python code/remove_red.py page.jpg --cpu
Cleaned images are written to cleaned/ (or --out) as <name>.jpg. The script prints
the detected red fraction per image. Pages with essentially no red are passed through
unchanged.
How it works (per image)
- Segment β pad to a multiple of 16, run the U-Net, threshold sigmoid > 0.5 β red mask.
- Erase β feed image + mask to DeepEraser (with the
module.prefix stripped on load); it removes the masked pixels and reconstructs the black. Large pages (> 1400 px on the long side) are processed in 768 px tiles with 96 px feathered overlap to bound memory. - Save β cleaned RGB written as JPEG q95.
Quantitative results (40 real held-out exam pages)
| Model | masked-L1 (red) β | PSNR β |
|---|---|---|
| DeepEraser zero-shot | 0.043 | 26.69 dB |
| DeepEraser, synthetic-only fine-tune | 0.078 | 26.51 dB |
| DeepEraser, real + synthetic fine-tune (ours) | 0.011 | 26.94 dB |
Synthetic-only fine-tuning is worse than zero-shot on real ink β it overfits to synthetic red. Only fine-tuning on real, registered pairs improves real-page performance.
Notes / limits
- The production eraser is
deeperaser_ft_real.safetensors. On the held-out real test set its red-region reconstruction error was ~0.011 vs 0.043 for the untouched upstream model and 0.078 for the synthetic-only fine-tune. - Under fully opaque red, removal is reconstruction/inference, not exact recovery β good enough to restore legibility for re-grading, not a guaranteed-original stroke.
- The segmenter can read reddish skin (fingers holding a page) as ink and desaturate it; harmless for answer content but visible in some renders.
deeperaser.safetensors(upstream) anddeeperaser_ft.safetensors(synthetic-only) are included only for comparison/ablation βremove_red.pyusesdeeperaser_ft_real.safetensors.
Training approach
Two data sources were combined for fine-tuning:
- Synthetic pairs (3,058 triples) β clean exam patches with programmatically painted red strokes (lines, circles, ticks, scribbles at random opacity 0.3-0.95). Perfectly pixel-aligned.
- Real pairs (1,774 patches) β genuine red-marked exam pages aligned via ORB features + RANSAC homography to their cleaned counterparts (produced by a commercial generative model).
The eraser was fine-tuned for 25 epochs, BS=4, 6-iteration recurrent unroll, Adam 1e-4 with cosine annealing. The loss function applies 4Γ weight to errors inside the red region and uses exponential weighting (Ξ³=0.8) across the 6 refinement passes to emphasize later iterations.
Citation
If you use this model in your work:
@misc{exam-red-remover,
title = {Red-Ink Removal for Exam Script Re-Grading},
author = {Grading Agent Team},
year = {2026}
}
@article{feng2024deeperaser,
title = {DeepEraser: Deep Iterative Context Mining for Generic Text Eraser},
author = {Feng, Hao and Wang, Wendi and Liu, Shaokai and Deng, Jiajun and Zhou, Wengang and Li, Houqiang},
journal = {IEEE Transactions on Multimedia},
year = {2024}
}
Full method write-up: docs/red-removal-process.html in the main repo.
- Downloads last month
- 25
Evaluation results
- masked-l1 on 40 Real Held-Out Exam Pagesself-reported0.011
- psnr on 40 Real Held-Out Exam Pagesself-reported26.940





