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

Before (examiner marks) After (cleaned)
before after
before after
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)

  1. Segment β€” pad to a multiple of 16, run the U-Net, threshold sigmoid > 0.5 β†’ red mask.
  2. 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.
  3. 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) and deeperaser_ft.safetensors (synthetic-only) are included only for comparison/ablation β€” remove_red.py uses deeperaser_ft_real.safetensors.

Training approach

Two data sources were combined for fine-tuning:

  1. 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.
  2. 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
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Evaluation results