PaulMartrenchar commited on
Commit
a73c25c
·
1 Parent(s): f8ebfdd

Better card detection

Browse files
Files changed (1) hide show
  1. app.py +91 -36
app.py CHANGED
@@ -58,53 +58,108 @@ def extract_flip7_value(card_img):
58
 
59
 
60
  def detect_cards_and_sum(image, game):
 
 
 
61
  try:
62
  if image is None:
63
  return 0, 0, "No image provided.", None
64
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  if not isinstance(image, np.ndarray):
66
  image = np.array(image.convert("RGB"))
67
- img = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
68
- display_img = img.copy()
69
 
70
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
71
- blur = cv2.GaussianBlur(gray, (5,5), 0)
72
- thresh = cv2.threshold(blur, 120, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
73
 
74
- contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
 
 
 
75
 
76
- total = 0
77
- detected_values = []
 
 
78
 
79
  for cnt in contours:
80
- peri = cv2.arcLength(cnt, True)
81
- approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
82
-
83
- if len(approx) == 4: # rectangle card
84
- pts = np.float32(approx.reshape(4, 2))
85
- dst = np.array([[0,0],[200,0],[200,300],[0,300]], dtype="float32")
86
- M = cv2.getPerspectiveTransform(pts, dst)
87
- warp = cv2.warpPerspective(img, M, (200,300))
88
-
89
- if game == "Skyjo":
90
- value = extract_skyjo_value(warp)
91
- else:
92
- value = extract_flip7_value(warp)
93
-
94
- if value is not None:
95
- detected_values.append(value)
96
- total += value
97
-
98
- # Draw bounding box + label
99
- cv2.polylines(display_img, [approx], True, (0,255,0), 3)
100
- x, y = approx[0][0]
101
- cv2.putText(display_img, str(value), (int(x), int(y)-10),
102
- cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0,0,255), 3)
103
-
104
- if not detected_values:
105
- return 0, 0, "No valid card values detected.", cv2.cvtColor(display_img, cv2.COLOR_BGR2RGB)
106
-
107
- return len(detected_values), total, None, cv2.cvtColor(display_img, cv2.COLOR_BGR2RGB)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  except Exception as e:
110
  return 0, 0, f"Error: {str(e)}", None
 
58
 
59
 
60
  def detect_cards_and_sum(image, game):
61
+ """
62
+ Returns: (num_cards, total, error_msg, annotated_rgb_image)
63
+ """
64
  try:
65
  if image is None:
66
  return 0, 0, "No image provided.", None
67
 
68
+ # --- helpers ---
69
+ def order_points(pts):
70
+ pts = np.array(pts, dtype="float32")
71
+ s = pts.sum(axis=1)
72
+ diff = np.diff(pts, axis=1)
73
+ tl = pts[np.argmin(s)]
74
+ br = pts[np.argmax(s)]
75
+ tr = pts[np.argmin(diff)]
76
+ bl = pts[np.argmax(diff)]
77
+ return np.array([tl, tr, br, bl], dtype="float32")
78
+
79
+ # accept both PIL and np image
80
  if not isinstance(image, np.ndarray):
81
  image = np.array(image.convert("RGB"))
82
+ img_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
83
+ annotated = img_bgr.copy()
84
 
85
+ H, W = annotated.shape[:2]
86
+ img_area = float(H * W)
 
87
 
88
+ # --- robust edge map for rectangles ---
89
+ gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
90
+ gray = cv2.GaussianBlur(gray, (5, 5), 0)
91
+ edges = cv2.Canny(gray, 60, 160)
92
+ edges = cv2.dilate(edges, np.ones((3, 3), np.uint8), iterations=1)
93
 
94
+ contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
95
+
96
+ values = []
97
+ drawn_boxes = 0
98
 
99
  for cnt in contours:
100
+ # geometric filtering
101
+ rect = cv2.minAreaRect(cnt) # (center, (w, h), angle)
102
+ (cx, cy), (rw, rh), _ = rect
103
+ if rw == 0 or rh == 0:
104
+ continue
105
+
106
+ box = cv2.boxPoints(rect) # 4 points, unordered
107
+ box = np.int32(box)
108
+
109
+ # use bounding area (more stable with edges than contour area)
110
+ box_area = rw * rh
111
+ if box_area < 0.01 * img_area or box_area > 0.8 * img_area:
112
+ continue # too small or too big to be a card
113
+
114
+ # aspect ratio (card ~ 1.4–1.8 between long/short edges)
115
+ long_side = max(rw, rh)
116
+ short_side = min(rw, rh)
117
+ ratio = long_side / short_side
118
+ if ratio < 1.2 or ratio > 1.9:
119
+ continue
120
+
121
+ # rectangularity: contour area close to its minAreaRect area
122
+ cnt_area = cv2.contourArea(cnt)
123
+ if cnt_area / box_area < 0.65:
124
+ continue
125
+
126
+ # perspective warp to a canonical card size (2:3 ratio)
127
+ dst_w, dst_h = 300, 450
128
+ M = cv2.getPerspectiveTransform(order_points(box),
129
+ np.array([[0, 0],
130
+ [dst_w-1, 0],
131
+ [dst_w-1, dst_h-1],
132
+ [0, dst_h-1]], dtype="float32"))
133
+ card = cv2.warpPerspective(img_bgr, M, (dst_w, dst_h))
134
+
135
+ # read value (center crop OCR handled in your extract_* funcs)
136
+ if game == "Skyjo":
137
+ val = extract_skyjo_value(card)
138
+ else:
139
+ val = extract_flip7_value(card)
140
+
141
+ if val is None:
142
+ continue
143
+
144
+ values.append(val)
145
+
146
+ # draw box + label at centroid (safer than vertex 0)
147
+ cv2.polylines(annotated, [box], True, (0, 255, 0), 3)
148
+ moments = cv2.moments(box)
149
+ if moments["m00"] != 0:
150
+ tx = int(moments["m10"] / moments["m00"])
151
+ ty = int(moments["m01"] / moments["m00"])
152
+ else:
153
+ tx, ty = int(cx), int(cy)
154
+ cv2.putText(annotated, str(val), (tx - 10, ty - 10),
155
+ cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3)
156
+ drawn_boxes += 1
157
+
158
+ if not values:
159
+ # return the annotated image anyway so you can see what was (not) detected
160
+ return 0, 0, "No valid card values detected.", cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)
161
+
162
+ return len(values), int(sum(values)), None, cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)
163
 
164
  except Exception as e:
165
  return 0, 0, f"Error: {str(e)}", None