Spaces:
Sleeping
Sleeping
Commit
·
a73c25c
1
Parent(s):
f8ebfdd
Better card detection
Browse files
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 |
-
|
| 68 |
-
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
thresh = cv2.threshold(blur, 120, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
|
| 73 |
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
|
| 79 |
for cnt in contours:
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
if
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|