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)))