Repository navigation
feat(scanmate-scan): port illumination correction, bit-exact - #74
Merged
Merged
Conversation
…unbumped packages Its twine executor now reads the version data `nx release publish` hands every `nx-release-publish` task and skips a project with no new version, as `@nx/js:release-publish` already did. That is what has been failing this workspace's release: only the changed package was meant to publish, but every Python project was uploaded, and the ones nx had not versioned built from their on-disk `0.0.1` - permanently stale, since `release.git.commit` is false. For the four already on PyPI that was silent noise (`--skip-existing` warns and exits 0, which is why `scanmate-ink`, `-align`, `-merge` and `-seal` each carry a junk `0.0.1` release, two of them a `0.0.2` as well). For the three PyPI has never seen it was the actual failure: each redundant upload tried to CREATE a project at `0.0.1` and spent a slot in PyPI's new-project rate limit, so the one genuine release - `[email protected]` - died on 429. `npm install` dropped the four nested `@emnapi` entries under the wasm32-wasi resolver bindings again, which breaks `npm ci`; restored from the previous lockfile, and `npm ci` re-verified. Co-Authored-By: Claude Opus 5 <[email protected]>
Levelling and contrast: `enhance_raster`, `estimate_contrast_points` and `resolve_contrast_points`, with the options and the applied-enhancement report. Every page is compared to the TypeScript as a whole image, sha256 over every byte, not sampled. Two roundings sit side by side here and either would be right on most pixels on its own: the contrast stretch rounds a half UP (`Math.round`), while the unsharp mask writes into a `Uint8ClampedArray`, which rounds a half TO EVEN. A generated document never lands on a half, so both survive a whole-image hash of thirteen random pages - which is exactly how a port using one rule for both would pass. So this adds pages built by hand, as recipes both sides follow: a uniform page whose every ratio is exactly 1 puts the stretch on exactly 255/6 = 42.5, and the TypeScript reads 43. The same reasoning applies to the contrast points. A generated page pins none of the bounds - its percentiles land mid-block among near-identical ratios, where moving one by a percent changes nothing. Two grey pages with a CHOSEN histogram put a single pixel on each boundary instead: both percentiles, both clamps, the `>=` the cumulative count is tested with, and a ratio of 0.25, whose histogram position is exactly 42.5 - the one value where the bin rounding is observable. `sharpen_raster` is absent: it is this same unsharp mask with the blur done by libvips, and `blur_raster` is absent from `scanmate-ink` for the reason that package's README gives. `sharpen` here uses the box-blur mask, which is the one the sharpening was tuned with. Mutation tested: 23 mutants, 20 killed. The three survivors are each provably equivalent or unreachable rather than untested, and `TestWhatNoFixtureCanSeparate` asserts the fact that makes each safe - the clamp bounds are ordered, despeckling copies alpha rather than filtering it, and the background radius is floored at 4 before the blur's own floor of 1 could ever apply. They are kept as written because the TypeScript is written that way. Co-Authored-By: Claude Opus 5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Levelling and contrast:
enhance_raster,estimate_contrast_pointsandresolve_contrast_points, with the options and the applied-enhancement report. Every pageis compared to the TypeScript as a whole image, sha256 over every byte, not sampled.
The two roundings, and why thirteen random pages don't catch them
Two roundings sit side by side in this slice, and either would be right on most pixels on
its own:
Math.round)Uint8ClampedArray, which rounds a half to evenA generated document never lands on a half, so both survive a whole-image hash of
thirteen random pages — which is exactly how a port using one rule for both would have
passed. Mutation testing said so, against a test file whose own docstring claimed the hash
covered it. It didn't.
So this adds pages built by hand, as recipes both sides follow. On a uniform page the
background mean is the page, so every ratio is exactly 1 and the stretch lands on exactly
255/6 = 42.5. The TypeScript reads 43; half-to-even would read 42.The same reasoning applies to the contrast points. A generated page pins none of the
bounds — its percentiles land mid-block among near-identical ratios, where moving one by a
percent changes nothing. Two grey pages with a chosen histogram put a single pixel on
each boundary instead: both percentiles, both clamps, the
>=the cumulative count istested with, and a ratio of
0.25, whose histogram position is exactly42.5— the onevalue where the bin rounding is observable at all.
Absent, on purpose
sharpen_raster— it is this same unsharp mask with the blur done by libvips, andblur_rasteris absent fromscanmate-inkfor the reason that package's README gives.sharpenhere uses the box-blur mask, which is the one the sharpening was tuned with.Mutation testing
23 mutants, 20 killed. The first pass killed 9 of 23; the fixtures above closed eleven
more. The three survivors are each provably equivalent or unreachable rather than untested,
and
TestWhatNoFixtureCanSeparateasserts the fact that makes each safe:min(high, max(low, v))↔max(low, min(high, v))low <= high, and both call sites pass literals that satisfy itdespecklecopies alpha rather than filtering it, so they are equal for every pagemax(1, …)radius floorEach is kept as written because the TypeScript is written that way — a reader comparing the
two files should find the same expression, not a locally-simplified one.
Also here
@mnci/nx-python-pip0.2.10 (its own commit), which is the fix for this workspace's redrelease: its twine executor now skips a project with no new version, so a release stops
uploading every Python package — the unbumped ones from their permanently-stale on-disk
0.0.1. That is what created the junk0.0.1releases on PyPI and what spent thenew-project rate limit that 429'd
[email protected].npm installdropped the four nested@emnapientries under the wasm32-wasi resolverbindings again (fourth time), which breaks
npm ci; restored, andnpm cire-verified.Verification
linttypechecktestbuildgreen forscanmate-scan; root ESLint clean;nx run-many -t testgreen across all 15 projects;parity:checkreports the goldens freshagainst a real TypeScript build. 43 tests in the new slice.
🤖 Generated with Claude Code