""" detect.py This module defines image-processing routes and functions for detecting circles and text from uploaded images using OpenCV and EasyOCR. Key functionalities: - Detect circular regions in an image and extract text inside/near them. - Detect textual regions across the entire image (excluding numeric-only text and quotes). - Provide a FastAPI endpoint (`POST /`) that accepts an image file and returns detected circles with text plus extracted non-numeric text regions. """ from fastapi import APIRouter, File, UploadFile import cv2 import numpy as np import easyocr import re # Initialize FastAPI router for detection-related endpoints router = APIRouter() # Initialize EasyOCR reader (supports English by default) reader = easyocr.Reader(['en']) def detect_circles_with_text_from_image_bytes(image_bytes): """ Detects circular shapes in the given image and extracts text within/around each circle. Steps: 1. Convert image bytes into an OpenCV image. 2. Convert to grayscale for circle detection. 3. Use Hough Circle Transform to detect circles. 4. For each detected circle: - Crop the circular region with some padding. - Perform OCR (EasyOCR) on the cropped region. - Identify possible `page_number` (format: a.) and `circle_text` (purely numeric). - Collect raw texts recognized in that region. 5. Return a structured list of circles with metadata. Returns: List of dictionaries containing: - id (int): Circle index - x, y (int): Circle center coordinates - r (int): Circle radius - page_number (str): Extracted page number if detected - circle_text (str): Extracted numeric text if detected - raw_texts (list): All OCR results from that circle region """ try: nparr = np.frombuffer(image_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if img is None: print("Failed to decode image") return [] gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Detect circles using Hough Circle Transform circles = cv2.HoughCircles( gray, cv2.HOUGH_GRADIENT, dp=1.2, minDist=20, param1=50, param2=100, minRadius=50, maxRadius=100 ) results = [] if circles is not None: circles = np.round(circles[0, :]).astype("int") for i, (x, y, r) in enumerate(circles): # Crop region around circle with padding top = max(y - r - 20, 0) bottom = min(y + r + 20, img.shape[0]) left = max(x - r - 20, 0) right = min(x + r + 20, img.shape[1]) crop = img[top:bottom, left:right] try: ocr_result = reader.readtext(crop) texts = [res[1].strip() for res in ocr_result] except Exception as e: print(f"OCR error for circle {i}: {e}") texts = [] # Extract structured info page_number, circle_text = "", "" for t in texts: t_clean = t.strip() if re.match(r"^a\d+\.\d+$", t_clean, re.IGNORECASE): page_number = t_clean elif re.match(r"^\d+$", t_clean): circle_text = t_clean results.append({ "id": i + 1, "x": int(x), "y": int(y), "r": int(r), "page_number": page_number, "circle_text": circle_text, "raw_texts": texts }) return results except Exception as e: print(f"Circle detection error: {e}") return [] def detect_text_from_image_bytes(image_bytes): """ Detects text regions from the entire image, excluding numeric-only text and strings with quotes. Steps: 1. Convert image bytes to an OpenCV image. 2. Run EasyOCR to detect text with bounding boxes. 3. Skip text if it: - Contains quotes (single/double). - Contains any digits. 4. Collect bounding box coordinates and the cleaned text. Returns: List of dictionaries containing: - id (int): Text index - x1, y1, x2, y2 (int): Bounding box coordinates - text (str): Extracted text string """ try: nparr = np.frombuffer(image_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if img is None: print("Failed to decode image for text detection") return [] results = reader.readtext(img) text_boxes = [] for i, (bbox, text, confidence) in enumerate(results): # Skip text containing quotes or numbers if "'" in text or '"' in text: continue if any(char.isdigit() for char in text): continue try: # Extract bounding box coordinates x_coords = [point[0] for point in bbox] y_coords = [point[1] for point in bbox] x1, x2 = int(min(x_coords)), int(max(x_coords)) y1, y2 = int(min(y_coords)), int(max(y_coords)) text_boxes.append({ "id": i + 1, "x1": x1, "y1": y1, "x2": x2, "y2": y2, "text": text.strip() }) except Exception as e: print(f"Error processing text box {i}: {e}") continue return text_boxes except Exception as e: print(f"Text detection error: {e}") return [] @router.post("/") async def detect_circles(file: UploadFile = File(...)): """ FastAPI endpoint to detect circles and text from an uploaded image. Steps: 1. Accepts an image file via POST request. 2. Reads image bytes. 3. Runs circle detection (with OCR inside circles). 4. Runs general text detection across the entire image. 5. Returns results as a JSON response containing: - circles: List of detected circles with text info - texts: List of detected text regions outside circles """ try: image_bytes = await file.read() circles_with_text = detect_circles_with_text_from_image_bytes(image_bytes) texts = detect_text_from_image_bytes(image_bytes) return {"circles": circles_with_text, "texts": texts} except Exception as e: print(f"Detection endpoint error: {e}") return {"error": str(e), "circles": [], "texts": []}