image-cleanup / app.py
kuko6's picture
Improved image cleaning
eab7c24
Raw
History Blame Contribute Delete
7.8 kB
import tempfile
import zipfile
from pathlib import Path
from uuid import uuid4
import cv2
import gradio as gr
import numpy as np
from cleaning import (
clean_display_image_and_mask,
cleaned_image_name,
read_image_preview_rgb,
read_image_rgb_and_preview,
write_image_rgb,
)
_BATCH_OUTPUTS = tempfile.TemporaryDirectory(prefix="ihc-cleaner-")
_EXAMPLE_IMAGES = [["data/example.png"], ["data/example.tiff"]]
_IMAGE_FILE_TYPES = ["image", ".tif", ".tiff"]
_PREVIEW_MAX_DIMENSION = 1200
_MASK_OUTLINE_KERNEL_SIZE = 3
def _uploaded_path(file_path: str | Path | None) -> Path:
if not file_path:
raise ValueError("Upload an input image.")
return Path(file_path)
def _preview_image(image):
height, width = image.shape[:2]
largest_dimension = max(height, width)
if largest_dimension <= _PREVIEW_MAX_DIMENSION:
return image
scale = _PREVIEW_MAX_DIMENSION / largest_dimension
preview_size = (round(width * scale), round(height * scale))
return cv2.resize(image, preview_size, interpolation=cv2.INTER_AREA)
def _mask_outline(image: np.ndarray, debris_mask: np.ndarray) -> np.ndarray:
outline_kernel = np.ones(
(_MASK_OUTLINE_KERNEL_SIZE, _MASK_OUTLINE_KERNEL_SIZE),
dtype=np.uint8,
)
boundary = cv2.morphologyEx(
debris_mask,
cv2.MORPH_GRADIENT,
outline_kernel,
) > 0
outlined = image.copy()
outlined[boundary] = [0, 255, 0]
return outlined
def preview_image(file_path: str | Path | None):
if not file_path:
return None, None, None, None, None
return _preview_image(read_image_preview_rgb(file_path)), None, None, None, None
def clean_uploaded_image(file_path: str | Path | None):
image_path = _uploaded_path(file_path)
output_dir = Path(_BATCH_OUTPUTS.name) / uuid4().hex
output_dir.mkdir(parents=True)
image_rgb, display_rgb = read_image_rgb_and_preview(image_path)
cleaned_display_rgb, debris_mask = clean_display_image_and_mask(
image_rgb,
display_rgb,
)
input_preview = _preview_image(display_rgb)
preview_mask = debris_mask
if preview_mask.shape != input_preview.shape[:2]:
preview_mask = cv2.resize(
preview_mask,
(input_preview.shape[1], input_preview.shape[0]),
interpolation=cv2.INTER_NEAREST,
)
mask_outline = _mask_outline(input_preview, preview_mask)
cleaned_path = output_dir / cleaned_image_name(image_path)
mask_path = output_dir / f"{image_path.stem}_debris_mask.png"
write_image_rgb(cleaned_path, cleaned_display_rgb)
if not cv2.imwrite(str(mask_path), debris_mask):
raise OSError(f"Could not write debris mask: {mask_path}")
return (
input_preview,
mask_outline,
_preview_image(cleaned_display_rgb),
str(mask_path),
str(cleaned_path),
)
def clean_directory(image_paths: list[str] | None) -> tuple[str, str]:
if not image_paths:
raise ValueError("Upload a directory containing images.")
batch_dir = Path(_BATCH_OUTPUTS.name) / uuid4().hex
cleaned_dir = batch_dir / "cleaned"
cleaned_dir.mkdir(parents=True)
used_names: set[str] = set()
for image_path_string in image_paths:
image_path = Path(image_path_string)
image_rgb, display_rgb = read_image_rgb_and_preview(image_path)
cleaned_rgb, _ = clean_display_image_and_mask(image_rgb, display_rgb)
output_name = cleaned_image_name(image_path, used_names)
write_image_rgb(cleaned_dir / output_name, cleaned_rgb)
archive_path = batch_dir / "cleaned_images.zip"
with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive:
for output_path in sorted(cleaned_dir.iterdir()):
archive.write(output_path, arcname=output_path.name)
count = len(used_names)
return (
str(archive_path),
f"Created cleaned images for {count} "
f"image{'s' if count != 1 else ''}.",
)
def build_app() -> gr.Blocks:
with gr.Blocks(title="Image Cleanup") as app:
gr.Markdown(
"# Image Cleanup\n"
"Upload an input image to remove dark, low-saturation artifacts."
)
with gr.Tab("Single image"):
with gr.Row():
input_image = gr.File(
label="Input image",
file_types=_IMAGE_FILE_TYPES,
type="filepath",
)
mask_file = gr.File(
label="Debris mask",
interactive=False,
)
cleaned_file = gr.File(
label="Cleaned image file",
interactive=False,
)
with gr.Row():
input_preview = gr.Image(
label="Input preview",
format="png",
buttons=["fullscreen"],
interactive=False,
)
outline_preview = gr.Image(
label="Mask outline (green = boundary)",
format="png",
buttons=["fullscreen"],
interactive=False,
)
cleaned_preview = gr.Image(
label="Cleaned image preview",
format="png",
buttons=["fullscreen"],
interactive=False,
)
with gr.Row():
clean_button = gr.Button(
"Clean image",
variant="primary",
)
gr.ClearButton(
[
input_image,
input_preview,
outline_preview,
cleaned_preview,
mask_file,
cleaned_file,
]
)
gr.Examples(
examples=_EXAMPLE_IMAGES,
inputs=input_image,
label="Example image",
)
input_image.change(
fn=preview_image,
inputs=input_image,
outputs=[
input_preview,
outline_preview,
cleaned_preview,
mask_file,
cleaned_file,
],
api_name=False,
)
clean_button.click(
fn=clean_uploaded_image,
inputs=input_image,
outputs=[
input_preview,
outline_preview,
cleaned_preview,
mask_file,
cleaned_file,
],
api_name="clean_image",
)
with gr.Tab("Image directory"):
directory_input = gr.File(
label="Image directory",
file_count="directory",
file_types=_IMAGE_FILE_TYPES,
type="filepath",
)
clean_directory_button = gr.Button(
"Clean directory",
variant="primary",
)
batch_status = gr.Textbox(label="Status", interactive=False)
batch_download = gr.File(
label="Cleaned images",
interactive=False,
)
clean_directory_button.click(
fn=clean_directory,
inputs=directory_input,
outputs=[batch_download, batch_status],
api_name="clean_directory",
)
return app
demo = build_app()
if __name__ == "__main__":
demo.launch(ssr_mode=False)