Download app.py from BuildingTHEITGUY/Campus-Vision-Lab: direct link, hf CLI and curl.
- Browser
- Download file 5.63 kB
-
https://huggingface.co/spaces/BuildingTHEITGUY/Campus-Vision-Lab/resolve/main/app.py
- Command line
-
hf download hf://spaces/BuildingTHEITGUY/Campus-Vision-Lab/app.py
-
curl -L -o app.py https://huggingface.co/spaces/BuildingTHEITGUY/Campus-Vision-Lab/resolve/main/app.py
5.63 kB
| """UD Campus Parking Lab — peak-time lot occupancy. Campus operations only.""" | |
| from __future__ import annotations | |
| from functools import lru_cache | |
| import gradio as gr | |
| import numpy as np | |
| from PIL import Image | |
| BANNER = ( | |
| "**UD Campus Parking Lab.** Peak-time lot occupancy from a photo you took. " | |
| "Counts vehicles. Estimates how full the lot is. " | |
| "Does **not** read plates, identify people, or store a watchlist." | |
| ) | |
| FOOTER_IDLE = "waiting for a lot photo" | |
| YOLO_ID = "yolov8n.pt" | |
| VLM_ID = "HuggingFaceTB/SmolVLM-500M-Instruct" | |
| VEHICLE = {"car", "truck", "bus", "motorcycle"} | |
| LOTS = [ | |
| "Academic City — student lot", | |
| "Academic City — staff lot", | |
| "Visitor / short stay", | |
| "Overflow / event parking", | |
| "Other campus lot", | |
| ] | |
| WINDOWS = [ | |
| "Morning arrival (07:30–09:00)", | |
| "Class change", | |
| "Lunch", | |
| "Evening leave", | |
| "Weekend / off-peak", | |
| ] | |
| def device() -> str: | |
| try: | |
| import torch | |
| return "cuda" if torch.cuda.is_available() else "cpu" | |
| except Exception: | |
| return "cpu" | |
| def yolo(): | |
| from ultralytics import YOLO | |
| return YOLO(YOLO_ID) | |
| def vlm(): | |
| import torch | |
| from transformers import AutoModelForVision2Seq, AutoProcessor | |
| proc = AutoProcessor.from_pretrained(VLM_ID) | |
| model = AutoModelForVision2Seq.from_pretrained( | |
| VLM_ID, | |
| torch_dtype=torch.float16 if device() == "cuda" else torch.float32, | |
| ).to(device()) | |
| model.eval() | |
| return proc, model | |
| def count_vehicles(image: Image.Image): | |
| result = yolo().predict(image, verbose=False)[0] | |
| plotted = result.plot()[:, :, ::-1] | |
| counts: dict[str, int] = {} | |
| if result.boxes is not None: | |
| names = result.names | |
| for b in result.boxes: | |
| name = names.get(int(b.cls.item()), "") | |
| if name in VEHICLE: | |
| counts[name] = counts.get(name, 0) + 1 | |
| total = sum(counts.values()) | |
| return Image.fromarray(plotted), counts, total | |
| def lot_note(image: Image.Image, lot: str, window: str) -> str: | |
| import torch | |
| proc, model = vlm() | |
| prompt = ( | |
| f"This is a photo of {lot} at {window}. " | |
| "In two sentences, say whether the lot looks empty, mixed, or packed. " | |
| "Mention parked cars and open stalls if you see them. " | |
| "Do not read license plates. Do not name or describe faces." | |
| ) | |
| messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": prompt}]}] | |
| text = proc.apply_chat_template(messages, add_generation_prompt=True) | |
| inputs = proc(text=text, images=[image], return_tensors="pt") | |
| inputs = {k: v.to(model.device) if hasattr(v, "to") else v for k, v in inputs.items()} | |
| with torch.inference_mode(): | |
| out = model.generate(**inputs, max_new_tokens=80) | |
| return proc.batch_decode(out, skip_special_tokens=True)[0] | |
| def peak_label(pct: float, window: str) -> str: | |
| if pct >= 90: | |
| return "STRESS — treat as full; send overflow" | |
| if pct >= 75: | |
| return "PEAK — few stalls left" | |
| if pct >= 40: | |
| return "MIXED" | |
| if "off-peak" in window.lower() or "Weekend" in window: | |
| return "QUIET (matches off-peak window)" | |
| return "QUIET — not peak yet" | |
| def run(image, lot, window, capacity): | |
| if image is None: | |
| return BANNER, None, "Take or upload a photo of a campus lot.", FOOTER_IDLE | |
| if not isinstance(image, Image.Image): | |
| image = Image.fromarray(np.asarray(image)).convert("RGB") | |
| else: | |
| image = image.convert("RGB") | |
| cap = max(1, int(capacity or 40)) | |
| vis, counts, total = count_vehicles(image) | |
| pct = min(100.0, 100.0 * total / cap) | |
| try: | |
| note = lot_note(image, lot, window) | |
| except Exception as e: | |
| note = f"Caption skipped ({type(e).__name__}). Counts still ran." | |
| breakdown = ", ".join(f"{k} {v}" for k, v in sorted(counts.items())) or "none" | |
| status = peak_label(pct, window) | |
| notes = ( | |
| f"Lot: {lot}\n" | |
| f"Window: {window}\n" | |
| f"Vehicles counted: {total} ({breakdown})\n" | |
| f"Capacity you set: {cap} stalls\n" | |
| f"Estimated occupancy: {pct:.0f}%\n" | |
| f"Peak flag: {status}\n\n" | |
| f"Scene note:\n{note}\n\n" | |
| "This is a still-photo estimate, not a live sensor feed. " | |
| "Double-count and occlusion will skew the number." | |
| ) | |
| footer = ( | |
| f"yolo: {YOLO_ID} | vlm: {VLM_ID} | device: {device()} | " | |
| f"vehicles {total}/{cap} | {status}" | |
| ) | |
| return BANNER, vis, notes, footer | |
| with gr.Blocks(title="UD Campus Parking Lab") as demo: | |
| gr.Markdown("# UD Campus Parking Lab") | |
| banner = gr.Markdown(BANNER) | |
| gr.Markdown( | |
| "University of Dubai campus operations. " | |
| "Photograph a lot at peak arrival or class change. " | |
| "Set how many stalls that lot has. The app counts vehicles and flags peak stress." | |
| ) | |
| with gr.Row(): | |
| lot = gr.Dropdown(LOTS, value=LOTS[0], label="Campus lot") | |
| window = gr.Dropdown(WINDOWS, value=WINDOWS[0], label="Time window") | |
| capacity = gr.Number(value=40, precision=0, label="Stall capacity") | |
| with gr.Row(): | |
| inp = gr.Image(type="pil", label="Lot photo or webcam", sources=["upload", "webcam", "clipboard"]) | |
| out = gr.Image(type="pil", label="Detections (vehicles)") | |
| notes = gr.Textbox(label="Occupancy report", lines=16) | |
| footer = gr.Textbox(label="Footer", value=FOOTER_IDLE) | |
| gr.Button("Estimate occupancy").click( | |
| fn=run, inputs=[inp, lot, window, capacity], outputs=[banner, out, notes, footer] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |