-
Notifications
You must be signed in to change notification settings - Fork 1
chore: JSONMultiClassMask #47
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| from ratiopath.parsers.asap_parser import ASAPParser | ||
| from ratiopath.parsers.darwin7_json_parser import Darwin7JSONParser | ||
| from ratiopath.parsers.geojson_parser import GeoJSONParser | ||
| from ratiopath.parsers.json_multi_class_mask import JSONMultiClassMask | ||
|
|
||
|
|
||
| __all__ = ["ASAPParser", "Darwin7JSONParser", "GeoJSONParser"] | ||
| __all__ = ["ASAPParser", "Darwin7JSONParser", "GeoJSONParser", "JSONMultiClassMask"] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,45 @@ | ||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||||||||
| from PIL import Image, ImageDraw | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| class JSONMultiClassMask: | ||||||||||||||||||||||||||||||||||
| """Parses JSON annotations and renders them into a multi-class mask array. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Expects class to value mapping on input. | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||
| json_path: Path, | ||||||||||||||||||||||||||||||||||
| mask_size: tuple[int, int], | ||||||||||||||||||||||||||||||||||
| scale_factor: float, | ||||||||||||||||||||||||||||||||||
| target_groups: dict[str, int], | ||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||
| self.json_path = json_path | ||||||||||||||||||||||||||||||||||
| self.mask_size = mask_size # (width, height) | ||||||||||||||||||||||||||||||||||
| self.scale = scale_factor | ||||||||||||||||||||||||||||||||||
| self.target_groups = target_groups | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def __call__(self) -> np.ndarray: | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||
| mask_img = Image.new("L", self.mask_size, 0) | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: For Pillow 11.1.0, the valid fill value range for Image mode "L" in ImageDraw.polygon is integers 0-255. "L" mode is 8-bit pixels, grayscale, where each pixel uses the full range 0-255. Citations:
🏁 Script executed: cat -n ratiopath/parsers/json_multi_class_mask.py | head -60Repository: RationAI/ratiopath Length of output: 1662 🌐 Web query:
💡 Result: In Pillow's ImageDraw.polygon for an 8-bit "L" mode image, if the fill value exceeds the valid range (0-255), it undergoes truncation to the lower 8 bits. This is equivalent to a modulo 256 operation for positive integers, as the C implementation casts the integer ink value to UINT8, discarding higher bits. Citations:
Validate class IDs against 8-bit mask limits to prevent class collision. Line 27 creates an "L" mode mask (8-bit grayscale, values 0–255), but Line 41 can pass arbitrary integer class values from For example, class IDs 256 and 0 would both render as 0; 257 and 1 would both render as 1. Add validation in the constructor to ensure all Suggested guard def __init__(
self,
json_path: Path,
mask_size: tuple[int, int],
scale_factor: float,
target_groups: dict[str, int],
):
+ invalid = {k: v for k, v in target_groups.items() if not (0 <= v <= 255)}
+ if invalid:
+ raise ValueError(
+ f"target_groups values must be in [0, 255] for mode 'L'. Got: {invalid}"
+ )
self.json_path = json_path
self.mask_size = mask_size # (width, height)
self.scale = scale_factor
self.target_groups = target_groups🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| draw = ImageDraw.Draw(mask_img) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+28
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| with open(self.json_path) as f: | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| with open(self.json_path) as f: | |
| with open(self.json_path, encoding="utf-8") as f: |
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.
To make the file loading more robust and user-friendly, it's beneficial to explicitly handle FileNotFoundError and json.JSONDecodeError. This provides clearer error messages to the user if the JSON file is missing or malformed, improving the debugging experience.
try:
with open(self.json_path) as f:
data = json.load(f)
except FileNotFoundError:
raise FileNotFoundError(f"JSON file not found at {self.json_path}")
except json.JSONDecodeError:
raise ValueError(f"Invalid JSON format in file {self.json_path}")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.
Harden coordinate parsing to avoid crashing on malformed items.
Line 42 assumes every coordinate is an indexable 2-item numeric pair. A single malformed polygon point can raise TypeError/IndexError and abort the entire parse.
✅ Suggested robust parsing
- scaled_coords = [(c[0] * self.scale, c[1] * self.scale) for c in coords]
+ try:
+ scaled_coords = [
+ (float(c[0]) * self.scale, float(c[1]) * self.scale)
+ for c in coords
+ if isinstance(c, (list, tuple)) and len(c) >= 2
+ ]
+ except (TypeError, ValueError) as exc:
+ raise ValueError(
+ f"Invalid coordinates for class '{class_name}' in {self.json_path}"
+ ) from exc
+
+ if len(scaled_coords) < 3:
+ continue
draw.polygon(scaled_coords, fill=fill_value)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| scaled_coords = [(c[0] * self.scale, c[1] * self.scale) for c in coords] | |
| draw.polygon(scaled_coords, fill=fill_value) | |
| try: | |
| scaled_coords = [ | |
| (float(c[0]) * self.scale, float(c[1]) * self.scale) | |
| for c in coords | |
| if isinstance(c, (list, tuple)) and len(c) >= 2 | |
| ] | |
| except (TypeError, ValueError) as exc: | |
| raise ValueError( | |
| f"Invalid coordinates for class '{class_name}' in {self.json_path}" | |
| ) from exc | |
| if len(scaled_coords) < 3: | |
| continue | |
| draw.polygon(scaled_coords, fill=fill_value) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ratiopath/parsers/json_multi_class_mask.py` around lines 42 - 43, The
comprehension building scaled_coords in json_multi_class_mask.py assumes every
item in coords is a 2-element numeric sequence and will crash on malformed
entries; update the parsing in the block that creates scaled_coords (and before
calling draw.polygon) to validate and sanitize each point: for each c in coords
ensure it is indexable with at least two elements and that c[0] and c[1] can be
converted to numbers (e.g., float), skip or log any invalid points, and only
include validated (x* self.scale, y* self.scale) pairs in scaled_coords so
draw.polygon receives a clean list and the parser doesn't raise
TypeError/IndexError on bad input.
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.
It's good practice to validate input parameters to ensure they meet the expected criteria. The
mask_sizeshould be a tuple of two positive integers. Adding this check improves the robustness of the class by preventing potential errors later if invalid dimensions are provided.