-
Notifications
You must be signed in to change notification settings - Fork 3
Enhance plot_MIP_with_mask_outlines with flexible unit handling and physical scaling #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR significantly enhances the plot_MIP_with_mask_outlines function to support flexible unit handling, automatic figure creation, and physical DICOM-based scaling for medical imaging visualization.
Changes:
- Enhanced
plot_MIP_with_mask_outlineswith automatic bounds detection, physical spacing support, unit conversion (Bq/ml, counts), and configurable color mapping via JSON config files - Added helper function
_find_config_filefor upward directory search (git-like) to locate configuration files - Updated configuration template with
plot_colorssection for customizable organ visualization colors
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 15 comments.
| File | Description |
|---|---|
| pytheranostics/plots/plots.py | Major enhancements to plot_MIP_with_mask_outlines: added automatic bounds detection, physical DICOM scaling, flexible unit handling, config-based color mapping, and auto figure creation. Added _find_config_file helper and new imports (json, importlib.resources). |
| pytheranostics/plots/init.py | Updated module docstring and added explicit imports/exports for plotting functions in __all__. |
| pytheranostics/data/configuration_templates/voi_mappings_config.json | Added plot_colors section with instructions for customizing organ contour colors in MIP plots. |
| docs/source/tutorials/getting_started/project_setup_tutorial.ipynb | Added documentation and code examples for the new plot colors configuration feature, including usage examples and best practices. |
Comments suppressed due to low confidence (1)
pytheranostics/plots/plots.py:430
- The mask shape check and transpose logic is unclear and potentially fragile. The code checks if
mip_mask.shape != spect_mip.shapeand transposes if they don't match, but this assumes that a shape mismatch means the dimensions are swapped. This could fail if the shapes differ for other reasons. Additionally, after the conditional transpose, the code always transposes again withnumpy.transpose(mip_mask, (1, 0))in the contour call. This double-transpose logic is confusing and error-prone. Consider refactoring to have a single, clear transformation based on the expected input format.
mip_mask = mask.max(axis=0)
if mip_mask.shape != spect_mip.shape:
mip_mask = mip_mask.T
plt.contour(
numpy.transpose(mip_mask, (1, 0)),
levels=[0.5],
colors=[color],
linewidths=1.5,
alpha=0.5,
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| plt.xlim(xlim_min, xlim_max) | ||
| plt.ylim(ylim_min, ylim_max) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The automatic bounds are calculated in transposed coordinates (from spect_mip.T) but then applied to the non-transposed image extent. Since imshow displays spect_mip_scaled.T, the x and y limits should account for the transpose. The current implementation uses xlim_min, xlim_max for the x-axis and ylim_min, ylim_max for the y-axis, but these were computed from transposed coordinates where rows became the first dimension and columns became the second. This coordinate mismatch may lead to incorrect cropping or clipping of the displayed image.
| plt.xlim(xlim_min, xlim_max) | |
| plt.ylim(ylim_min, ylim_max) | |
| # Limits were computed in coordinates of spect_mip.T, while the image | |
| # is displayed as spect_mip_scaled.T; swap to match display axes. | |
| plt.xlim(ylim_min, ylim_max) | |
| plt.ylim(xlim_min, xlim_max) |
| config_file = files( | ||
| "pytheranostics.data.configuration_templates" | ||
| ).joinpath("voi_mappings_config.json") | ||
|
|
||
| with config_file.open("r") as f: | ||
| config = json.load(f) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The files() API from importlib.resources returns a Traversable object that may not have an .open() method in all Python versions. When config_file is assigned from files().joinpath(), it's a Traversable, not a Path object. While this typically works, it would be more robust to explicitly convert it or use as_file() context manager for compatibility across Python versions.
| aspect=data_aspect, | ||
| ) | ||
|
|
||
| # Add colorbar if requested |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When ax is provided by the caller and colorbar=True, the colorbar is added to the provided axes. However, if the caller didn't allocate space for the colorbar in their figure layout, this can cause the plot to be resized or overlap with other subplots. Consider documenting this behavior in the parameter description or adding a note about potential layout issues when using an existing axes with colorbar enabled.
| # Add colorbar if requested | |
| # Add colorbar if requested. | |
| # Note: when an external ``ax`` is provided, the colorbar is attached to that | |
| # axes via ``ax=ax`` and may change its size/layout. Callers using custom | |
| # subplot arrangements should reserve space for the colorbar accordingly. |
| from importlib.resources import files | ||
| except ImportError: | ||
| from importlib_resources import files |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The files() function from importlib.resources was added in Python 3.9, but the project supports Python 3.8+ (as specified in pyproject.toml). For Python 3.8 compatibility, you need to either:
- Add
importlib_resourcesas a dependency for Python < 3.9 in pyproject.toml (e.g.,"importlib_resources; python_version<'3.9'"), or - Use the older
importlib.resources.read_text()API that works in Python 3.7+, or - Update the minimum Python version to 3.9+.
Without this, the code will fail on Python 3.8 with an ImportError when trying to use the fallback import.
| from importlib.resources import files | |
| except ImportError: | |
| from importlib_resources import files | |
| # Python 3.9+: importlib.resources.files is available in the standard library | |
| from importlib.resources import files | |
| except ImportError: | |
| # Older Python versions: try the backport, and if that is also missing, | |
| # provide a clear error when `files` is used. | |
| try: | |
| from importlib_resources import files # type: ignore[import] | |
| except ImportError: | |
| def files(*args, **kwargs): | |
| raise ImportError( | |
| "importlib.resources.files is not available in this Python version " | |
| "and the 'importlib_resources' backport is not installed. " | |
| "Install 'importlib_resources' or upgrade to Python 3.9+." | |
| ) |
| # Calculate aspect ratio for proper physical scaling | ||
| if spacing is not None: | ||
| # spacing is (x, y, z) in mm | ||
| # For proper aspect ratio: aspect = dy/dx | ||
| data_aspect = spacing[1] / spacing[0] # y-spacing / x-spacing |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The aspect ratio calculation uses spacing[1] / spacing[0] (y-spacing / x-spacing), but the comment says "spacing is (x, y, z)". The order of indices should be verified against the DICOM standard. DICOM PixelSpacing is typically (row spacing, column spacing) which corresponds to (y, x) in array indexing, not (x, y). This could lead to incorrect aspect ratios if the spacing parameter follows DICOM conventions. Clarify the expected order in the parameter documentation and ensure consistency.
| def plot_MIP_with_mask_outlines( | ||
| SPECT, | ||
| masks=None, | ||
| vmax=300000, | ||
| label=None, | ||
| save_path=None, | ||
| dpi=300, | ||
| ax=None, | ||
| figsize=None, | ||
| spacing=None, | ||
| mask_colors=None, | ||
| config_path=None, | ||
| colorbar=False, | ||
| units=None, | ||
| ): |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a breaking change to the function signature. The original function had ax as the first positional parameter, but now SPECT is first. This will break any existing code that calls plot_MIP_with_mask_outlines(ax, SPECT, ...) with positional arguments. All existing calls would need to be updated, or the change should be handled more carefully to maintain backwards compatibility (e.g., by deprecating the old signature first or using keyword-only arguments).
| else: | ||
| # plot_colors key missing, use fallback | ||
| color_map = fallback_defaults | ||
| except (FileNotFoundError, json.JSONDecodeError, KeyError): |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exception handling catches KeyError but this exception is not actually raised by the code in the try block. The code only accesses config["plot_colors"] with an if "plot_colors" in config check, so KeyError won't be raised. The KeyError in the except clause is likely unnecessary and could be removed for clarity.
| except (FileNotFoundError, json.JSONDecodeError, KeyError): | |
| except (FileNotFoundError, json.JSONDecodeError): |
| from .plots import ewin_montage, plot_MIP_with_mask_outlines, plot_tac_residuals | ||
|
|
||
| __all__ = [ | ||
| "ewin_montage", | ||
| "plot_MIP_with_mask_outlines", | ||
| "plot_tac_residuals", | ||
| ] |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function plot_MIP_with_mask_outlines is exported in pytheranostics.plots.__all__ but is not included in the top-level pytheranostics.__all__. This means it won't be available when users do from pytheranostics import *. Consider either adding it to the top-level __all__ for consistency with other plotting functions like ewin_montage and plot_tac_residuals, or import it at the top level like the other plotting functions (line 11 of pytheranostics/__init__.py only imports ewin_montage and plot_tac_residuals).
| The axes object containing the plot. | ||
| """ | ||
| plt.sca(ax) | ||
| plt.sca(ax) if ax is not None else None |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line creates a side effect even when it evaluates to None, which is unnecessary. The ternary operator should be restructured to avoid the redundant statement. Consider using a simple if statement instead: if ax is not None: plt.sca(ax)
| plt.sca(ax) if ax is not None else None | |
| if ax is not None: | |
| plt.sca(ax) |
| if config_path is not None: | ||
| # User-provided config path | ||
| config_file = Path(config_path) |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When config_path is provided as a string or Path, the code doesn't verify if the file exists before trying to open it. This could result in a FileNotFoundError that gets caught and falls back to the default colors, which might not be the intended behavior. If a user explicitly provides a config path, they likely expect the function to use that specific file or raise a clear error if it's not found. Consider handling the user-provided path differently from the auto-discovered paths.
Significantly improved the
plot_MIP_with_mask_outlinesfunction to support flexible unit handling, automatic figure creation, and physical DICOM-based scaling.