Oamitai commited on
Commit
e59412a
·
1 Parent(s): b98d2ee

Updated app.py with new improvements

Browse files
Files changed (1) hide show
  1. app.py +103 -75
app.py CHANGED
@@ -11,6 +11,27 @@ from itertools import combinations
11
  from huggingface_hub import hf_hub_download
12
  import spaces # Added for ZeroGPU support
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  # =============================================================================
15
  # MODEL LOADING
16
  # =============================================================================
@@ -35,31 +56,34 @@ fill_classification_model = tf.keras.models.load_model(
35
  hf_hub_download("Oamitai/fill-classification", "fill_model.keras")
36
  )
37
 
 
 
 
 
 
 
 
 
 
38
  # =============================================================================
39
  # ORIENTATION CORRECTION FUNCTIONS
40
  # =============================================================================
41
- def check_and_rotate_input_image(board_image, card_detection_model):
42
  """
43
- Checks the orientation of the board image by detecting card bounding boxes.
44
  If the average detected card height is greater than the average card width,
45
  rotates the image 90° clockwise.
46
  """
47
- card_results = card_detection_model(board_image)
48
- card_boxes = card_results[0].boxes.xyxy.cpu().numpy().astype(int)
49
-
50
- # If no cards are detected, assume no rotation is needed.
51
  if len(card_boxes) == 0:
52
  return board_image, False
53
 
54
- total_width = total_height = count = 0
55
  for box in card_boxes:
56
  x1, y1, x2, y2 = box
57
  total_width += (x2 - x1)
58
  total_height += (y2 - y1)
59
- count += 1
60
-
61
- avg_width = total_width / count
62
- avg_height = total_height / count
63
 
64
  if avg_height > avg_width:
65
  rotated_image = cv2.rotate(board_image, cv2.ROTATE_90_CLOCKWISE)
@@ -69,7 +93,7 @@ def check_and_rotate_input_image(board_image, card_detection_model):
69
 
70
  def restore_original_orientation(image, was_rotated):
71
  """
72
- If the image was rotated for processing, rotate it back to the original orientation.
73
  """
74
  if was_rotated:
75
  return cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
@@ -80,16 +104,14 @@ def restore_original_orientation(image, was_rotated):
80
  # =============================================================================
81
  def predict_color(shape_image):
82
  """
83
- Predict the dominant color (green, purple, or red) using HSV thresholds.
84
  """
85
  hsv_image = cv2.cvtColor(shape_image, cv2.COLOR_BGR2HSV)
86
- # Define HSV ranges
87
  green_mask = cv2.inRange(hsv_image, np.array([40, 50, 50]), np.array([80, 255, 255]))
88
  purple_mask = cv2.inRange(hsv_image, np.array([120, 50, 50]), np.array([160, 255, 255]))
89
  red_mask1 = cv2.inRange(hsv_image, np.array([0, 50, 50]), np.array([10, 255, 255]))
90
  red_mask2 = cv2.inRange(hsv_image, np.array([170, 50, 50]), np.array([180, 255, 255]))
91
  red_mask = cv2.bitwise_or(red_mask1, red_mask2)
92
- # Count non-zero pixels in each mask
93
  color_counts = {
94
  'green': cv2.countNonZero(green_mask),
95
  'purple': cv2.countNonZero(purple_mask),
@@ -99,8 +121,8 @@ def predict_color(shape_image):
99
 
100
  def predict_card_features(card_image, shape_detection_model, fill_model, shape_model, box):
101
  """
102
- For a given card image, detect shapes and predict fill and shape attributes.
103
- Returns a dictionary of features.
104
  """
105
  shape_results = shape_detection_model(card_image)
106
  card_height, card_width = card_image.shape[:2]
@@ -112,47 +134,47 @@ def predict_card_features(card_image, shape_detection_model, fill_model, shape_m
112
  x1, y1, x2, y2 = detected_box.astype(int)
113
  shape_area = (x2 - x1) * (y2 - y1)
114
  if shape_area > 0.03 * card_area:
115
- filtered_boxes.append(detected_box)
116
 
117
- count = min(len(filtered_boxes), 3)
118
- color_labels, fill_labels, shape_labels = [], [], []
119
 
120
- for shape_box in filtered_boxes:
121
- shape_box = shape_box.astype(int)
122
- shape_img = card_image[shape_box[1]:shape_box[3], shape_box[0]:shape_box[2]]
123
- # Preprocess images for classification models
124
- fill_input_shape = fill_model.input_shape[1:3]
125
- shape_input_shape = shape_model.input_shape[1:3]
126
- fill_img = cv2.resize(shape_img, fill_input_shape) / 255.0
127
- shape_img_resized = cv2.resize(shape_img, shape_input_shape) / 255.0
128
- fill_img = np.expand_dims(fill_img, axis=0)
129
- shape_img_resized = np.expand_dims(shape_img_resized, axis=0)
130
- # Make predictions
131
- fill_pred = fill_model.predict(fill_img)
132
- shape_pred = shape_model.predict(shape_img_resized)
133
- fill_labels.append(['empty', 'full', 'striped'][np.argmax(fill_pred)])
134
- shape_labels.append(['diamond', 'oval', 'squiggle'][np.argmax(shape_pred)])
135
- color_labels.append(predict_color(shape_img))
136
 
137
- if count > 0:
138
- color_label = max(set(color_labels), key=color_labels.count)
139
- fill_label = max(set(fill_labels), key=fill_labels.count)
140
- shape_label = max(set(shape_labels), key=shape_labels.count)
141
- else:
142
- color_label = fill_label = shape_label = 'unknown'
 
143
 
144
- return {
145
- 'count': count,
146
- 'color': color_label,
147
- 'fill': fill_label,
148
- 'shape': shape_label,
149
- 'box': box
150
- }
151
 
152
  def is_set(cards):
153
  """
154
- Check if a combination of cards forms a valid set.
155
- For each feature, values must be either all the same or all different.
156
  """
157
  for feature in ['Count', 'Color', 'Fill', 'Shape']:
158
  if len({card[feature] for card in cards}) not in [1, 3]:
@@ -161,7 +183,7 @@ def is_set(cards):
161
 
162
  def find_sets(card_df):
163
  """
164
- Examine every combination of three cards from the DataFrame and return valid sets.
165
  """
166
  sets_found = []
167
  for combo in combinations(card_df.iterrows(), 3):
@@ -176,7 +198,7 @@ def find_sets(card_df):
176
 
177
  def detect_cards_from_image(board_image, card_detection_model):
178
  """
179
- Use the YOLO card detection model to detect cards on the board image.
180
  Returns a list of tuples: (cropped card image, bounding box).
181
  """
182
  card_results = card_detection_model(board_image)
@@ -186,17 +208,18 @@ def detect_cards_from_image(board_image, card_detection_model):
186
  x1, y1, x2, y2 = box
187
  card_img = board_image[y1:y2, x1:x2]
188
  cards.append((card_img, box))
189
- return cards
190
 
191
- def classify_cards_from_board_image(board_image, card_detection_model, shape_detection_model, fill_model, shape_model):
192
  """
193
- For each detected card on the board image, predict its features.
194
  Returns a pandas DataFrame of card feature data.
195
  """
196
- cards = detect_cards_from_image(board_image, card_detection_model)
197
  card_data = []
198
- for card_image, box in cards:
199
- features = predict_card_features(card_image, shape_detection_model, fill_model, shape_model, box)
 
 
200
  card_data.append({
201
  "Count": features['count'],
202
  "Color": features['color'],
@@ -208,12 +231,20 @@ def classify_cards_from_board_image(board_image, card_detection_model, shape_det
208
 
209
  def classify_and_find_sets_from_array(board_image, card_detection_model, shape_detection_model, fill_model, shape_model):
210
  """
211
- Processes a board image (in BGR format), corrects its orientation, detects cards,
212
- classifies them, finds valid sets, and finally restores the original orientation.
213
- Returns a tuple: (sets_found, annotated image).
 
 
 
214
  """
215
- board_image, was_rotated = check_and_rotate_input_image(board_image, card_detection_model)
216
- card_df = classify_cards_from_board_image(board_image, card_detection_model, shape_detection_model, fill_model, shape_model)
 
 
 
 
 
217
  sets_found = find_sets(card_df)
218
  annotated_image = draw_sets_on_image(board_image.copy(), sets_found)
219
  final_image = restore_original_orientation(annotated_image, was_rotated)
@@ -224,7 +255,7 @@ def classify_and_find_sets_from_array(board_image, card_detection_model, shape_d
224
  # =============================================================================
225
  def draw_sets_on_image(board_image, sets_info):
226
  """
227
- Draw bounding boxes and set labels on the board image for each detected set.
228
  """
229
  colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255),
230
  (255, 255, 0), (255, 0, 255), (0, 255, 255)]
@@ -248,19 +279,18 @@ def draw_sets_on_image(board_image, sets_info):
248
  return board_image
249
 
250
  # =============================================================================
251
- # GRADIO INTERFACE FUNCTION
252
  # =============================================================================
253
- @spaces.GPU # Decorator added for ZeroGPU dynamic GPU allocation
254
  def detect_and_display_sets_interface(input_image):
255
  """
256
- Gradio interface function:
257
- - Accepts a PIL image (board image)
258
- - Converts it to a cv2 BGR image
259
- - Processes it for set detection
260
- - Returns the annotated image (as PIL) and a status message.
261
  """
262
  try:
263
- # Convert input PIL image to OpenCV (BGR) format.
264
  image_cv = cv2.cvtColor(np.array(input_image), cv2.COLOR_RGB2BGR)
265
  sets_found, final_image = classify_and_find_sets_from_array(
266
  image_cv,
@@ -269,12 +299,10 @@ def detect_and_display_sets_interface(input_image):
269
  fill_classification_model,
270
  shape_classification_model
271
  )
272
- # Convert back to RGB for display.
273
  final_image_rgb = cv2.cvtColor(final_image, cv2.COLOR_BGR2RGB)
274
  return Image.fromarray(final_image_rgb), "Sets detected successfully."
275
  except Exception as e:
276
  err = f"❌ Error: {str(e)}\n{traceback.format_exc()}"
277
- # Return a blank image with error details.
278
  return Image.fromarray(np.zeros((100, 100, 3), dtype=np.uint8)), err
279
 
280
  # =============================================================================
 
11
  from huggingface_hub import hf_hub_download
12
  import spaces # Added for ZeroGPU support
13
 
14
+ # -----------------------------------------------------------------------------
15
+ # SAFE CUDA CHECK
16
+ # -----------------------------------------------------------------------------
17
+ def safe_cuda_available():
18
+ try:
19
+ return torch.cuda.is_available()
20
+ except Exception as e:
21
+ print("Error checking CUDA availability:", e)
22
+ return False
23
+
24
+ gpu_available = safe_cuda_available()
25
+ if gpu_available:
26
+ try:
27
+ # Move YOLO models to GPU later after loading models.
28
+ gpu_message = "GPU is available. Will move models to GPU."
29
+ except Exception as e:
30
+ print("Failed to set GPU mode:", e)
31
+ gpu_available = False
32
+ else:
33
+ print("CUDA not available or error encountered. Running on CPU.")
34
+
35
  # =============================================================================
36
  # MODEL LOADING
37
  # =============================================================================
 
56
  hf_hub_download("Oamitai/fill-classification", "fill_model.keras")
57
  )
58
 
59
+ # Move YOLO models to GPU if available
60
+ if gpu_available:
61
+ try:
62
+ card_detection_model.to("cuda")
63
+ shape_detection_model.to("cuda")
64
+ print("YOLO models moved to GPU.")
65
+ except Exception as e:
66
+ print("Failed to move YOLO models to GPU:", e)
67
+
68
  # =============================================================================
69
  # ORIENTATION CORRECTION FUNCTIONS
70
  # =============================================================================
71
+ def check_and_rotate_input_image(board_image, card_boxes):
72
  """
73
+ Checks the orientation of the board image based on detected card bounding boxes.
74
  If the average detected card height is greater than the average card width,
75
  rotates the image 90° clockwise.
76
  """
 
 
 
 
77
  if len(card_boxes) == 0:
78
  return board_image, False
79
 
80
+ total_width = total_height = 0
81
  for box in card_boxes:
82
  x1, y1, x2, y2 = box
83
  total_width += (x2 - x1)
84
  total_height += (y2 - y1)
85
+ avg_width = total_width / len(card_boxes)
86
+ avg_height = total_height / len(card_boxes)
 
 
87
 
88
  if avg_height > avg_width:
89
  rotated_image = cv2.rotate(board_image, cv2.ROTATE_90_CLOCKWISE)
 
93
 
94
  def restore_original_orientation(image, was_rotated):
95
  """
96
+ Rotates the image back to the original orientation if it was rotated.
97
  """
98
  if was_rotated:
99
  return cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
 
104
  # =============================================================================
105
  def predict_color(shape_image):
106
  """
107
+ Predicts the dominant color (green, purple, or red) using HSV thresholds.
108
  """
109
  hsv_image = cv2.cvtColor(shape_image, cv2.COLOR_BGR2HSV)
 
110
  green_mask = cv2.inRange(hsv_image, np.array([40, 50, 50]), np.array([80, 255, 255]))
111
  purple_mask = cv2.inRange(hsv_image, np.array([120, 50, 50]), np.array([160, 255, 255]))
112
  red_mask1 = cv2.inRange(hsv_image, np.array([0, 50, 50]), np.array([10, 255, 255]))
113
  red_mask2 = cv2.inRange(hsv_image, np.array([170, 50, 50]), np.array([180, 255, 255]))
114
  red_mask = cv2.bitwise_or(red_mask1, red_mask2)
 
115
  color_counts = {
116
  'green': cv2.countNonZero(green_mask),
117
  'purple': cv2.countNonZero(purple_mask),
 
121
 
122
  def predict_card_features(card_image, shape_detection_model, fill_model, shape_model, box):
123
  """
124
+ For a given card image, detects shapes and batches the cropped shape images for
125
+ classification. Returns a dictionary of features.
126
  """
127
  shape_results = shape_detection_model(card_image)
128
  card_height, card_width = card_image.shape[:2]
 
134
  x1, y1, x2, y2 = detected_box.astype(int)
135
  shape_area = (x2 - x1) * (y2 - y1)
136
  if shape_area > 0.03 * card_area:
137
+ filtered_boxes.append([x1, y1, x2, y2])
138
 
139
+ if len(filtered_boxes) == 0:
140
+ return {'count': 0, 'color': 'unknown', 'fill': 'unknown', 'shape': 'unknown', 'box': box}
141
 
142
+ # Precompute input shapes for classification models
143
+ fill_input_shape = fill_model.input_shape[1:3]
144
+ shape_input_shape = shape_model.input_shape[1:3]
145
+ fill_imgs = []
146
+ shape_imgs = []
147
+ color_list = []
148
+ for fb in filtered_boxes:
149
+ x1, y1, x2, y2 = fb
150
+ shape_img = card_image[y1:y2, x1:x2]
151
+ fill_img = cv2.resize(shape_img, tuple(fill_input_shape)) / 255.0
152
+ shape_img_resized = cv2.resize(shape_img, tuple(shape_input_shape)) / 255.0
153
+ fill_imgs.append(fill_img)
154
+ shape_imgs.append(shape_img_resized)
155
+ color_list.append(predict_color(shape_img))
156
+ fill_imgs = np.array(fill_imgs)
157
+ shape_imgs = np.array(shape_imgs)
158
 
159
+ # Batch predictions (GPU or CPU will be used based on model configuration)
160
+ fill_preds = fill_model.predict(fill_imgs, batch_size=len(fill_imgs))
161
+ shape_preds = shape_model.predict(shape_imgs, batch_size=len(shape_imgs))
162
+ fill_labels_list = ['empty', 'full', 'striped']
163
+ shape_labels_list = ['diamond', 'oval', 'squiggle']
164
+ predicted_fill = [fill_labels_list[np.argmax(pred)] for pred in fill_preds]
165
+ predicted_shape = [shape_labels_list[np.argmax(pred)] for pred in shape_preds]
166
 
167
+ count = min(len(filtered_boxes), 3)
168
+ final_color = max(set(color_list), key=color_list.count)
169
+ final_fill = max(set(predicted_fill), key=predicted_fill.count)
170
+ final_shape = max(set(predicted_shape), key=predicted_shape.count)
171
+
172
+ return {'count': count, 'color': final_color, 'fill': final_fill, 'shape': final_shape, 'box': box}
 
173
 
174
  def is_set(cards):
175
  """
176
+ Returns True if for each feature (Count, Color, Fill, Shape) the values are
177
+ all the same or all different; otherwise, returns False.
178
  """
179
  for feature in ['Count', 'Color', 'Fill', 'Shape']:
180
  if len({card[feature] for card in cards}) not in [1, 3]:
 
183
 
184
  def find_sets(card_df):
185
  """
186
+ Examines every combination of three cards from the DataFrame and returns valid sets.
187
  """
188
  sets_found = []
189
  for combo in combinations(card_df.iterrows(), 3):
 
198
 
199
  def detect_cards_from_image(board_image, card_detection_model):
200
  """
201
+ Uses the YOLO card detection model to detect cards on the board image.
202
  Returns a list of tuples: (cropped card image, bounding box).
203
  """
204
  card_results = card_detection_model(board_image)
 
208
  x1, y1, x2, y2 = box
209
  card_img = board_image[y1:y2, x1:x2]
210
  cards.append((card_img, box))
211
+ return cards, card_boxes
212
 
213
+ def classify_cards_from_board_image(board_image, card_boxes, shape_detection_model, fill_model, shape_model):
214
  """
215
+ For each detected card (using precomputed bounding boxes), predicts its features.
216
  Returns a pandas DataFrame of card feature data.
217
  """
 
218
  card_data = []
219
+ for box in card_boxes:
220
+ x1, y1, x2, y2 = box
221
+ card_img = board_image[y1:y2, x1:x2]
222
+ features = predict_card_features(card_img, shape_detection_model, fill_model, shape_model, box)
223
  card_data.append({
224
  "Count": features['count'],
225
  "Color": features['color'],
 
231
 
232
  def classify_and_find_sets_from_array(board_image, card_detection_model, shape_detection_model, fill_model, shape_model):
233
  """
234
+ Processes a board image by:
235
+ 1. Detecting cards and checking/correcting orientation.
236
+ 2. Re-detecting cards if the image was rotated.
237
+ 3. Classifying each card.
238
+ 4. Finding valid sets and annotating the image.
239
+ Returns a tuple: (sets_found, final_image).
240
  """
241
+ # First detection for orientation check
242
+ _, card_boxes = detect_cards_from_image(board_image, card_detection_model)
243
+ board_image, was_rotated = check_and_rotate_input_image(board_image, card_boxes)
244
+ # Re-run detection if image was rotated to get correct bounding boxes.
245
+ if was_rotated:
246
+ _, card_boxes = detect_cards_from_image(board_image, card_detection_model)
247
+ card_df = classify_cards_from_board_image(board_image, card_boxes, shape_detection_model, fill_model, shape_model)
248
  sets_found = find_sets(card_df)
249
  annotated_image = draw_sets_on_image(board_image.copy(), sets_found)
250
  final_image = restore_original_orientation(annotated_image, was_rotated)
 
255
  # =============================================================================
256
  def draw_sets_on_image(board_image, sets_info):
257
  """
258
+ Draws bounding boxes and set labels on the board image for each detected set.
259
  """
260
  colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255),
261
  (255, 255, 0), (255, 0, 255), (0, 255, 255)]
 
279
  return board_image
280
 
281
  # =============================================================================
282
+ # GRADIO INTERFACE FUNCTION (ZeroGPU)
283
  # =============================================================================
284
+ @spaces.GPU
285
  def detect_and_display_sets_interface(input_image):
286
  """
287
+ Gradio interface function that:
288
+ - Accepts a PIL image.
289
+ - Converts it to OpenCV (BGR) format.
290
+ - Processes it for set detection.
291
+ - Returns the annotated image (as a PIL image) along with a status message.
292
  """
293
  try:
 
294
  image_cv = cv2.cvtColor(np.array(input_image), cv2.COLOR_RGB2BGR)
295
  sets_found, final_image = classify_and_find_sets_from_array(
296
  image_cv,
 
299
  fill_classification_model,
300
  shape_classification_model
301
  )
 
302
  final_image_rgb = cv2.cvtColor(final_image, cv2.COLOR_BGR2RGB)
303
  return Image.fromarray(final_image_rgb), "Sets detected successfully."
304
  except Exception as e:
305
  err = f"❌ Error: {str(e)}\n{traceback.format_exc()}"
 
306
  return Image.fromarray(np.zeros((100, 100, 3), dtype=np.uint8)), err
307
 
308
  # =============================================================================