Spaces:
Sleeping
Sleeping
| import io | |
| import os | |
| import datetime | |
| from typing import Tuple, List | |
| import cv2 | |
| import numpy as np | |
| from PIL import Image | |
| import gradio as gr | |
| CASCADE_PATH = cv2.data.haarcascades + "haarcascade_frontalface_default.xml" | |
| face_cascade = cv2.CascadeClassifier(CASCADE_PATH) | |
| # Optional Supabase client (configured via environment variables in Spaces) | |
| supabase = None | |
| try: | |
| from supabase import create_client | |
| SUPABASE_URL = os.environ.get('SUPABASE_URL') | |
| SUPABASE_KEY = os.environ.get('SUPABASE_SERVICE_ROLE_KEY') | |
| if SUPABASE_URL and SUPABASE_KEY: | |
| supabase = create_client(SUPABASE_URL, SUPABASE_KEY) | |
| except Exception: | |
| supabase = None | |
| def detect_faces_np(img: np.ndarray, scaleFactor: float = 1.1, minNeighbors: int = 5, minSize: Tuple[int, int] = (30, 30)) -> Tuple[Image.Image, List[Tuple[int, int, int, int]]]: | |
| """Detect faces in a numpy RGB image, return annotated PIL image and list of boxes (x,y,w,h).""" | |
| # Ensure image is RGB numpy array | |
| if img is None: | |
| raise ValueError("No image provided") | |
| # Convert to OpenCV BGR for drawing | |
| if img.ndim == 2: | |
| rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) | |
| bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) | |
| else: | |
| # assume RGB | |
| bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) | |
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) | |
| faces = face_cascade.detectMultiScale(gray, scaleFactor=scaleFactor, minNeighbors=minNeighbors, minSize=minSize) | |
| # Draw rectangles | |
| out_bgr = bgr.copy() | |
| boxes = [] | |
| for (x, y, w, h) in faces: | |
| cv2.rectangle(out_bgr, (x, y), (x + w, y + h), (0, 255, 0), 2) | |
| boxes.append((int(x), int(y), int(w), int(h))) | |
| # Convert back to RGB PIL Image | |
| out_rgb = cv2.cvtColor(out_bgr, cv2.COLOR_BGR2RGB) | |
| pil = Image.fromarray(out_rgb) | |
| return pil, boxes | |
| def detect_faces_gradio(image: Image.Image, scaleFactor: float = 1.1, minNeighbors: int = 5, minSize: int = 30): | |
| """Gradio wrapper: accepts PIL image and returns annotated image and textual summary.""" | |
| if image is None: | |
| return None, "No image provided" | |
| img_np = np.array(image.convert('RGB')) | |
| pil_out, boxes = detect_faces_np(img_np, scaleFactor=scaleFactor, minNeighbors=int(minNeighbors), minSize=(int(minSize), int(minSize))) | |
| if len(boxes) == 0: | |
| return pil_out, "No faces detected" | |
| summary_lines = [f"faces: {len(boxes)}"] | |
| for i, (x, y, w, h) in enumerate(boxes, start=1): | |
| summary_lines.append(f"{i}: x={x}, y={y}, w={w}, h={h}") | |
| return pil_out, "\n".join(summary_lines) | |
| def upload_image_to_supabase(pil_image: Image.Image, folder: str = 'captures'): | |
| """Upload a PIL Image to Supabase Storage and return public URL or None.""" | |
| if supabase is None: | |
| return None | |
| buf = io.BytesIO() | |
| pil_image.save(buf, format='JPEG', quality=85) | |
| buf.seek(0) | |
| filename = f"{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S_%f')}.jpg" | |
| try: | |
| supabase.storage.from_(folder).upload(path=filename, file=buf.getvalue(), file_options={"content-type": "image/jpeg"}) | |
| res = supabase.storage.from_(folder).get_public_url(filename) | |
| if isinstance(res, dict): | |
| return res.get('publicUrl') or res.get('public_url') | |
| return str(res) | |
| except Exception as e: | |
| print('Supabase upload error:', e) | |
| return None | |
| def upload_annotated(image: Image.Image): | |
| if image is None: | |
| return "No image to upload" | |
| url = upload_image_to_supabase(image) | |
| if url: | |
| return url | |
| if supabase is None: | |
| return "Supabase not configured (set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY)" | |
| return "Upload failed" | |
| def build_interface(): | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# Face Detection (OpenCV Haar Cascade)") | |
| with gr.Row(): | |
| with gr.Column(): | |
| # Create Image input with webcam support when available; provide | |
| # fallbacks for older Gradio versions that don't accept `source`. | |
| try: | |
| inp = gr.Image(source="webcam", type="pil", label="Webcam") | |
| except TypeError: | |
| try: | |
| inp = gr.Image(type="pil", label="Webcam") | |
| except Exception: | |
| try: | |
| inp = gr.inputs.Image(image_mode="RGB", label="Webcam") | |
| except Exception: | |
| inp = gr.Image(label="Webcam") | |
| # Only provide a single Capture button for simplicity | |
| run = gr.Button("Capture") | |
| with gr.Column(): | |
| out_img = gr.Image(type="pil", label="Annotated image") | |
| out_text = gr.Textbox(label="Detections") | |
| # Camera returns either a numpy array or a PIL Image depending on Gradio version | |
| def detect_wrapper(cam_image): | |
| # Convert numpy array (BGR or RGB) to PIL Image consistently | |
| img = cam_image | |
| if isinstance(cam_image, (list, tuple)): | |
| # Received as list (rare) -> convert to array | |
| img = np.array(cam_image) | |
| if isinstance(img, np.ndarray): | |
| # If the array has 3 channels assume RGB from browser; convert to PIL | |
| try: | |
| pil = Image.fromarray(img.astype('uint8'), 'RGB') | |
| except Exception: | |
| # fallback: convert BGR->RGB | |
| pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) | |
| else: | |
| pil = img | |
| return detect_faces_gradio(pil) | |
| run.click(detect_wrapper, inputs=[inp], outputs=[out_img, out_text]) | |
| return demo | |
| if __name__ == '__main__': | |
| demo = build_interface() | |
| demo.launch(server_name='0.0.0.0', server_port=int(__import__('os').environ.get('PORT', 7860))) |