Spaces:
Sleeping
Sleeping
File size: 8,131 Bytes
799d6a7 98253f0 adea4a9 1d42af3 a7f0aae 0d5b69a 1d42af3 a7f0aae 1d42af3 98253f0 1d42af3 8b4ba30 1d42af3 a7f0aae 1d42af3 a7f0aae 799d6a7 a7f0aae 1d42af3 98253f0 a73c25c 799d6a7 f8ebfdd 799d6a7 a73c25c 799d6a7 a73c25c 799d6a7 a73c25c 799d6a7 a0612ff a73c25c a0612ff a73c25c 799d6a7 a73c25c a0612ff a73c25c 799d6a7 a0612ff a73c25c a0612ff a73c25c a0612ff a73c25c d9bbe60 a73c25c a0612ff a73c25c a0612ff a73c25c a0612ff a73c25c 9331987 a73c25c 0d5b69a a73c25c 0d5b69a 104b989 0d5b69a 104b989 0d5b69a a73c25c a0612ff 8b4ba30 a73c25c 799d6a7 f8ebfdd 799d6a7 98253f0 a0612ff 799d6a7 98253f0 f8ebfdd 98253f0 f8ebfdd 98253f0 799d6a7 98253f0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
import cv2
import numpy as np
import pytesseract
import re
import gradio as gr
# Regex pattern for allowed card values
VALID_PATTERN = re.compile(r"-?\d+")
def extract_skyjo_value(card_img):
h, w, _ = card_img.shape
# Crop the top-left corner
crop_margin_top = int(h / 11)
crop_margin_left = int(w / 7)
crop_size = int(w / 3)
roi = card_img[
crop_margin_top:crop_margin_top + crop_size,
crop_margin_left:crop_margin_left + crop_size
]
# Convert to grayscale
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
# Threshold to keep only black/dark pixels (adjust threshold value as needed)
_, thresh = cv2.threshold(gray, 50, 255, cv2.THRESH_BINARY_INV)
# Optional: morphological closing to fill small gaps
kernel = np.ones((2, 2), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
# Invert back to black on white for OCR
thresh = cv2.bitwise_not(thresh)
# Run OCR on the thresholded image
raw_text = pytesseract.image_to_string(
thresh,
config="--psm 10 --oem 3 -c tessedit_char_whitelist=-0123456789"
)
print("Raw OCR text:", raw_text)
# Extract numbers
matches = re.findall(r"-?\d+", raw_text)
for m in matches:
try:
val = int(m)
if -2 <= val <= 12:
return val
except ValueError:
continue
return None
def extract_flip7_value(card_img):
h, w, _ = card_img.shape
margin = 50
roi = card_img[margin:h-margin, margin:w-margin]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)[1]
raw_text = pytesseract.image_to_string(
thresh,
config="--psm 6 -c tessedit_char_whitelist=0123456789"
)
matches = re.findall(r"\d+", raw_text)
for m in matches:
try:
val = int(m)
if 0 <= val <= 12:
return val
except ValueError:
continue
return None
def detect_cards_and_sum(image, game):
"""
Returns: (num_cards, total, error_msg, annotated_rgb_image)
"""
try:
if image is None:
return 0, 0, "No image provided.", None
# --- helpers ---
def order_points(pts):
pts = np.array(pts, dtype="float32")
s = pts.sum(axis=1)
diff = np.diff(pts, axis=1)
tl = pts[np.argmin(s)]
br = pts[np.argmax(s)]
tr = pts[np.argmin(diff)]
bl = pts[np.argmax(diff)]
return np.array([tl, tr, br, bl], dtype="float32")
# accept both PIL and np image
if not isinstance(image, np.ndarray):
image = np.array(image.convert("RGB"))
img_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
annotated = img_bgr.copy()
H, W = annotated.shape[:2]
img_area = float(H * W)
# --- edge map ---
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blur, 50, 150)
edges = cv2.dilate(edges, np.ones((3, 3), np.uint8), iterations=1)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# sort largest to smallest
contours = sorted(contours, key=cv2.contourArea, reverse=True)
values = []
for cnt in contours:
rect = cv2.minAreaRect(cnt)
(cx, cy), (rw, rh), _ = rect
if rw == 0 or rh == 0:
continue
box = cv2.boxPoints(rect)
box = np.int32(box)
box_area = rw * rh
if box_area < 0.002 * img_area or box_area > 0.9 * img_area:
continue # too small or too big to be a card
# aspect ratio (card ~ 1.4-1.8 between long/short edges)
long_side = max(rw, rh)
short_side = min(rw, rh)
ratio = long_side / short_side
if ratio < 1.1 or ratio > 2.2:
continue
# rectangularity: contour area close to its minAreaRect area
cnt_area = cv2.contourArea(cnt)
if cnt_area / box_area < 0.5:
continue
# perspective warp to a canonical card size (2:3 ratio)
dst_w, dst_h = 300, 450
M = cv2.getPerspectiveTransform(order_points(box),
np.array([[0, 0],
[dst_w-1, 0],
[dst_w-1, dst_h-1],
[0, dst_h-1]], dtype="float32"))
card = cv2.warpPerspective(img_bgr, M, (dst_w, dst_h))
# read value (center crop OCR handled in your extract_* funcs)
if game == "Skyjo":
val = extract_skyjo_value(card)
else:
val = extract_flip7_value(card)
if val is None:
val = 100
values.append(val)
# --- Draw card outline ---
cv2.polylines(annotated, [box], True, (0, 255, 0), 3)
# --- Draw cropped region and value ---
h, w, _ = card.shape
crop_margin_top = int(h / 11)
crop_margin_left = int(w / 7)
crop_size = int(w / 3)
# Define crop region points (without homogeneous coordinate)
crop_tl = np.array([crop_margin_left, crop_margin_top], dtype="float32")
crop_br = np.array([crop_margin_left + crop_size, crop_margin_top + crop_size], dtype="float32")
# Reshape for perspectiveTransform: (N, 1, 2)
crop_tl_reshaped = crop_tl.reshape(-1, 1, 2)
crop_br_reshaped = crop_br.reshape(-1, 1, 2)
# Inverse perspective transform to get points in original image
M_inv = cv2.getPerspectiveTransform(
np.array([[0, 0], [dst_w-1, 0], [dst_w-1, dst_h-1], [0, dst_h-1]], dtype="float32"),
order_points(box)
)
crop_tl_orig = cv2.perspectiveTransform(crop_tl_reshaped, M_inv)
crop_br_orig = cv2.perspectiveTransform(crop_br_reshaped, M_inv)
# Draw rectangle around cropped region in original image
cv2.rectangle(
annotated,
tuple(crop_tl_orig[0][0].astype(int)),
tuple(crop_br_orig[0][0].astype(int)),
(255, 0, 0), 2
)
# --- Draw value near the card ---
moments = cv2.moments(box)
if moments["m00"] != 0:
tx = int(moments["m10"] / moments["m00"])
ty = int(moments["m01"] / moments["m00"])
else:
tx, ty = int(cx), int(cy)
cv2.putText(annotated, str(val), (tx-10, ty-10),
cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 0, 255), 3)
if not values:
# return the annotated image anyway so you can see what was (not) detected
return 0, 0, "No valid card values detected.", cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)
return len(values), int(sum(values)), None, cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)
except Exception as e:
return 0, 0, f"Error: {str(e)}", None
# Gradio UI
with gr.Blocks() as demo:
gr.Markdown("## Card Value Detector (Skyjo / Flip7)")
with gr.Row():
image_input = gr.Image(type="pil", label="Upload photo")
game_choice = gr.Radio(["Skyjo", "Flip7"], value="Skyjo", label="Game")
num_cards = gr.Number(label="Number of Cards Detected")
total = gr.Number(label="Total Sum")
error = gr.Textbox(label="Error Message")
annotated = gr.Image(type="numpy", label="Detected Cards with Values")
run_btn = gr.Button("Process")
run_btn.click(
fn=detect_cards_and_sum,
inputs=[image_input, game_choice],
outputs=[num_cards, total, error, annotated]
)
if __name__ == "__main__":
demo.launch()
|