Repository navigation
Conversation
…king PR and a labels_of variable
So that tests can import the script without sam2 or ethology installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace create_mask_zarr with create_dataset_store, which writes ds_bboxes, a lazy empty labels variable and label_of as a template, and returns the labels zarr array for SAM2 to fill. Path attributes are converted to str so that zarr can store them, and the space coordinate is cast to object to avoid zarr 3's unstable fixed-width string format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compute_mask_props reads the stored labels back one frame at a time and measures each mask with regionprops_table. Each parameter is a data variable on (image_id, id), at the slot of the mask's bounding box. The ellipse angle is converted to be measured from the image x-axis, in [0, pi). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open the bboxes_and_masks store with xr.open_zarr instead of loading the bboxes with ethology and the masks with dask, and draw the ellipses from the stored parameters instead of recomputing them with regionprops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cast avoids zarr 3's unstable fixed-width string format. It only matters when writing to zarr, so it belongs in the function that writes the store, not in main. Callers, including the test fixture, no longer have to repeat it. Appending the mask parameters later keeps the stored object dtype, so the cast is not needed there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Labels are the position along id + 1, stored as uint16. With more than 65535 id slots they would wrap round to 0 (background) or collide with other labels, with no error. Unlikely for crabs, but the check costs one line and prevents silently corrupted masks. It raises a ValueError rather than using assert, which python -O removes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zarr stores dict keys as strings, so map_image_id_to_filename read back from the store has str keys, and get_image_paths failed on it with a KeyError. It now converts the keys to int, so it works on the dataset from ethology and on the one read from the store. The notebook now calls get_image_paths instead of copying its logic, so there is one copy of the image_id-to-file alignment. The notebook adds the repo root to sys.path to import the script, whether it runs from notebooks/ or from the root. The lazy dask stack stays, because napari needs an array with a dtype, which ImageArrayLazy does not provide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The labels array is sized from the first image. An image of a different size made the run fail part way, after SAM2 compute had been spent, and left a half-written store. ImageArrayLazy now reads the size of every image from its file header, which is cheap, and raises upfront. The check uses the files rather than ethology's image_shape variable, because image_shape is optional and gets a placeholder value when the COCO file has no width or height, so it could raise false errors. Also read the first image from self.img_paths, so that a generator of paths works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flatten_masks_smallest_wins decides which box an overlapping pixel belongs to. A mistake there, such as a change to the sort order, would put masks and parameters at the wrong (image_id, id) and no test would fail. The new tests check that the smaller mask keeps the shared pixels, and that masks of equal area are painted in input order. Also check that the eccentricity equals sqrt(1 - (minor / major)^2), as the proposal said it would. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_bboxes_mask_labels_per_frame used .T, which assumes position and
shape have dims (image_id, space, id). With another order the (N, 4)
boxes would be scrambled with no error. transpose("id", "space") does
not depend on the order. The new test runs with both orders, and checks
that boxes come out as [x1, y1, x2, y2], with labels at position + 1,
skipping empty slots.
Also name the returned tuple (boxes, labels) in the docstring, matching
the names it uses below.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
predict_masks_across_images writes the annotated_frames attr through zarr, which does not update the consolidated metadata that xr.open_zarr reads. Until the parameters were appended, the attr was invisible, so a run that failed in between left a store without it. annotated_frames is kept, because it is the attribute that OCTRON-style mask stores use. The notebook now raises a clear error if the store has no mask parameters, instead of an AttributeError further down. The mask_* variables already mark a finished run, so no separate "complete" attr is added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
image_ids repeats the image_id coordinate, n_images and image_shape can be read from labels.shape, and image_filenames repeats map_image_id_to_filename, which get_image_paths now uses. Each copy could drift from the data. image_shape also had the same name as ethology's per-image image_shape variable, with a different meaning. id_offset stays: together with background_label and mask_encoding it describes how masks are encoded, rather than repeating data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review suggested measuring the masks inside the prediction loop to avoid reading every frame again. Reading them back was a deliberate choice in the proposal: the values can be reproduced from the store, and the function can run on existing stores. Reading one compressed frame costs little next to a SAM2 pass on it. So the approach stays, and the docstring now says it is deliberate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layout said label = id + 1, which only holds when the id coordinate is 0..n-1; the label is the position along id + 1, as the function docstrings already say. Note that ellipse_major_axis_theta is arbitrary for near-circular masks. It is documented rather than set to NaN, because any eccentricity threshold would be arbitrary too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`to_zarr(mode="a")` replaces the store's root attributes with those of the appended dataset. `ds_props` has none, so every store lost `map_image_id_to_filename` and the rest of ethology's and the script's metadata. Copy the stored attributes onto `ds_props` before appending, in a new `append_mask_props` shared by `main` and the round-trip test, which now checks that the attributes survive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sfmig
force-pushed
the
smg/gt-ellipses-prediction
branch
from
October 22, 2026 16:24
09331ce to
4fc9153
Compare
…ect). Clarify append_mask_props. update fixture to represent ethology 0.1.4 fixture
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.
Description
What is this PR?
Why is this PR needed?
scripts/generate_masks_from_bboxes.pypredicts a SAM2 mask for every annotated bounding box, but it only saves the masks, as a bare zarr array. To use the masks downstream we need simple shape parameters per crab, such as area, centroid and the orientation of a fitted ellipse. Each parameter should be easy to trace back to the bounding box it came from. Until now the notebook recomputed the ellipses on the fly withregionprops, so they were never stored.What does this PR do?
The script now writes a single xarray dataset to
<data_dir>/annotations/bboxes_and_masks_<timestamp>.zarr. It holds the bounding boxes, the masks and the mask parameters, all on the same(image_id, id)grid:So everything about one box is a single
.selaway:Changes:
create_dataset_storereplacescreate_mask_zarr.ds_bboxes, an empty lazylabelsvariable andlabel_ofas a template, withto_zarr(..., compute=False), then returns thelabelszarr array for SAM2 to fill. This is the same pattern ascreate_mask_storein Add mask-tracked-crabs entry point #293.Pathattributes are converted tostr, because zarr cannot store them.compute_mask_propsis new.labelsback one frame at a time and measures each mask with scikit-image'sregionprops_table.(image_id, id)of its box: labelk + 1→idindexk.mask_centroid: mean(x, y)of the mask pixels, which is also the ellipse centre. This is not the bbox centre.mask_area: number of mask pixels.mask_area_to_convex_hull_area: scikit-image'ssolidity.mask_area_to_mask_bbox_area: scikit-image'sextent. It uses the mask's own axis-aligned box, not the annotated box, so it depends on rotation.ellipse_major_axis_lengthandellipse_minor_axis_length: full lengths, not semi-axes.ellipse_major_axis_theta: angle from the image x-axis towards +y, in radians, in[0, π). It works directly with matplotlib:Ellipse(xy, w, h, angle=np.degrees(theta)).ellipse_eccentricity.main:spacecoordinate toobject, to avoid zarr 3'sUnstableSpecificationWarning(fixed upstream in ethology#159);append_mask_props, which keeps the store's root attributes (see below).sam2andethologyare now imported insidemain, so that tests can import the script.xarray,daskandscikit-imageare added to the inline script dependencies.notebook_visualise_masks_from_zarr.pynow reads the one store withxr.open_zarr, and draws the ellipses (matplotlib and napari) from the stored variables.ellipses_from_labelsis removed.map_image_id_to_filename;uint16;idposition;Warning
Stores written before this PR (
masks_<timestamp>.zarr) do not open in the updated notebook. Re-run the script to get abboxes_and_masks_<timestamp>.zarrstore, and setmap_month_to_zarrin the notebook to its name.Fix: root attributes were lost when appending the mask parameters
to_zarr(mode="a")replaces the store's root attributes with those of the appended dataset, andds_propshas none. Sobboxes_and_masks_<timestamp>.zarrstores written before 09331ce have"attributes": {}. They lost:annotation_files,annotation_format,images_directories,map_category_to_strandmap_image_id_to_filename, whichget_image_pathsneeds;timestamp,sam2_model,source_images_dir,annotations_file,annotations_format,batch_size,multimask_output,prompt_type,mask_encoding,background_label,id_offset,occlusion_policyanddevice.annotated_framessurvived, because it is on thelabelsarray, not the root.The new
append_mask_propscopies the stored attributes ontods_propsbefore appending.mainandtest_store_round_tripboth use it.Existing stores do not need SAM2 to run again. The script below rebuilds the root attributes from the COCO file with ethology and writes only the attributes. It stops if the store's images or boxes differ from the COCO file.
sam2_model,batch_sizeanddevicewere never saved anywhere else, so it fills in the CLI defaults and"unknown"; edit them if the run used other values. It also addsattrs_restored_on.restore_store_attrs.pyNote
The store's boxes match a fresh ethology load per image, but in a different order along
id, in all 544 images ofsep2023-full. Two fresh loads in the same environment agree with each other. ethology sorts with pandas' defaultsort_values, which is not stable, so the order of boxes within an image may depend on the environment. This is not verified. Until it is,idshould be read as meaningful only inside the store that defines it: downstream code should slice the store, not rebuild boxes from the COCO file. The restore script compares boxes regardless of order for this reason.The design is in
claude-plans/proposal-mask-ellipse-params.md. It covers the parameter list, whyperimeteris left out, the angle convention, and why the parameters are measured on the stored labels.References
labels/label_oflayout.spacecoordinate dtype.How has this PR been tested?
tests/test_unit/test_generate_masks_from_bboxes.py. They use synthetic ellipses and need no SAM2 and no real data:test_compute_mask_props: values land at the right(image_id, id); area, centroid, full axis lengths and θ (0 and π/4) are correct; empty slots areNaN.test_create_dataset_store: the template has all-zerouint16labels,label_of, the bbox variables and zarr-safe attributes.test_store_round_trip: runs the three passes ofmain(template → filllabels→ append parameters) and checks that the stored masks and areas agree, and that the root attributes survive the append. Without the fix this fails withKeyError: 'map_image_id_to_filename'.restore_store_attrs.pywas run on copies of the localsep2023-fullandaug2023-fullstores (without thelabelschunks). The checks pass, and 19 root attributes are written, including amap_image_id_to_filenamewith 544 entries forsep2023-full.ellipse_shapes_from_dsgives the same napari ellipses and axes as the oldregionprops-basedellipses_from_labels;Ellipsedrawn from the stored parameters covers 99.7 % of a rotated mask's pixels;annotated_framesattribute thatpredict_masks_across_imageswrites onlabelssurvivesxr.open_zarr.aug2023-full), and a visual check in the notebook that the ellipses sit on the crabs. This would also confirm the attribute types that ethology sets.Does this PR require an update to the documentation?
Yes, and it is updated here:
scripts/generate_masks_from_bboxes.pynow describes the store layout, every parameter and the angle convention;Checklist:
Note
The
check-manifesthook fails on this branch becauseclaude-plans/proposal-mask-ellipse-params.mdis tracked but not in the sdist. All other hooks pass. The proposal should probably be removed (orclaude-plans/added to thecheck-manifestignore list) before merging.🤖 Generated with Claude Code