import cv2, numpy as np from typing import List, Tuple, TypedDict import logging log = logging.getLogger(__name__) class Box(TypedDict): id: str; x: int; y: int; w: int; h: int; angle: float LONG_SIDE = 1800 MIN_AREA_FRAC = 0.006 MAX_AREA_FRAC = 0.98 AR_MIN, AR_MAX = 0.55, 2.80 WHITENESS_MIN = 120 def _resize_keep(img, long_side=LONG_SIDE): h, w = img.shape[:2] s = long_side / max(h, w) if s < 1.0: img = cv2.resize(img, (int(w*s), int(h*s)), interpolation=cv2.INTER_AREA) else: s = 1.0 return img, s def _iou(a,b): xa1,ya1,xa2,ya2=a; xb1,yb1,xb2,yb2=b inter = max(0,min(xa2,xb2)-max(xa1,xb1)) * max(0,min(ya2,yb2)-max(ya1,yb1)) if inter == 0: return 0.0 area_a=(xa2-xa1)*(ya2-ya1); area_b=(xb2-xb1)*(yb2-yb1) return inter / (area_a+area_b-inter+1e-6) def segment_cards(image_bgr) -> Tuple[List[Box], int, int]: H0, W0 = image_bgr.shape[:2] work, s = _resize_keep(image_bgr, LONG_SIDE) H, W = work.shape[:2]; back = 1.0/s gray = cv2.cvtColor(work, cv2.COLOR_BGR2GRAY) gray = cv2.bilateralFilter(gray, 7, 50, 50) _, bin_ = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) bin_ = cv2.morphologyEx(bin_, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT,(3,3)),1) bin_ = cv2.morphologyEx(bin_, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_RECT,(9,9)),1) cnts,_ = cv2.findContours(bin_, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) min_area = MIN_AREA_FRAC * (W*H) max_area = MAX_AREA_FRAC * (W*H) cand=[] for c in cnts: a=cv2.contourArea(c) if amax_area: continue r=cv2.minAreaRect(c); (cx,cy),(rw,rh),ang=r if rw<10 or rh<10: continue ar=max(rw,rh)/max(1.0,min(rw,rh)) if arAR_MAX: continue pts=cv2.boxPoints(r).astype(int) m=np.zeros((H,W),np.uint8); cv2.drawContours(m,[pts],-1,255,-1) if cv2.mean(gray, mask=m)[0] < WHITENESS_MIN: continue xs,ys=pts[:,0],pts[:,1] x1,y1,x2,y2=xs.min(),ys.min(),xs.max(),ys.max() x,y,w,h=int(x1*back),int(y1*back),int((x2-x1)*back),int((y2-y1)*back) x=max(0,x); y=max(0,y); w=min(W0-x,w); h=min(H0-y,h) if w*h<=0: continue cand.append((x,y,w,h,float(ang))) cand.sort(key=lambda z:z[2]*z[3], reverse=True) picked=[] for x,y,w,h,ang in cand: rect=(x,y,x+w,y+h) if all(_iou(rect,(px,py,px+pw,py+ph))<0.20 for px,py,pw,ph,_ in picked): picked.append((x,y,w,h,ang)) if not picked: m=int(min(W0,H0)*0.03) x,y=m,m; w,h=max(1,W0-2*m),max(1,H0-2*m) log.info("No contours -> fallback to full-image box.") boxes=[{"id":"card-0","x":x,"y":y,"w":w,"h":h,"angle":0.0}] else: boxes=[{"id":f"card-{i}","x":x,"y":y,"w":w,"h":h,"angle":ang} for i,(x,y,w,h,ang) in enumerate(picked)] boxes.sort(key=lambda b:(b["y"], b["x"])) return boxes, W0, H0