Rubbing Image Preparation
Prepare auditable image baselines for research. Treat every output as a deterministic derivative (DER), never as newly recovered writing.
Preconditions
- Register the source and rights with
research-provenance-ledger. - Keep the original file immutable and outside public serving directories.
- Inspect untrusted downloads in quarantine before decoding them.
- Use a Python runtime with the versions in
requirements.txt.
Read method-policy.md before interpreting metrics or choosing an OCR input. Read output-contract.md when another Skill will consume the manifest. Read destain-derivation.md before removing anything from an image.
Workflow
1. Inspect without deriving
python3 scripts/prepare_image.py inspect \
--input /path/to/immutable-image.tif \
--output-report /path/to/inspection.json
Review dimensions, mode, frame count, clipping, entropy, contrast, gradient energy, and warnings. A metric describes the decoded pixels; it does not prove legibility or authenticity.
2. Produce deterministic baselines
python3 scripts/prepare_image.py derive \
--input /path/to/immutable-image.tif \
--output-dir /path/to/new-run
The command refuses an existing output directory and writes:
grayscale.png;contrast.png;binary-dark-foreground.png;binary-light-foreground.png;manifest.json.
Keep both polarity variants until located evidence or OCR evaluation establishes which is appropriate. Do not silently select the visually pleasing result.
To re-derive one area at full resolution — for example the neighbourhood of an
OCR box a human is being asked to look at — add --region:
python3 scripts/prepare_image.py derive \
--input /path/to/immutable-image.tif \
--output-dir /path/to/new-region-run \
--region 1200,900,640,480
X,Y,WIDTH,HEIGHT are pixel-edge offsets in the derived (post-EXIF-orientation)
frame, the same frame OCR observation boxes are normalized into. The four output
names are unchanged, the parent hash is still the whole asset's, the region is
recorded in the manifest, and the manifest is written as schema 1.1. Two runs
with the same arguments produce byte-identical outputs. A region is not a new
observation: it is the same evidence at the same resolution, cropped.
Known integration gap. A region crop cannot yet be fed back into the OCR chain
as a DER lane: rubbing-ocr-review's validate_der_chain accepts only schema
1.0 derivation manifests, so passing a 1.1 region manifest to
ocr_review.py run --der-manifest fails with "DER manifest is not a supported
image-preparation manifest". That is fail-closed, not unsafe — an unrecognised
provenance document is refused rather than trusted — but until that Skill widens
its accepted set, --region is for looking at an area (context crops, human
review handoffs), not for re-OCRing it. See
docs/research/records/validations/20260724--VAL-VERIFY-002--dependency-boundary-repair.md.
3. Verify before handoff
python3 scripts/prepare_image.py verify \
--manifest /path/to/new-run/manifest.json
Require status: pass. Register every output hash and its parent hash before
OCR. Preserve the manifest's raw-to-derived and derived-to-raw coordinate
matrices so an OCR box on an EXIF-oriented derivative can be located on the raw
source.
4. Remove stains only through the separate destain module
The four baselines never erase anything. Deleting content is a different act with a different contract, so it lives in its own module and its own schema namespace:
python3 scripts/destain_derivation.py destain \
--input /path/to/parent.png \
--output-dir /path/to/new-destain-run \
--route speck-close --foreground dark --radius 1 \
--parent-layer DER
There is no default route, no default polarity and no default radius, because no combination of the three is universally safe. The module refuses a radius outside the gate set by the image's own measured stroke width, refuses the route/polarity pairing that fills holes instead of deleting marks, refuses a run that deletes more apparent foreground than the limit allows, and refuses a run that changes nothing. It publishes what it removed as a second image, and the parent is recoverable from the pair exactly.
The result is DER with one field the baselines do not need:
absence_claims_permitted: false. A cleaned image can never support "no stroke
here" — deletion is how it was made. Verify with
destain_derivation.py verify --manifest ..., and before citing any image as
evidence run destain_derivation.py gate --role evidence.
5. Compare downstream outcomes
Run the same OCR/evaluation protocol on the immutable original and each justified derivative. Preserve failures and regressions. Call an image variant “better” only when a declared metric or human review supports that bounded claim.
Gates
- Reject symlinks, non-regular files, unsupported formats, multipage images, byte-limit violations, and pixel-limit violations.
- Never overwrite an input, report, output directory, or derived file. Output publication first claims a new directory and then creates fixed files exclusively.
- Remove source metadata from derived PNGs; preserve metadata only in the immutable original and manifest summary.
- Do not inpaint, hallucinate strokes, super-resolve, rotate by visual guess, or label a binary mask as restored evidence.
- Escalate ambiguous orientation, cropping, polarity, or apparent missing strokes to human review.
- Send generative reconstruction to a separate hypothetical (
HYP) workflow. - Never remove content through
derive. The four baselines are additive-free and deletion-free; stain removal goes throughscripts/destain_derivation.py, which measures and publishes every deletion. - Never let a cleaned image support a claim that something was absent, and never cite one as evidence. A destain deletes small marks it cannot distinguish from 小 strokes, so an absence in it is the operator's output, not the stone's state.
Runtime
The script uses Pillow and NumPy only. It does not fetch models, execute embedded content, invoke a shell, or make network requests.