Spaces:
Sleeping
Sleeping
| """ | |
| utils.py | |
| -------- | |
| Image I/O and visualization helpers: | |
| * safe image loading / validation | |
| * colored prediction mask (dark gray / bright yellow) | |
| * semi-transparent magenta crop overlay | |
| * combined three-panel image + PNG export for download | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError | |
| # --------------------------------------------------------------------------- # | |
| # Visualization palette | |
| # --------------------------------------------------------------------------- # | |
| COLOR_BACKGROUND = (50, 50, 50) # dark gray -> Class 0 (No Crop) | |
| COLOR_CROP = (255, 255, 0) # bright yellow -> Class 1 (Crop) | |
| OVERLAY_COLOR = (255, 0, 255) # magenta overlay for crop regions | |
| OVERLAY_ALPHA = 0.5 # overlay transparency (0..1) | |
| class ImageLoadError(Exception): | |
| """Raised when an uploaded image is invalid or corrupted.""" | |
| def load_image(file) -> Image.Image: | |
| """ | |
| Safely load an uploaded file into an RGB PIL image. | |
| Raises ImageLoadError on invalid / corrupted input. | |
| """ | |
| try: | |
| image = Image.open(file) | |
| image.load() # force a full decode so corrupted files fail here | |
| except (UnidentifiedImageError, OSError, ValueError) as exc: | |
| raise ImageLoadError( | |
| "The uploaded file is not a valid image or is corrupted." | |
| ) from exc | |
| return image.convert("RGB") | |
| def create_color_mask(mask: np.ndarray) -> Image.Image: | |
| """Convert a {0, 1} mask into an RGB image (dark gray / bright yellow).""" | |
| h, w = mask.shape | |
| color = np.zeros((h, w, 3), dtype=np.uint8) | |
| color[mask == 0] = COLOR_BACKGROUND | |
| color[mask == 1] = COLOR_CROP | |
| return Image.fromarray(color, mode="RGB") | |
| def create_overlay(original: Image.Image, mask: np.ndarray) -> Image.Image: | |
| """Blend a semi-transparent magenta layer over predicted crop regions.""" | |
| base = np.asarray(original.convert("RGB"), dtype=np.float32) | |
| crop_pixels = mask == 1 | |
| overlay = base.copy() | |
| overlay[crop_pixels] = ( | |
| (1.0 - OVERLAY_ALPHA) * base[crop_pixels] | |
| + OVERLAY_ALPHA * np.array(OVERLAY_COLOR, dtype=np.float32) | |
| ) | |
| return Image.fromarray(overlay.clip(0, 255).astype(np.uint8), mode="RGB") | |
| def _load_font(size: int): | |
| """Load a default font, gracefully handling older Pillow versions.""" | |
| try: | |
| return ImageFont.load_default(size=size) | |
| except TypeError: # Pillow without the size argument | |
| return ImageFont.load_default() | |
| def combine_panels( | |
| original: Image.Image, | |
| color_mask: Image.Image, | |
| overlay: Image.Image, | |
| gap: int = 12, | |
| label_height: int = 36, | |
| ) -> Image.Image: | |
| """ | |
| Combine the three panels horizontally into a single labeled image. | |
| All panels are resized to identical dimensions (the original image size) | |
| before being concatenated. | |
| """ | |
| target_size = original.size # (w, h) | |
| panels = [ | |
| ("Original", original.resize(target_size)), | |
| ("Predicted Mask", color_mask.resize(target_size)), | |
| ("Crop Overlay", overlay.resize(target_size)), | |
| ] | |
| w, h = target_size | |
| total_w = w * 3 + gap * 2 | |
| total_h = h + label_height | |
| canvas = Image.new("RGB", (total_w, total_h), (255, 255, 255)) | |
| draw = ImageDraw.Draw(canvas) | |
| font = _load_font(20) | |
| x = 0 | |
| for label, img in panels: | |
| canvas.paste(img, (x, label_height)) | |
| try: | |
| text_w = draw.textlength(label, font=font) | |
| except AttributeError: # very old Pillow | |
| text_w = len(label) * 6 | |
| draw.text( | |
| (x + (w - text_w) / 2, max(0, (label_height - 20) / 2)), | |
| label, | |
| fill=(0, 0, 0), | |
| font=font, | |
| ) | |
| x += w + gap | |
| return canvas | |
| def image_to_png_bytes(image: Image.Image) -> bytes: | |
| """Encode a PIL image as PNG bytes (used by the download button).""" | |
| buffer = io.BytesIO() | |
| image.save(buffer, format="PNG") | |
| return buffer.getvalue() | |