"""Hugging Face Space for street-scene detection and segmentation.""" from __future__ import annotations import os import time import uuid from functools import lru_cache from pathlib import Path import gradio as gr import numpy as np import torch from PIL import Image from transformers import AutoImageProcessor, SegformerForSemanticSegmentation try: import spaces except ImportError: # `spaces` is injected by the ZeroGPU runtime. Keep local/CPU execution valid. class _SpacesFallback: @staticmethod def GPU(*_args, **_kwargs): def decorator(function): return function return decorator spaces = _SpacesFallback() from segmentation_utils import ( build_class_table, render_segmentation, resize_for_output, write_class_csv, ) from detection_utils import ( build_detection_summary, build_detection_table, build_street_indicators, render_detection, write_detection_csv, ) SEGMENTATION_MODEL_ID = "nvidia/segformer-b0-finetuned-cityscapes-1024-1024" DETECTION_MODEL_ID = "yolo26s.pt" OUTPUT_ROOT = Path("/tmp/street-scene-vision") SAMPLE_ROOT = ( "https://raw.githubusercontent.com/" "LabMingzeChen/HNIVision/main/space/examples" ) SAMPLE_IMAGES = [f"{SAMPLE_ROOT}/ubc-campus-main-mall.jpeg"] @lru_cache(maxsize=1) def load_model(): """Download once per container, then reuse the processor and model.""" torch.set_num_threads(max(1, min(4, os.cpu_count() or 1))) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") processor = AutoImageProcessor.from_pretrained(SEGMENTATION_MODEL_ID) model = ( SegformerForSemanticSegmentation.from_pretrained(SEGMENTATION_MODEL_ID) .to(device) .eval() ) id2label = {int(key): value for key, value in model.config.id2label.items()} return processor, model, id2label, device @lru_cache(maxsize=1) def load_detector(): """Download YOLO26-s once per container and reuse it.""" from ultralytics import YOLO return YOLO(DETECTION_MODEL_ID) def _format_detection_indicators( detections: list[dict[str, object]], ) -> str: indicators = build_street_indicators(detections) average_confidence = ( sum(float(item["confidence"]) for item in detections) / len(detections) if detections else 0.0 ) return f""" ### Detection-based street indicators | Indicator | Visible count | |---|---:| | People | {indicators['people']} | | Active-mobility objects (`person` + `bicycle`) | {indicators['active_mobility']} | | Motor vehicles | {indicators['motor_vehicles']} | | All transport objects | {indicators['all_transport']} | | All detected objects | {len(detections)} | Average detection confidence: **{average_confidence:.2f}** > Counts describe visible COCO detections in this image. They are not traffic-flow, > occupancy, accessibility, or safety measurements. """ @spaces.GPU(duration=90) def detect_street_objects( image: Image.Image | None, confidence_threshold: float, ): """Run YOLO object detection and return visual and tabular outputs.""" if image is None: raise gr.Error("Please upload a street-scene image first.") started_at = time.perf_counter() prepared_image = resize_for_output(image) device = "cuda" if torch.cuda.is_available() else "cpu" try: detector = load_detector() predictions = detector.predict( source=np.asarray(prepared_image), conf=float(confidence_threshold), imgsz=1024, device=device, max_det=100, verbose=False, ) prediction = predictions[0] names = prediction.names detections: list[dict[str, object]] = [] if prediction.boxes is not None: coordinates = prediction.boxes.xyxy.detach().cpu().tolist() confidences = prediction.boxes.conf.detach().cpu().tolist() class_ids = prediction.boxes.cls.detach().cpu().tolist() for coordinates_row, confidence, class_id_value in zip( coordinates, confidences, class_ids, ): class_id = int(class_id_value) detections.append( { "class_id": class_id, "class_name": str(names[class_id]), "confidence": float(confidence), "x1": float(coordinates_row[0]), "y1": float(coordinates_row[1]), "x2": float(coordinates_row[2]), "y2": float(coordinates_row[3]), } ) except Exception as exc: raise gr.Error( f"Object detection failed: {type(exc).__name__}: {exc}" ) from exc overlay = render_detection(prepared_image, detections) summary_rows = build_detection_summary(detections) detection_rows = build_detection_table(detections) output_dir = OUTPUT_ROOT / uuid.uuid4().hex output_dir.mkdir(parents=True, exist_ok=True) overlay_path = output_dir / "street_object_detection_overlay.png" csv_path = output_dir / "street_object_detections.csv" overlay.save(overlay_path) write_detection_csv(csv_path, detection_rows) elapsed = time.perf_counter() - started_at visible_classes = len(summary_rows) status = ( f"Done · {prepared_image.width}×{prepared_image.height} · " f"{len(detections)} objects · {visible_classes} COCO classes · " f"{elapsed:.1f}s · device={device}" ) return ( overlay, summary_rows, detection_rows, [str(overlay_path), str(csv_path)], _format_detection_indicators(detections), status, ) @spaces.GPU(duration=90) def segment_street_scene( image: Image.Image | None, opacity: float, min_share_percent: float, ): """Run semantic segmentation and return visual, tabular, and raw outputs.""" if image is None: raise gr.Error("Please upload a street-scene image first.") started_at = time.perf_counter() prepared_image = resize_for_output(image) try: processor, model, id2label, device = load_model() inputs = processor(images=prepared_image, return_tensors="pt") inputs = {name: tensor.to(device) for name, tensor in inputs.items()} with torch.inference_mode(): outputs = model(**inputs) target_size = (prepared_image.height, prepared_image.width) class_map_tensor = processor.post_process_semantic_segmentation( outputs, target_sizes=[target_size], )[0] class_map = class_map_tensor.cpu().numpy().astype(np.uint8) except Exception as exc: raise gr.Error( f"Segmentation failed: {type(exc).__name__}: {exc}" ) from exc overlay, color_mask = render_segmentation( prepared_image, class_map, id2label, float(opacity), ) rows = build_class_table(class_map, id2label, float(min_share_percent)) output_dir = OUTPUT_ROOT / uuid.uuid4().hex output_dir.mkdir(parents=True, exist_ok=True) overlay_path = output_dir / "street_segmentation_overlay.png" mask_path = output_dir / "street_segmentation_color_mask.png" class_ids_path = output_dir / "street_segmentation_class_ids.png" csv_path = output_dir / "street_segmentation_classes.csv" overlay.save(overlay_path) color_mask.save(mask_path) Image.fromarray(class_map).save(class_ids_path) write_class_csv(csv_path, rows) elapsed = time.perf_counter() - started_at visible_classes = len(np.unique(class_map)) status = ( f"Done · {prepared_image.width}×{prepared_image.height} · " f"{visible_classes} street-scene classes · {elapsed:.1f}s · " f"device={device.type}" ) return ( overlay, color_mask, rows, [str(overlay_path), str(mask_path), str(class_ids_path), str(csv_path)], status, ) CSS = """ .gradio-container {max-width: 1260px !important;} .hero {text-align: center; margin: 0 auto 1rem;} .hero h1 {font-size: 2.1rem; margin-bottom: .3rem;} .muted {color: #64748b;} .project-links {display: flex; justify-content: center; gap: .55rem; flex-wrap: wrap; margin-top: .75rem;} .project-link { display: inline-block; padding: .42rem .78rem; border: 1px solid #d7deea; border-radius: 999px; color: inherit !important; text-decoration: none !important; background: white; font-size: .92rem; font-weight: 600; } .project-link:hover {border-color: #6366f1; box-shadow: 0 2px 8px rgba(99, 102, 241, .12);} .guide-grid {display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .8rem; margin: .8rem 0;} .guide-card {border: 1px solid #e2e8f0; border-radius: 12px; padding: .85rem 1rem; background: rgba(255,255,255,.55);} .guide-card h3 {margin: 0 0 .35rem; font-size: 1rem;} .guide-card p {margin: 0; color: #475569; font-size: .93rem; line-height: 1.45;} @media (max-width: 760px) {.guide-grid {grid-template-columns: 1fr;}} """ with gr.Blocks(title="Street Scene Vision Toolkit", theme=gr.themes.Soft(), css=CSS) as demo: gr.Markdown( """

🚦 Street Scene Vision Toolkit

Map every pixel with semantic segmentation, then detect individual objects with bounding boxes.

YOLO26-s · COCO 80 objects · SegFormer-B0 · 19 Cityscapes classes · no API key required

""" ) with gr.Row(equal_height=False): with gr.Column(scale=5): image_input = gr.Image( type="pil", label="Upload a street-scene image", height=470, sources=["upload", "clipboard", "webcam"], ) if SAMPLE_IMAGES: gr.Examples( examples=SAMPLE_IMAGES, inputs=image_input, label="Try the UBC campus street example", examples_per_page=1, ) with gr.Accordion("Segmentation settings", open=True): opacity_input = gr.Slider( 0.15, 0.85, value=0.55, step=0.05, label="Overlay opacity", ) min_share_input = gr.Slider( 0.0, 5.0, value=0.1, step=0.1, label="Minimum class area shown in table (%)", ) with gr.Accordion("Object-detection settings", open=False): confidence_input = gr.Slider( 0.05, 0.90, value=0.25, step=0.05, label="Minimum detection confidence", ) with gr.Row(): segmentation_button = gr.Button( "Segment pixels", variant="primary", size="lg", ) detection_button = gr.Button("Detect objects", size="lg") clear_button = gr.ClearButton(value="Clear image", components=[image_input]) with gr.Column(scale=7): with gr.Tabs(selected="segmentation") as visual_tabs: with gr.Tab("Segmentation overlay", id="segmentation"): overlay_output = gr.Image(label="Segmentation overlay", height=470) segmentation_status = gr.Markdown() with gr.Tab("Color mask", id="mask"): mask_output = gr.Image(label="Cityscapes color mask", height=470) with gr.Tab("Object detection", id="detection"): detection_output = gr.Image( label="YOLO26-s bounding boxes", height=470, ) detection_status = gr.Markdown() with gr.Tabs(selected="segmentation-results") as result_tabs: with gr.Tab("Segmentation results", id="segmentation-results"): table_output = gr.Dataframe( headers=["Class ID", "Class", "Pixels", "Area share (%)", "Color"], datatype=["number", "str", "number", "number", "str"], label="Detected street-scene classes", interactive=False, wrap=True, ) files_output = gr.File( label="Download segmentation overlay, color mask, class IDs, and CSV", file_count="multiple", ) with gr.Tab("Detection results", id="detection-results"): detection_indicators = gr.Markdown() detection_summary = gr.Dataframe( headers=["Class", "Count", "Average confidence", "Maximum confidence"], datatype=["str", "number", "number", "number"], label="Detected object classes", interactive=False, wrap=True, ) with gr.Accordion("Detailed bounding-box coordinates", open=False): detection_table = gr.Dataframe( headers=["Object ID", "Class", "Confidence", "x1", "y1", "x2", "y2"], datatype=["number", "str", "number", "number", "number", "number", "number"], label="Individual detections", interactive=False, wrap=True, ) detection_files = gr.File( label="Download detection overlay and bounding-box CSV", file_count="multiple", ) gr.Markdown( """ ## How to use the app

1 · Choose an image

Upload, paste, use a webcam, or select the UBC campus example.

2 · Segment, then detect

Start with SegFormer semantic segmentation, then optionally run YOLO object detection on the same image.

3 · Explore and download

Compare boxes, overlays, masks, counts, pixel shares, coordinates, and reusable CSV outputs.

## What the results mean - **Object detection** finds separate COCO objects, draws bounding boxes, and reports a confidence score for each detection. - **Detection indicators** summarize visible people, active-mobility objects, and transport objects. They are transparent image counts, not traffic-flow estimates. - **Segmentation overlay** blends the Cityscapes prediction with the original photograph. White lines mark class boundaries. - **Color mask and area share** show pixel-level scene composition. Area share describes visual coverage, not physical land area. - **Downloadable data** include bounding-box coordinates, class-ID pixels, overlays, masks, and CSV summaries. | Scene layer | Segmentation classes | |---|---| | Travel surfaces | road, sidewalk | | Built environment | building, wall, fence, pole, traffic light, traffic sign | | Nature and sky | vegetation, terrain, sky | | People | person, rider | | Transport | car, truck, bus, train, motorcycle, bicycle | ## Classroom and research ideas - Compare detected people, bicycles, and motor vehicles across several street images. - Compare what bounding boxes reveal with what pixel-level segmentation reveals. - Discuss missed objects, false positives, confidence thresholds, and segmentation boundary errors. - Export both CSV files and build object-count and class-coverage charts. - Compare the same location across seasons, weather conditions, or camera viewpoints. > **Important:** predictions are model estimates, not ground truth. COCO detection is limited to its trained object vocabulary, while Cityscapes segmentation is specialized for road-driving imagery. Do not use either output for safety-critical decisions, surveillance, or identifying individuals. [Read the YOLO26 documentation](https://docs.ultralytics.com/models/yolo26/) · [Explore the COCO dataset](https://cocodataset.org/) · [Read the SegFormer paper](https://arxiv.org/abs/2105.15203) · [Explore the Cityscapes dataset](https://www.cityscapes-dataset.com/) · [View the source on GitHub](https://github.com/LabMingzeChen/StreetSceneSegmentation) """ ) detection_event = detection_button.click( fn=detect_street_objects, inputs=[image_input, confidence_input], outputs=[ detection_output, detection_summary, detection_table, detection_files, detection_indicators, detection_status, ], api_name="detect", scroll_to_output=True, ) detection_event.then( fn=lambda: ( gr.Tabs(selected="detection"), gr.Tabs(selected="detection-results"), ), outputs=[visual_tabs, result_tabs], queue=False, api_name=False, ) segmentation_event = segmentation_button.click( fn=segment_street_scene, inputs=[image_input, opacity_input, min_share_input], outputs=[ overlay_output, mask_output, table_output, files_output, segmentation_status, ], api_name="segment", scroll_to_output=True, ) segmentation_event.then( fn=lambda: ( gr.Tabs(selected="segmentation"), gr.Tabs(selected="segmentation-results"), ), outputs=[visual_tabs, result_tabs], queue=False, api_name=False, ) if __name__ == "__main__": demo.queue(max_size=8, default_concurrency_limit=1).launch()