AI_Lab_CAM / app.py
Aditya7864's picture
readme update
7023f81
Raw
History Blame Contribute Delete
26.8 kB
"""CV Lab Camera: Gradio scientific camera and image filtering lab."""
from __future__ import annotations
import json
import tempfile
from pathlib import Path
from typing import Any
import gradio as gr
import numpy as np
from PIL import Image
from batch.dataset_processor import process_dataset
from cv_ops.analysis import histogram_figure, low_resolution_pair, pixel_preview, stats
from cv_ops.morphology import apply_morphology
from cv_ops.transforms import reflect, rotate, scale_image, translate
from filters.builtin import BUILTIN_FILTERS, ensure_rgb
from filters.registry import apply_definition, apply_step, delete_filter, load_definition, names, operation_to_definition, save_filter
from models.face_filters import apply_face_filter, ar_filter_names, delete_ar_filter, load_ar_definition, save_ar_filter
from models.style_transfer import stylize
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
ROOT = Path(__file__).resolve().parent
STYLE_SAMPLES = sorted(str(p) for p in (ROOT / "assets").glob("style_*.png"))
DEFAULT_CUSTOM_AR_JSON = json.dumps(
{
"elements": [
{"landmark": "forehead", "shape": "crown", "color": [255, 215, 0], "scale": 1.0, "offset_y": -0.15},
{"landmark": "eyes", "shape": "visor", "color": [0, 255, 255], "scale": 1.0},
{"landmark": "mouth", "shape": "mustache", "color": [40, 20, 20], "scale": 0.9, "offset_y": -0.05},
]
},
indent=2,
)
def export_image(image: np.ndarray | None) -> str | None:
if image is None:
return None
arr = np.asarray(image)
if arr.ndim == 2:
img_obj = Image.fromarray(arr)
else:
img_obj = Image.fromarray(arr.astype(np.uint8))
out_dir = Path(tempfile.mkdtemp(prefix="cv_lab_dl_"))
filepath = out_dir / "cv_lab_result.png"
img_obj.save(filepath)
return str(filepath)
def set_global_image(image: np.ndarray | None) -> tuple[np.ndarray | None, np.ndarray | None, str]:
if image is None:
return None, None, "No image loaded."
img = ensure_rgb(image)
return img, img, f"Loaded image: {img.shape[1]} x {img.shape[0]}"
def make_code_snippet(operation: str, params: dict[str, Any]) -> str:
params_str = json.dumps(params, indent=2)
return f"""import numpy as np
from PIL import Image
from filters.registry import apply_step
image = np.array(Image.open("input.png").convert("RGB"))
params = {params_str}
result = apply_step(image, "{operation}", params)
Image.fromarray(result).save("output.png")
"""
def run_builtin(image: np.ndarray | None, filter_name: str, blur_method: str, kernel_size: int, edge_method: str, low: int, high: int, brightness: int, contrast: float, saturation: float, hue_shift: int, channel: str) -> tuple[np.ndarray | None, dict[str, Any], str]:
if image is None:
raise gr.Error("Load or capture an image first.")
params = {
"method": blur_method if filter_name == "Blur" else edge_method,
"kernel_size": kernel_size,
"low": low,
"high": high,
"brightness": brightness,
"contrast": contrast,
"saturation": saturation,
"hue_shift": hue_shift,
"channel": channel,
}
result = apply_step(image, filter_name, params)
definition = operation_to_definition(filter_name, params)
code = make_code_snippet(filter_name, params)
return result, definition, code
def preview_kernel(image: np.ndarray | None, kernel_text: str) -> tuple[np.ndarray | None, dict[str, Any], str]:
if image is None:
raise gr.Error("Load or capture an image first.")
kernel = parse_kernel(kernel_text)
definition = {"type": "kernel", "kernel": kernel}
code = f"""import numpy as np
from PIL import Image
from filters.builtin import custom_kernel
image = np.array(Image.open("input.png").convert("RGB"))
kernel = {json.dumps(kernel)}
result = custom_kernel(image, kernel=kernel)
Image.fromarray(result).save("output.png")
"""
return apply_definition(image, definition), definition, code
def preview_pipeline(image: np.ndarray | None, pipeline_text: str) -> tuple[np.ndarray | None, dict[str, Any], str]:
if image is None:
raise gr.Error("Load or capture an image first.")
definition = parse_pipeline(pipeline_text)
code = f"""import numpy as np
from PIL import Image
from filters.registry import apply_definition
image = np.array(Image.open("input.png").convert("RGB"))
pipeline = {json.dumps(definition, indent=2)}
result = apply_definition(image, pipeline)
Image.fromarray(result).save("output.png")
"""
return apply_definition(image, definition), definition, code
def save_current_filter(name: str | None, definition: dict[str, Any] | None) -> tuple[str, gr.Dropdown]:
if not name or not name.strip():
raise gr.Error("Enter a filter name before saving.")
if not definition:
raise gr.Error("Preview a built-in, kernel, or pipeline filter before saving.")
saved = save_filter(name, definition)
return f"Saved filter '{saved['name']}'.", gr.Dropdown(choices=names(False), value=saved["name"])
def load_saved_filter(image: np.ndarray | None, name: str | None) -> tuple[np.ndarray | None, dict[str, Any], str]:
if not name:
raise gr.Error("Select a saved filter from the dropdown to load.")
if image is None:
raise gr.Error("Load or capture an image first.")
try:
definition = load_definition(name)
except Exception as exc:
raise gr.Error(str(exc))
return apply_definition(image, definition), definition, json.dumps(definition, indent=2)
def delete_saved(name: str | None) -> tuple[str, gr.Dropdown]:
if not name:
raise gr.Error("Select a saved filter from the dropdown to delete.")
delete_filter(name)
return f"Deleted '{name}'.", gr.Dropdown(choices=names(False), value=None)
def analyze(image: np.ndarray | None, percent: int, x: int, y: int):
if image is None:
raise gr.Error("Load or capture an image first.")
high_rgb, low_rgb, high_gray, low_gray = low_resolution_pair(image, percent)
return high_rgb, low_rgb, high_gray, low_gray, pixel_preview(high_rgb, x, y), pixel_preview(low_rgb, x, y), histogram_figure(high_rgb, low_rgb), {"high_rgb": stats(high_rgb), "low_rgb": stats(low_rgb), "high_gray": stats(high_gray), "low_gray": stats(low_gray)}
def transform(image: np.ndarray | None, op: str, tx: int, ty: int, border: str, angle: float, scale: float, cx: float, cy: float, expand: bool, sx: float, sy: float, interp: str, flip: str):
if image is None:
raise gr.Error("Load or capture an image first.")
if op == "Translation":
return translate(image, tx, ty, border)
if op == "Rotation":
return rotate(image, angle, scale, cx, cy, expand)
if op == "Scaling":
return scale_image(image, sx, sy, interp)
return reflect(image, flip)
def morph(image: np.ndarray | None, operation: str, threshold_method: str, threshold: int, shape: str, size: int, iterations: int):
if image is None:
raise gr.Error("Load or capture an image first.")
result, kernel = apply_morphology(image, operation, shape, size, iterations, threshold_method, threshold)
return result, kernel.tolist()
def run_style(image: np.ndarray | None, style_upload: np.ndarray | None, style_path: str | None, max_size: int):
if image is None:
raise gr.Error("Load or capture a content image first.")
if style_upload is None and not style_path:
raise gr.Error("Upload a style image or choose a sample style.")
style = style_upload if style_upload is not None else np.asarray(Image.open(style_path).convert("RGB"))
result, seconds, message = stylize(image, style, max_size)
return result, f"{message} Inference time: {seconds:.2f}s"
def run_face(image: np.ndarray | None, filter_name: str):
if image is None:
raise gr.Error("Load or capture an image first.")
return apply_face_filter(image, filter_name)
def preview_ar_custom(image: np.ndarray | None, custom_json: str | None) -> tuple[np.ndarray | None, str]:
if image is None:
raise gr.Error("Load or capture an image first.")
if not custom_json or not custom_json.strip():
raise gr.Error("Enter a custom AR filter JSON definition.")
try:
definition = json.loads(custom_json)
except Exception as exc:
raise gr.Error(f"Invalid AR filter JSON: {exc}")
return apply_face_filter(image, filter_name="Custom", custom_def=definition)
def save_custom_ar_filter(name: str | None, custom_json: str | None) -> tuple[str, gr.Dropdown, gr.Dropdown]:
if not name or not name.strip():
raise gr.Error("Enter an AR filter name before saving.")
if not custom_json or not custom_json.strip():
raise gr.Error("Enter a custom AR filter JSON definition.")
try:
definition = json.loads(custom_json)
except Exception as exc:
raise gr.Error(f"Invalid AR filter JSON: {exc}")
saved = save_ar_filter(name, definition)
return f"Saved AR filter '{saved['name']}'.", gr.Dropdown(choices=ar_filter_names(True), value=saved["name"]), gr.Dropdown(choices=ar_filter_names(False), value=saved["name"])
def load_saved_ar_filter(image: np.ndarray | None, name: str | None) -> tuple[np.ndarray | None, str, str]:
if not name:
raise gr.Error("Select a saved AR filter from the dropdown to load.")
if image is None:
raise gr.Error("Load or capture an image first.")
try:
definition = load_ar_definition(name)
except Exception as exc:
raise gr.Error(str(exc))
result, msg = apply_face_filter(image, filter_name=name)
return result, json.dumps(definition, indent=2), msg
def delete_saved_ar_filter(name: str | None) -> tuple[str, gr.Dropdown, gr.Dropdown]:
if not name:
raise gr.Error("Select a saved AR filter from the dropdown to delete.")
delete_ar_filter(name)
return f"Deleted AR filter '{name}'.", gr.Dropdown(choices=ar_filter_names(True), value="Glasses"), gr.Dropdown(choices=ar_filter_names(False), value=None)
def run_batch(files: list[str] | None, directory: str, filter_name: str, progress=gr.Progress()):
if not filter_name:
raise gr.Error("Choose a built-in or saved filter.")
return process_dataset(files, directory or None, filter_name, progress)
custom_theme = gr.themes.Soft(
primary_hue="indigo",
secondary_hue="cyan",
neutral_hue="slate",
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
)
with gr.Blocks(title="CV Lab Camera Studio") as demo:
current_image = gr.State()
current_definition = gr.State()
gr.Markdown(
"""# πŸ“Έ CV Lab Camera Studio
### Comprehensive Computer Vision & AR Laboratory for Photo Editors, Scientists & Developers
*Capture with your webcam or upload an image to use seamlessly across all processing modules below.*
"""
)
with gr.Row():
global_input = gr.Image(label="Global Image Input", sources=["webcam", "upload"], type="numpy")
global_preview = gr.Image(label="Active Image Canvas", type="numpy")
status = gr.Markdown("✨ Load or capture an image above to start processing across all tabs.")
global_input.change(set_global_image, global_input, [current_image, global_preview, status])
with gr.Tabs():
with gr.Tab("🎨 Photo Filters & Pipelines"):
gr.Markdown("Apply built-in visual filters, adjust parameters, or create reusable custom matrix kernels and multi-step JSON pipelines.")
with gr.Row():
before = gr.Image(value=None, label="Original Input", type="numpy")
after = gr.Image(label="Filtered Result", type="numpy")
current_image.change(lambda x: x, current_image, before)
with gr.Group():
filter_name = gr.Dropdown(list(BUILTIN_FILTERS.keys())[:-1], value="Sepia", label="Built-in Filter Selection")
with gr.Row():
blur_method = gr.Radio(["Gaussian", "Median", "Bilateral"], value="Gaussian", label="Blur Method", info="Smoothing algorithm")
kernel_size = gr.Slider(1, 31, value=7, step=2, label="Kernel Size", info="Must be an odd integer")
edge_method = gr.Radio(["Canny", "Sobel"], value="Canny", label="Edge Detection Method")
with gr.Row():
low = gr.Slider(0, 255, value=80, step=1, label="Canny Low Threshold", info="Lower hysteresis bound")
high = gr.Slider(0, 255, value=160, step=1, label="Canny High Threshold", info="Upper hysteresis bound")
brightness = gr.Slider(-100, 100, value=0, step=1, label="Brightness Shift")
contrast = gr.Slider(0.1, 3.0, value=1.0, step=0.05, label="Contrast Multiplier")
with gr.Row():
saturation = gr.Slider(0, 3, value=1, step=0.05, label="Saturation Factor")
hue_shift = gr.Slider(-90, 90, value=0, step=1, label="Hue Shift Degrees")
channel = gr.Radio(["R", "G", "B"], value="R", label="Channel Isolation")
apply_builtin = gr.Button("⚑ Apply Built-in Filter", variant="primary")
with gr.Accordion("βš™οΈ Custom Kernel & Pipeline JSON Editor", open=False):
with gr.Row():
with gr.Column():
kernel_text = gr.Textbox(value="[[0,-1,0],[-1,5,-1],[0,-1,0]]", lines=4, label="Custom 3x3 Matrix Kernel JSON")
apply_kernel = gr.Button("Preview Custom Kernel")
with gr.Column():
pipeline_text = gr.Textbox(value='[{"operation":"Grayscale","params":{}},{"operation":"Sharpen","params":{"amount":1.4}}]', lines=5, label="Multi-Step Pipeline JSON")
apply_pipeline = gr.Button("Preview Pipeline")
with gr.Row():
save_name = gr.Textbox(label="Filter Name to Save")
save_btn = gr.Button("πŸ’Ύ Save Current Filter")
saved_dropdown = gr.Dropdown(choices=names(False), label="Load Saved Filter Preset")
load_btn = gr.Button("πŸ“‚ Load Filter")
delete_btn = gr.Button("πŸ—‘οΈ Delete Filter", variant="stop")
filter_msg = gr.Markdown()
with gr.Accordion("πŸ“‹ Filter JSON Definition & Python Code Snippet", open=False):
filter_json = gr.JSON(label="Current Filter Definition JSON")
dev_code = gr.Code(label="Python Code Snippet for Developers", language="python")
download_filter = gr.DownloadButton("πŸ“₯ Download Filtered Result", value=None)
apply_builtin.click(run_builtin, [current_image, filter_name, blur_method, kernel_size, edge_method, low, high, brightness, contrast, saturation, hue_shift, channel], [after, current_definition, dev_code]).then(export_image, after, download_filter).then(lambda d: d, current_definition, filter_json)
apply_kernel.click(preview_kernel, [current_image, kernel_text], [after, current_definition, dev_code]).then(export_image, after, download_filter).then(lambda d: d, current_definition, filter_json)
apply_pipeline.click(preview_pipeline, [current_image, pipeline_text], [after, current_definition, dev_code]).then(export_image, after, download_filter).then(lambda d: d, current_definition, filter_json)
save_btn.click(save_current_filter, [save_name, current_definition], [filter_msg, saved_dropdown])
load_btn.click(load_saved_filter, [current_image, saved_dropdown], [after, current_definition, pipeline_text]).then(export_image, after, download_filter).then(lambda d: d, current_definition, filter_json)
delete_btn.click(delete_saved, saved_dropdown, [filter_msg, saved_dropdown])
with gr.Tab("πŸ”¬ Resolution & Color Analysis"):
gr.Markdown("Compare high/low resolution RGB and grayscale representations, analyze pixel crops, and inspect intensity histograms.")
with gr.Row():
percent = gr.Slider(5, 100, value=25, step=5, label="Low Resolution Percent", info="Downsample scale percentage")
px = gr.Number(value=0, precision=0, label="Pixel Crop X Coordinate")
py = gr.Number(value=0, precision=0, label="Pixel Crop Y Coordinate")
analyze_btn = gr.Button("πŸ”¬ Run Analysis", variant="primary")
with gr.Row():
high_rgb = gr.Image(label="High RGB Original", type="numpy")
low_rgb = gr.Image(label="Low RGB Upsampled", type="numpy")
with gr.Row():
high_gray = gr.Image(label="High Grayscale", type="numpy")
low_gray = gr.Image(label="Low Grayscale", type="numpy")
with gr.Row():
pix_high = gr.Dataframe(label="High RGB Pixel Values", row_count=5)
pix_low = gr.Dataframe(label="Low RGB Pixel Values", row_count=5)
hist = gr.Plot(label="Channel Intensity Histograms")
stat_json = gr.JSON(label="Detailed Image Statistics")
analyze_btn.click(analyze, [current_image, percent, px, py], [high_rgb, low_rgb, high_gray, low_gray, pix_high, pix_low, hist, stat_json])
with gr.Tab("πŸ“ Geometric Transformations"):
gr.Markdown("Apply affine geometric transformations including translation, rotation, scaling, and reflection while inspecting matrix parameters.")
op = gr.Radio(["Translation", "Rotation", "Scaling", "Reflection"], value="Rotation", label="Transformation Operation")
with gr.Group():
with gr.Row():
tx = gr.Slider(-300, 300, value=30, step=1, label="Translation X Offset (px)")
ty = gr.Slider(-300, 300, value=30, step=1, label="Translation Y Offset (px)")
border = gr.Radio(["constant", "reflect", "replicate"], value="constant", label="Border Extrapolation")
with gr.Row():
angle = gr.Slider(0, 360, value=30, step=1, label="Rotation Angle (deg)")
rot_scale = gr.Slider(0.1, 3, value=1, step=0.05, label="Rotation Scale Factor")
cx = gr.Slider(0, 1, value=0.5, step=0.05, label="Center X Ratio")
cy = gr.Slider(0, 1, value=0.5, step=0.05, label="Center Y Ratio")
expand = gr.Checkbox(value=True, label="Expand Canvas Bounds")
with gr.Row():
sx = gr.Slider(0.1, 4, value=1.2, step=0.05, label="Scale X Factor")
sy = gr.Slider(0.1, 4, value=1.2, step=0.05, label="Scale Y Factor")
interp = gr.Radio(["nearest", "linear", "cubic", "area"], value="linear", label="Interpolation Mode")
flip = gr.Radio(["horizontal", "vertical", "both"], value="horizontal", label="Reflection Axis")
trans_btn = gr.Button("πŸ“ Apply Transformation", variant="primary")
with gr.Row():
trans_before = gr.Image(label="Original Canvas", type="numpy")
trans_after = gr.Image(label="Transformed Canvas", type="numpy")
matrix = gr.JSON(label="Affine Transformation Matrix 2x3")
trans_dl = gr.DownloadButton("πŸ“₯ Download Transformed Result", value=None)
current_image.change(lambda x: x, current_image, trans_before)
trans_btn.click(transform, [current_image, op, tx, ty, border, angle, rot_scale, cx, cy, expand, sx, sy, interp, flip], [trans_after, matrix]).then(export_image, trans_after, trans_dl)
with gr.Tab("πŸ§ͺ Morphological Operations"):
gr.Markdown("Apply thresholding and mathematical morphology operations with custom structuring element kernels.")
with gr.Group():
with gr.Row():
morph_op = gr.Dropdown(["Erosion", "Dilation", "Opening", "Closing", "Gradient", "Top-Hat", "Black-Hat"], value="Opening", label="Morphological Operation")
thresh_method = gr.Radio(["Otsu", "Manual"], value="Otsu", label="Binarization Threshold Method")
thresh_value = gr.Slider(0, 255, value=128, step=1, label="Manual Threshold Value")
with gr.Row():
shape = gr.Radio(["rect", "ellipse", "cross"], value="rect", label="Kernel Structuring Shape")
morph_size = gr.Slider(1, 31, value=5, step=2, label="Kernel Size", info="Must be an odd integer")
iterations = gr.Slider(1, 10, value=1, step=1, label="Iteration Count")
morph_btn = gr.Button("πŸ§ͺ Apply Morphology", variant="primary")
with gr.Row():
morph_before = gr.Image(label="Original Input", type="numpy")
morph_after = gr.Image(label="Morphology Output", type="numpy")
kernel_view = gr.JSON(label="Structuring Element Kernel Matrix")
morph_dl = gr.DownloadButton("πŸ“₯ Download Result", value=None)
current_image.change(lambda x: x, current_image, morph_before)
morph_btn.click(morph, [current_image, morph_op, thresh_method, thresh_value, shape, morph_size, iterations], [morph_after, kernel_view]).then(export_image, morph_after, morph_dl)
with gr.Tab("🌌 Neural Style Transfer"):
gr.Markdown("Transfer artistic textures from a style reference image to your content image using the TensorFlow Hub Magenta deep neural model.")
with gr.Row():
style_upload = gr.Image(label="Custom Style Image Upload", type="numpy")
style_choice = gr.Dropdown(choices=STYLE_SAMPLES, label="Synthetic Sample Style Presets")
max_size = gr.Slider(128, 1024, value=512, step=64, label="Inference Resolution Max Size (px)", info="Higher resolution takes longer")
style_btn = gr.Button("🌌 Run Style Transfer", variant="primary")
style_out = gr.Image(label="Stylized Result", type="numpy")
style_msg = gr.Markdown()
style_dl = gr.DownloadButton("πŸ“₯ Download Stylized Result", value=None)
style_btn.click(run_style, [current_image, style_upload, style_choice, max_size], [style_out, style_msg]).then(export_image, style_out, style_dl)
with gr.Tab("🎭 Face AR Filters"):
gr.Markdown("Apply landmark-anchored AR face overlays or design, preview, and save custom JSON AR filters.")
with gr.Row():
ar_choice = gr.Dropdown(choices=ar_filter_names(True), value="Glasses", label="AR Filter Presets")
ar_btn = gr.Button("🎭 Apply AR Filter", variant="primary")
with gr.Row():
ar_before = gr.Image(label="Original Face Input", type="numpy")
ar_out = gr.Image(label="AR Overlay Result", type="numpy")
current_image.change(lambda x: x, current_image, ar_before)
with gr.Accordion("🎨 Custom AR Filter Designer & JSON Editor", open=False):
ar_custom_text = gr.Textbox(value=DEFAULT_CUSTOM_AR_JSON, lines=8, label="Custom AR Filter JSON Definition")
ar_preview_btn = gr.Button("πŸ‘οΈ Preview Custom AR Filter")
with gr.Row():
ar_save_name = gr.Textbox(label="AR Filter Name to Save")
ar_save_btn = gr.Button("πŸ’Ύ Save Custom AR Filter")
ar_saved_dropdown = gr.Dropdown(choices=ar_filter_names(False), label="Load Saved AR Preset")
ar_load_btn = gr.Button("πŸ“‚ Load AR Filter")
ar_delete_btn = gr.Button("πŸ—‘οΈ Delete AR Filter", variant="stop")
ar_msg = gr.Markdown()
ar_dl = gr.DownloadButton("πŸ“₯ Download AR Result", value=None)
ar_btn.click(run_face, [current_image, ar_choice], [ar_out, ar_msg]).then(export_image, ar_out, ar_dl)
ar_preview_btn.click(preview_ar_custom, [current_image, ar_custom_text], [ar_out, ar_msg]).then(export_image, ar_out, ar_dl)
ar_save_btn.click(save_custom_ar_filter, [ar_save_name, ar_custom_text], [ar_msg, ar_choice, ar_saved_dropdown])
ar_load_btn.click(load_saved_ar_filter, [current_image, ar_saved_dropdown], [ar_out, ar_custom_text, ar_msg]).then(export_image, ar_out, ar_dl)
ar_delete_btn.click(delete_saved_ar_filter, ar_saved_dropdown, [ar_msg, ar_choice, ar_saved_dropdown])
with gr.Tab("πŸ“¦ Batch Dataset Processing"):
gr.Markdown("Apply built-in or custom filter pipelines to multiple images, folders, or zip dataset archives with a reproducible manifest.")
batch_files = gr.File(label="Upload Images or Zip Archive", file_count="multiple", type="filepath")
batch_dir = gr.Textbox(label="Optional Local Dataset Directory Path")
batch_filter = gr.Dropdown(choices=names(True), value="Grayscale", label="Filter / Pipeline Selection")
refresh_filters = gr.Button("πŸ”„ Refresh Filter List")
batch_btn = gr.Button("πŸš€ Process Batch Dataset", variant="primary")
batch_zip = gr.File(label="Processed Output Zip + manifest.json")
refresh_filters.click(lambda: gr.Dropdown(choices=names(True)), None, batch_filter)
batch_btn.click(run_batch, [batch_files, batch_dir, batch_filter], batch_zip)
with gr.Tab("πŸ“– Guide & Documentation"):
gr.Markdown(
"""## Persona & Feature Guide
### 🎨 Photo Editors
- Use **Photo Filters & Pipelines** for instant visual edits like Sepia, Vignette, Emboss, Cartoonify, or Channel Isolation.
- Experiment with **Neural Style Transfer** to stylize portraits or landscapes with synthetic or custom reference artwork.
- Have fun with **Face AR Filters** for accessories (Glasses, Visors, Crown, Pirate Eyepatch, Dog Ears).
### πŸ”¬ Scientists & Researchers
- Use **Resolution & Color Analysis** to downsample images and evaluate RGB vs Grayscale degradation, intensity histograms, and pixel crop tables.
- Perform reproducible **Morphological Operations** (Opening, Closing, Top-Hat, Black-Hat) with explicit structuring element kernels.
- Process large experiment datasets with **Batch Dataset Processing** to export processed images alongside `manifest.json`.
### πŸ’» Developers
- Use the **Python SDK** (`developer_api.py`) or **CLI** (`cli.py`) for command line processing.
- Build multi-step JSON filter pipelines or custom AR landmark overlays in the Web UI, save them to disk, and export auto-generated Python code snippets.
"""
)
if __name__ == "__main__":
demo.launch(ssr_mode=False, share=False)