Skip to content

adding sub_mask function to plantcv.plantcv#1864

Open
joshqsumner wants to merge 5 commits into
v5.0from
sub-mask
Open

adding sub_mask function to plantcv.plantcv#1864
joshqsumner wants to merge 5 commits into
v5.0from
sub-mask

Conversation

@joshqsumner
Copy link
Copy Markdown
Contributor

Describe your changes
Adds sub_mask function to plantcv.plantcv to sample N circular ROIs from a mask.

Type of update
This is a new feature.

Associated issues
Closes #1863

For the reviewer
See this page for instructions on how to review the pull request.

  • PR functionality reviewed in a Jupyter Notebook
  • All tests pass
  • Test coverage remains 100%
  • Documentation tested
  • New documentation pages added to plantcv/mkdocs.yml
  • Changes to function input/output signatures added to updating.md
  • Code reviewed
  • PR approved

@joshqsumner joshqsumner added new feature New feature ideas and solutions work in progress Mark work in progress labels Jan 13, 2026
@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented Jan 13, 2026

DeepSource Code Review

We reviewed changes in a4a089e...52ed75c on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Coverage  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Mar 30, 2026 10:16p.m. Review ↗
Test coverage Mar 30, 2026 10:16p.m. Review ↗

Code Coverage Summary

Language Line Coverage (New Code) Line Coverage (Overall)
Aggregate
100%
[✓ above threshold]
99.9%
[▼ down 0.1% from main]
Python
100%
[✓ above threshold]
99.9%
[▼ down 0.1% from main]

➟ Additional coverage metrics may have been reported. See full coverage report ↗

@joshqsumner joshqsumner added ready to review and removed work in progress Mark work in progress labels Jan 13, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new sub_mask feature to PlantCV for sampling multiple non-overlapping circular ROIs within an existing object mask (Spot Sampling, closes #1863).

Changes:

  • Introduces plantcv.plantcv.sub_mask implementation for generating labeled circular sub-masks.
  • Exposes sub_mask via plantcv/plantcv/__init__.py and adds user documentation + MkDocs nav entry.
  • Adds initial unit tests for expected sub-mask behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
plantcv/plantcv/submask.py New implementation of random circular sub-mask sampling and helpers.
plantcv/plantcv/__init__.py Exports sub_mask in the public plantcv.plantcv API.
tests/plantcv/test_sub_mask.py Adds tests for successful placement and “cannot place” scenarios.
docs/sub_mask.md New documentation page for sub_mask.
mkdocs.yml Adds the new docs page to site navigation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +81 to +88
# Find coordinates in mask where mask is non-zero
coords = np.column_stack(np.where(mask > 0))
# Randomly select a center point from these coordinates
center = coords[random.randint(0, len(coords) - 1)]
x, y = center[1], center[0]
# Create an ROI from the random center point
spot = circle(img=img, x=x, y=y, r=radius)
return spot
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_make_random_circle can raise at runtime in two common cases: (1) if mask has no non-zero pixels then len(coords) == 0 and random.randint(0, -1) will error; (2) if a non-zero pixel is near the image border, roi_methods.circle will raise (fatal_error) because the ROI extends outside the image. Consider filtering candidate centers to those that are at least radius pixels from the image edges and returning early (or warning) when no valid centers exist.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +25
# Create an empty mask
labeled_mask = np.zeros_like(mask)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

labeled_mask = np.zeros_like(mask) inherits the input mask dtype (commonly uint8), so labels can overflow/wrap once sample_num > 255. PlantCV labeled masks elsewhere use np.int32 (e.g., plantcv/plantcv/masks2labels.py:32). Consider initializing labeled_mask as a 2D np.int32 array matching mask.shape[:2].

Suggested change
# Create an empty mask
labeled_mask = np.zeros_like(mask)
# Create an empty labeled mask with sufficient integer range for labels
labeled_mask = np.zeros(mask.shape[:2], dtype=np.int32)

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +33
tries = 0
sample_num = 0
while len(np.unique(labeled_mask)) - 1 < num_masks:
tries += 1
spot = _make_random_circle(img=img, mask=mask, radius=radius)
spot_mask = roi2mask(img=img, roi=spot)
within = _is_mask_within(full_mask=mask, submask=spot_mask)
if within and _check_overlapping_masks(labeled_mask, spot_mask):
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop condition recalculates np.unique(labeled_mask) on every iteration, which scans the full array repeatedly and can be expensive on large masks. Since you already track sample_num, you can drive the loop with while sample_num < num_masks: (or maintain a counter) to avoid repeated full-array uniques.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +23
"""
Make circular sub-masks inside the mask of an object

Parameters
----------
img = numpy.ndarray, input image
mask = numpy.ndarray, binary mask of object
num_masks = int, number of circular sub-masks to make
radius = int, radius of circular mask to make. Defaults to 5.

Returns
-------
labeled_mask = numpy.ndarray, labelled mask of circular masks each within complete mask.
"""
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sub_mask docstring uses a one-off parameter format (img = numpy.ndarray, ...) that doesn’t match the NumPy-style name : type format used widely in this repo (e.g., plantcv/plantcv/masks2labels.py:10-27). Aligning the docstring style will make API docs more consistent and easier to parse.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +10
def test_sub_mask_success(test_data):
"""Test for PlantCV."""
img = cv2.imread(test_data.small_rgb_img, -1)
mask = cv2.imread(test_data.small_bin_img, -1)
spots = sub_mask(img, mask, 2, 2)
assert len(np.unique(spots)) == 3
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests assert exact label counts, but sub_mask is inherently random and currently has no way to provide a deterministic seed/RNG. This can make CI flaky if a different set of random centers results in a different number of placed spots. Consider adding an optional seed/rng parameter to sub_mask (similar to io.random_subset) and setting it in the tests.

Copilot uses AI. Check for mistakes.
Comment thread docs/sub_mask.md
Comment on lines +5 to +21
**plantcv.plantcv.sub_mask**(*img, mask, num_masks=1, radius=5*)

**returns** A `numpy.ndarray` labelled mask.

- **Parameters:**
- img - Grayscale or RGB image.
- mask - Binary mask of the image.
- num_masks - A number of circular regions to make masks of, defaults to 1. These spots cannot overlap, so specifying too many may trigger a warning that fewer masks could be placed than were specified.
- radius - Radius of the circular region(s) to select. These spots cannot overlap, so specifying too large of a radius may trigger a warning that fewer masks could be placed than were specified.


- **Context:**
- Used to downsample an image, particularly for spectral analysis.


- **Example use:**
- Below
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page doesn’t follow the established docs pattern used elsewhere (e.g., docs/auto_crop.md): it uses plantcv.plantcv.sub_mask instead of plantcv.sub_mask, and several list items are indented with tabs which can render as code blocks in Markdown. Consider switching to the standard **plantcv.sub_mask**(...) heading and replacing tabs with spaces for consistent rendering.

Suggested change
**plantcv.plantcv.sub_mask**(*img, mask, num_masks=1, radius=5*)
**returns** A `numpy.ndarray` labelled mask.
- **Parameters:**
- img - Grayscale or RGB image.
- mask - Binary mask of the image.
- num_masks - A number of circular regions to make masks of, defaults to 1. These spots cannot overlap, so specifying too many may trigger a warning that fewer masks could be placed than were specified.
- radius - Radius of the circular region(s) to select. These spots cannot overlap, so specifying too large of a radius may trigger a warning that fewer masks could be placed than were specified.
- **Context:**
- Used to downsample an image, particularly for spectral analysis.
- **Example use:**
- Below
**plantcv.sub_mask**(*img, mask, num_masks=1, radius=5*)
**returns** A `numpy.ndarray` labelled mask.
- **Parameters:**
- img - Grayscale or RGB image.
- mask - Binary mask of the image.
- num_masks - A number of circular regions to make masks of, defaults to 1. These spots cannot overlap, so specifying too many may trigger a warning that fewer masks could be placed than were specified.
- radius - Radius of the circular region(s) to select. These spots cannot overlap, so specifying too large of a radius may trigger a warning that fewer masks could be placed than were specified.
- **Context:**
- Used to downsample an image, particularly for spectral analysis.
- **Example use:**
- Below

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature ideas and solutions ready to review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants