codebook / potato /export /mask_exporter.py
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
4.77 kB
"""
Mask Exporter
Exports segmentation mask annotations as PNG binary images.
Each label gets a separate PNG where filled pixels are the label color
and background is transparent.
Requires: numpy and Pillow (PIL)
"""
import os
import logging
from typing import Optional, Tuple, List
from .base import BaseExporter, ExportContext, ExportResult
from .cv_utils import (
extract_image_annotations,
get_image_dimensions,
get_image_filename,
build_category_mapping,
decode_rle,
)
logger = logging.getLogger(__name__)
class MaskExporter(BaseExporter):
format_name = "mask_png"
description = "Segmentation masks as PNG images (requires Pillow)"
file_extensions = [".png"]
def can_export(self, context: ExportContext) -> Tuple[bool, str]:
# Check for Pillow
try:
from PIL import Image
except ImportError:
return False, "Pillow (PIL) is required for mask export. Install with: pip install Pillow"
has_image_schema = any(
s.get("annotation_type") == "image_annotation"
for s in context.schemas
)
if not has_image_schema:
return False, "No image_annotation schema found in config"
return True, ""
def export(self, context: ExportContext, output_path: str,
options: Optional[dict] = None) -> ExportResult:
from PIL import Image
options = options or {}
warnings = []
files_written = []
os.makedirs(output_path, exist_ok=True)
category_map = build_category_mapping(context.annotations, context.schemas)
# Assign colors to categories
default_colors = [
(255, 0, 0), (0, 255, 0), (0, 0, 255),
(255, 255, 0), (255, 0, 255), (0, 255, 255),
(128, 0, 0), (0, 128, 0), (0, 0, 128),
(128, 128, 0),
]
category_colors = {}
for name, idx in category_map.items():
category_colors[name] = default_colors[idx % len(default_colors)]
masks_exported = 0
for ann in context.annotations:
instance_id = ann.get("instance_id", "")
item = context.items.get(instance_id, {})
img_anns = extract_image_annotations(ann)
if not img_anns:
continue
width, height = get_image_dimensions(item)
if width <= 0 or height <= 0:
# Try to get from mask RLE size
for _, objects in img_anns:
for obj in objects:
if obj.get("type") == "mask" and "rle" in obj:
size = obj["rle"].get("size", [])
if len(size) == 2:
height, width = size
break
if width > 0:
break
if width <= 0 or height <= 0:
warnings.append(f"No dimensions for {instance_id}, skipping masks")
continue
file_name = get_image_filename(item) or instance_id
raw_stem = os.path.splitext(os.path.basename(file_name))[0]
stem = "".join(c if c.isalnum() or c in "-_." else "_" for c in raw_stem)
for schema_name, objects in img_anns:
for obj in objects:
if obj.get("type") != "mask":
continue
label = obj.get("label", "unknown")
rle = obj.get("rle", {})
if not rle.get("counts"):
continue
mask_data = decode_rle(rle, width, height)
color = category_colors.get(label, (255, 255, 255))
# Create RGBA image
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
pixels = img.load()
for i, val in enumerate(mask_data):
if val:
y = i // width
x = i % width
if x < width and y < height:
pixels[x, y] = (color[0], color[1], color[2], 200)
safe_label = "".join(c if c.isalnum() or c in "-_." else "_" for c in label)
mask_file = os.path.join(output_path, f"{stem}_{safe_label}_mask.png")
img.save(mask_file)
files_written.append(mask_file)
masks_exported += 1
return ExportResult(
success=True,
format_name=self.format_name,
files_written=files_written,
warnings=warnings,
stats={"num_masks": masks_exported},
)