Repository navigation
Calibration: unit catalog, unknown is a value, field states, _calibration request (QuPath #15) - #28
Merged
Merged
Conversation
…tion request (SPEC_WRONG_SCIENCE section 2) - units come from the conformance catalog of the installed labconstrictor-tools (symbols exact, words case-insensitive after trimming, 72/96 dpi unknown, damaged micro sign refused) - a linked PixelSizeOf field is unset / auto / user (a flag set by the person's edits); an unknown source clears it; the declared default is never a stand-in - a required linked field that is unset blocks the run with the spec sentence; an Optional one sends null - anisotropic pixels (tolerance 1e-3) and non-length units are unknown with a warning - the request carries the per-axis reserved input _calibration (x and y); the run record keeps it - TIFF files are read by their own tags (OME / ImageJ unit first, resolution tags only when not a default dpi); QuPath's own 72 dpi reading of such a TIFF is not trusted unless the calibration was entered in QuPath - tests/gui_test_calibration_body.groovy runs CAL-01..20 and CAL-T1..T9 through the form, the request and real worker runs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN
…e2-qupath-calibration
…chine on its own, the run re-reads an edited calibration Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN
guijacquemet
marked this pull request as ready for review
October 13, 2026 15:22
guijacquemet
changed the base branch from
claude/phase2-qupath-coordinate-frame
to
main
October 13, 2026 16:00
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.
Phase 2, theme 3 (calibration) for the QuPath host:
docs/SPEC_WRONG_SCIENCE.mdsection 2 of LabConstrictor-Tools andwrong_science.calibrationofconformance.json(CAL-01..20, CAL-T1..T9; CAL-09 and CAL-T7 are the QuPath-specific cases). Fixes #15. The Napari reference is LabConstrictor-napari #53.Stacked on #27
This PR is stacked on the unmerged coordinate-frame PR #27 (base branch
claude/phase2-qupath-coordinate-frame); it contains its commits. A PR stacked on an unmerged parent does not reachmainwhen the parent is squash-merged: after #27 is merged, retarget this PR tomain(do not merge it stacked). The calibration commits themselves are820ae4aand6161775; the rest is #27.What changes
LcUnits): loaded from the installed labconstrictor-tools (python -m labconstrictor_tools.conformance --path, run with the interpreter of a registered app;LC_CONFORMANCEoverrides it for tests). The script keeps no unit list. Symbols (um,nm, micro-sign forms, Angstrom signs, ...) are exact, case-sensitive text; words are case-insensitive after trimming; anything else is an unknown unit; U+FFFD +mis refused with thedamagedsentence. If the catalog cannot be loaded every calibration is unknown and the sentence says why (the number alone is never evidence).px, an unknown or non-length unit (unit 's' is not a length), a damaged unit, or pixels that are not square (X and Y differ by more than 1e-3 relative to the larger,pixels are not square) give an unknown calibration: the field is emptied, the tooltip saysnot calibrated: 'image' has no pixel size, the warning is in the status line. Within the tolerance the field takes the mean of X and Y; it is never filled from one axis.PixelCalibration(unit and number) through the catalog. A file input: its own TIFF tags, read without decoding pixels (classic and BigTIFF, both byte orders): OMEPhysicalSizeor an ImageJunit=first; the resolution tags only for INCH/CENTIMETER and not when they look like 72 or 96 dpi (28.35/37.80 per cm);NONEis unknown; other file formats through QuPath's reader. QuPath-specific trap: QuPath's own TIFF reader turns a 72 dpi tag into 352.78 um per pixel withhasPixelSizeMicrons()true (the test prints it). For a local TIFF whose calibration was not entered in QuPath (server metadata equal to the reader's original) the TIFF rules apply too; a calibration the person entered in QuPath (metadata differs from the original) is explicit and kept (CAL-09).PixelSizeOffield is a text box with a button "Use the image's calibration", stateunset/auto/userkept as a flag set by the person's edit events (typed or pasted text, and a key event that leaves the text unchanged), never inferred by comparing numbers; a programmatic write never makes ituser. The box follows the chosen image, the typed file (a file wins over the open image), and the open image (listener on QuPath's image property) until the person enters a value. A source that is unknown clears anautofield (Do not retain stale PixelSizeOf values when QuPath image calibration changes #15: the previous image's value never stays); an empty box takes the image's calibration again when it loses the focus or a run starts. The declared default is never a stand-in (the form no longer starts at 0.65).Optionaland has no value blocks the run with'pixel_size' needs a value: 'image' has no pixel size. Enter it in um.; anOptional[float]that is unset sendsnull; text that is not a number, 0 or a negative number is refused by name. Every field that is not the person's own is read again at the click (a calibration edited in QuPath after choosing the image is used)._calibration(reserved input, 2.3.4): per axis (x, y),valuein um,unit,sourceimage / file / user and the raw number and unit text, frozen with the values at the click. Tools never see it; the run record (run.json,inputs) keeps it (the installed Tools worker does not copy it itself, so the host does it, as Napari).pixel size differs: Y .., X .." behaviour, the "keep the value when the file has none" behaviour,autoPixel(value comparison).Issues
Tests
tests/gui_test_calibration_body.groovy(142 checks,tests/run_gui_ci.sh, README). Cases and catalog are read from the conformance file. Every case goes through the real form (file input, project images, the image open in QuPath) and checks the request the host would send (readFormValues+prepareInputs: the manifest value and_calibration), the field text, the tooltip and the blocked-run sentence; the transitions also run the real worker (test toolsPixel size required, with a declared default 1.0, andPixel size optionalintests/frame_app) and readrun.json. Images are written bytests/calibration_fixtures.py(tifffile, with the Python of the registeredframeapp): OME files keep the unit text exactly, so CAL-01..07, 15..20 and all 49unit_match_refusednames run as files; ImageJ files give CAL-08 and CAL-10; TIFF tags give CAL-10..14 (also BigTIFF and big-endian); CAL-09 sets the open image'sPixelCalibration(true with 1.0, false). CAL-T1..T9 are driven with real key events typed into the box (T9: typing the value the box already shows). The field state machine is also tested on its own._calibration, programmatic write counted as an edit, typed value not a flag, user value not kept, previous value kept when unknown, OME ignored, ImageJ unit ignored, damaged sign accepted, Optional blocked, QuPath's 72 dpi reading trusted, calibration entered in QuPath ignored, no re-read at the click,sourcealways image, raw text dropped, file losing to the image, resolutionNONEtrusted). All caught. One (the user-value guard insideLcLink.sourceChanged) was first missed because the caller has the same guard (the mutant was equivalent at behaviour level); I added tests of the state machine on its own and it is now caught.Existing tests changed (superseded by the spec)
tests/gui_test_parity_pixel_body.groovy(QP-8), each change markedwas:in the code:pixel_field_starts_at_its_default->pixel_field_starts_unset_not_at_its_declared_default(CAL-T8: a default is no stand-in).a_file_without_calibration_leaves_the_value->a_file_without_calibration_clears_the_field(CAL-T5: another image's value must not stay).typed_value_is_not_overwritten_by_*: the value is now typed with key events; writing throughdlg.settersis not an edit (2.2).the_other_pixel_field_was_not_touched: the other field is unset, not at its default 0.325.anisotropic_pixels_are_said(old sentencepixel size differs: Y 0.25 µm, X 0.5 µm - the tool takes one value and gets X) ->pixels are not square;anisotropic_pixels_send_x->anisotropic_pixels_leave_the_field_unset(CAL-06, 2.1.5).unreadable_file_is_said_and_value_kept->..._and_the_field_is_unset.Unchanged and passing: file header fills the field, another file refills, centimetre unit, fresh form follows the file, square pixels say nothing, unreadable file logged, a path still being typed is silent.
tests/gui_test_body.groovy(NucleiSky, not run in CI) was not edited; itsQ3expects 0.65 / 0.325 from ImageJ-unit files, which this rule still gives, but it was not run.Spec gaps and conservative choices
LC_CONFORMANCE. An app whose labconstrictor-tools has noconformancemodule gives unknown calibrations until updated (stated in the status).Optional(as Napari): a linked float with a declared default is required in the spec's sense (CAL-T8), although the manifest marks itrequired: false.unsetat once but re-derived only on focus loss, the button, a source change or Run, so typing "0.3" over a selected value is not undone half-way. Non-numeric text is the person's value and is refused at the run by name ('Pixel size': 'abc' is not a number).ResolutionUnittag is read as INCH (the TIFF default) and then the 72/96 rule applies; if either axis looks like a default the file is unknown. Same as Napari.inch,km, case variants such asUM) gets theis not a lengthwarning; the spec only says unknown.pixel,px, empty,none,dimensionlessare unknown without a warning.source: file, a project image or the open imageimage, a typed valueuser.Min(...)declared on a linked field is not enforced beyond "greater than 0".Not verified
tests/gui_test_body.groovy(CellTracksColab, NucleiSky) is skipped in CI and was not run.🤖 Generated with Claude Code
https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN