Skip to content

Eval: rotation and darkening transforms in the runner - #12

Merged
06pratyush merged 2 commits into
mainfrom
eval-transforms
Oct 28, 2026
Merged

06pratyush merged 2 commits into
mainfrom
eval-transforms

Conversation

@shashwat-deep

@shashwat-deep shashwat-deep commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Next step of the evidence half (docs/plan.md, "Your half", item 4): the rotation and darkening transforms of protocol part B. The protocol fixes their parameters in eval/transforms.py before the first run. Stacked on #10, which is stacked on #9 and #2; merge those first.

What is in it

  • eval/transforms.py makes the images:
    • rot<degrees>: Pillow's Image.rotate, counterclockwise, bicubic, on a canvas enlarged to fit, uncovered corners mid-grey (128, 128, 128). 90° and 180° are exact transposes. The geometry score.py already uses matches these images; tests check this against Pillow at seven angles.
    • dark<EV>: linear RGB (IEC 61966-2-1), × 2^−EV, Poisson–Gaussian noise, back to 8-bit sRGB. The noise for a linear value x is a Poisson count with mean x / λ_shot, times λ_shot, plus Gaussian noise of variance λ_read. Its variance is λ_read + λ_shot·x, the model of Brooks et al. (CVPR 2019, Eq. 1). Each image and level gets its own seeded generator.
  • run_detect.py --transforms none,rot15,dark2,… makes each copy just before the page asks for it, as a PNG, and keeps none of them. It checks the decoded size of every copy and records the parameters and the Pillow, numpy and LittleCMS versions in run_info.json.
  • run_info.json also gets servedSha256, a SHA-256 over every image given to the page. When two runs differ, it shows whether the images did. The lines follow the manifest, each image with its transforms in turn, so each image is decoded and colour-converted once.
  • Darkened pixels are rounded to 8 bits by comparing with fixed thresholds, so a rerun doesn't depend on the platform's pow(). On a test image this gives the same pixels as the formula.
  • eval/check_colour.py: see below.
  • Tests:
    • 16 new: rotation geometry; the sRGB round trip; for each EV, the darkened mean and variance against the model (within 0.7%); determinism; colour handling; the check script in Chromium.
    • One new runner test: none, rot0, rot45, rot90 and dark2 end to end, scored by score.py.
    • 66 eval tests pass in total. A mutation check confirms the tests fail if the noise is added before darkening, the read noise is dropped, or the corners are black.

Colour: what I found

430 of the 1,000 test images carry an ICC profile, about 120 of them wide-gamut (Adobe RGB, camera RGB, ProPhoto). Chromium converts these to sRGB when it decodes them for the untransformed run, so the transformed copies must start from the same pixels. transforms.upright() converts with LittleCMS (relative colorimetric).

check_colour.py decodes test images in headless Chromium, as the page does, and compares. On 90 images covering all 29 kinds of profile, no pixel differs by more than 1. Getting there turned up one Chromium behaviour: it applies an RGB profile to a greyscale JPEG (14 test images carry one), and it ignores a CMYK profile on an RGB image (1 image). upright() now does the same.

For both of us to agree before any run

  1. Noise level. λ_shot = 0.0011 is the geometric middle of the range Brooks et al. fitted to the Darmstadt Noise Dataset (0.0001 to 0.012). λ_read = 1.17 × 10⁻⁶ comes from their fitted line, log λ_read = 2.18 log λ_shot + 1.2. I checked both numbers against their paper (arXiv 1811.11127, Eq. 3).
    • The model holds the camera's gain fixed while the light drops, so shot noise grows relative to the signal: at −4 EV, a mid-grey face has a signal-to-noise ratio of about 3.4.
    • A phone in dim light usually raises its gain instead, which would argue for a higher λ_shot (their range goes up to 0.012).
    • Shashwat has chosen to keep the middle (0.0011).
  2. Rotation corners: mid-grey adds no new content. Reflecting the image into the corners would add mirrored faces with no boxes. Black would make strong edges.
  3. 0° in the series goes through the same steps (Pillow decoding, ICC conversion, PNG) as the other angles, so comparing none with rot0 shows whether the decoding path itself changes anything.

Speed

On 20 test images with all twelve transforms, the stub runner took 41 s. The full report split (800 images × 12 transforms) would take about 27 minutes plus detection time.

🤖 Generated with Claude Code

shashwat-deep and others added 2 commits October 28, 2026 01:21
transforms.py now makes the protocol's rot<degrees> and dark<EV> images:
bicubic rotation with mid-grey corners, and darkening in linear light with
Poisson-Gaussian sensor noise at the middle of the range Brooks et al.
(CVPR 2019) fitted. Both start from the image converted to sRGB by its ICC
profile, as Chromium converts it; check_colour.py checks the two agree within
1 for test images with each kind of profile. run_detect.py --transforms makes
each copy just before the page asks for it and records the parameters in
run_info.json. Part B of the protocol now states the parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
darken() turns linear light into 8-bit sRGB by comparing with fixed
thresholds rather than calling pow() for each pixel (same pixels on the test
image, slightly faster), so reruns don't depend on the platform's pow().
run_info.json records servedSha256 over every image the page was given. Each
image is decoded and colour-converted once for all its transforms, so the
lines now follow the manifest. upright() refuses image modes check_colour.py
has not compared, and check_colour.py answers unknown requests with 404.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shashwat-deep
shashwat-deep marked this pull request as ready for review October 28, 2026 04:41
@06pratyush
06pratyush changed the base branch from eval-runner to main October 28, 2026 10:26
@06pratyush
06pratyush merged commit 0e93c01 into main Oct 28, 2026
2 checks passed
@06pratyush
06pratyush deleted the eval-transforms branch October 28, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants