laudari commited on
Commit
aefb53c
·
verified ·
1 Parent(s): 1cc9814

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +138 -53
app.py CHANGED
@@ -109,41 +109,55 @@ class PolygonAugmentation:
109
  return simplified
110
 
111
  def create_donut_polygon(self, external_contour: np.ndarray, internal_contours: List[np.ndarray]) -> List[List[float]]:
 
112
  external_points = external_contour.reshape(-1, 2).tolist()
113
  if not internal_contours:
114
  if self.debug:
115
  logger.info("[DEBUG] No internal contours found, returning external points.")
116
  return external_points
117
 
 
118
  result_points = external_points.copy()
119
 
120
- for internal_contour in internal_contours:
 
121
  internal_points = internal_contour.reshape(-1, 2).tolist()
 
 
122
  min_dist = float('inf')
123
- ext_idx = 0
124
- int_idx = 0
125
 
126
- for i, p1 in enumerate(external_points):
127
- for j, p2 in enumerate(internal_points):
128
- dist = np.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)
 
129
  if dist < min_dist:
130
  min_dist = dist
131
- ext_idx = i
132
- int_idx = j
133
 
134
- bridge_to = external_points[ext_idx]
135
- bridge_from = internal_points[int_idx]
 
136
 
137
  if self.debug:
138
- logger.info(f"[DEBUG] Creating bridge between external index {ext_idx} and internal index {int_idx}, distance {min_dist:.2f}")
139
 
140
- new_points = (
141
- result_points[:ext_idx+1] +
142
- internal_points[int_idx:] + internal_points[:int_idx+1] +
143
- [bridge_to] +
144
- external_points[ext_idx+1:]
 
 
 
145
  )
146
- result_points = new_points
 
 
 
 
147
 
148
  return result_points
149
 
@@ -171,16 +185,22 @@ class PolygonAugmentation:
171
  "confidence": 1.0
172
  })
173
 
 
 
 
174
  aug_data = {
175
  "version": original_data.get("version", "5.0.1"),
176
  "flags": original_data.get("flags", {}),
177
  "shapes": new_shapes,
178
  "imagePath": aug_img_name,
179
  "imageData": None,
180
- "imageHeight": aug_image.shape[0],
181
- "imageWidth": aug_image.shape[1]
182
  }
183
 
 
 
 
184
  return aug_data
185
 
186
  def polygons_to_masks(self, image: np.ndarray, polygons: List[List[List[float]]], labels: List[str]) -> Tuple[np.ndarray, List[str]]:
@@ -302,27 +322,49 @@ class PolygonAugmentation:
302
  logger.info(f"[DEBUG] Skipping contour {ext_idx} (area too small: {relative_area:.4f})")
303
  continue
304
 
 
 
305
  is_background = label.lower() in ['background', 'bg', 'back']
306
- if is_background and internal_contours:
 
 
307
  try:
 
308
  donut_points = self.create_donut_polygon(external_contour, internal_contours)
309
  simplified_donut = self.simplify_polygon(donut_points, tolerance=tol, label=label)
 
310
  if len(simplified_donut) >= 3:
311
- poly_labelme = [[round(max(0, min(float(x), width - 1)), 2),
312
- round(max(0, min(float(y), height - 1)), 2)]
313
- for x, y in simplified_donut]
 
 
 
 
314
  all_polygons.append(poly_labelme)
315
  all_labels.append(label)
 
316
  if self.debug:
317
- logger.info(f"[DEBUG] Added donut polygon with {len(poly_labelme)} points.")
 
 
 
 
 
 
 
 
 
318
  except Exception as e:
319
  if self.debug:
320
- logger.info(f"[DEBUG] Error creating donut: {str(e)}, fallback to separate polygons.")
 
321
  self.process_contours(
322
  external_contour, internal_contours, width, height,
323
  label, all_polygons, all_labels, tol
324
  )
325
  else:
 
326
  self.process_contours(
327
  external_contour, internal_contours, width, height,
328
  label, all_polygons, all_labels, tol
@@ -342,31 +384,36 @@ class PolygonAugmentation:
342
  ) -> Tuple[np.ndarray, Dict[str, Any]]:
343
  logger.info(f"Applying augmentation: {aug_type} with parameter {aug_param}")
344
  height, width = image.shape[:2]
345
- crop_scale = random.uniform(0.8, 0.9)
346
- crop_height = int(height * crop_scale)
347
- crop_width = int(width * crop_scale)
348
-
349
- aug_dict = {
350
- "rotate": A.Rotate(limit=aug_param, p=1.0),
351
- "horizontal_flip": A.HorizontalFlip(p=1.0 if aug_param == 1 else 0.0),
352
- "vertical_flip": A.VerticalFlip(p=1.0 if aug_param == 1 else 0.0),
353
- "scale": A.Affine(scale=aug_param, p=1.0),
354
- "brightness_contrast": A.RandomBrightnessContrast(
355
- brightness_limit=aug_param,
356
- contrast_limit=aug_param,
357
- p=1.0
358
- ),
359
- "pixel_dropout": A.PixelDropout(dropout_prob=min(max(aug_param, 0.0), 1.0), p=1.0)
360
- }
361
 
362
- if aug_type not in aug_dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  raise ValueError(f"Unsupported augmentation type: {aug_type}")
364
 
365
- transform = A.Compose([
366
- aug_dict[aug_type],
367
- A.RandomCrop(width=crop_width, height=crop_height, p=0.8)
368
- ])
369
-
370
  masks, mask_labels = self.polygons_to_masks(image, polygons, labels)
371
  if masks.shape[0] == 0:
372
  raise ValueError("No valid masks created from polygons")
@@ -377,10 +424,9 @@ class PolygonAugmentation:
377
  # Create additional targets for each mask
378
  additional_targets = {f'mask{i}': 'mask' for i in range(len(masks_list))}
379
 
380
- # Update transform with additional targets
381
  transform = A.Compose([
382
- aug_dict[aug_type],
383
- A.RandomCrop(width=crop_width, height=crop_height, p=0.8)
384
  ], additional_targets=additional_targets)
385
 
386
  # Prepare input dictionary
@@ -392,10 +438,16 @@ class PolygonAugmentation:
392
  aug_result = transform(**input_dict)
393
  aug_image = aug_result['image']
394
 
395
- # Collect augmented masks
396
  aug_masks_list = []
 
 
397
  for i in range(len(masks_list)):
398
- aug_masks_list.append(aug_result[f'mask{i}'])
 
 
 
 
399
 
400
  aug_masks = np.array(aug_masks_list, dtype=np.uint8)
401
 
@@ -403,13 +455,46 @@ class PolygonAugmentation:
403
  if aug_image is None or aug_image.size == 0:
404
  raise ValueError("Augmented image is empty or invalid")
405
 
 
406
  aug_polygons, aug_labels = self.masks_to_labelme_polygons(
407
  aug_masks, mask_labels, original_areas, self.area_threshold, self.tolerance
408
  )
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  aug_data = self.save_augmented_data(aug_image, aug_polygons, aug_labels, original_data, "input")
411
 
412
- logger.info(f"Augmentation completed: {len(aug_polygons)} polygons generated")
413
  return aug_image, aug_data
414
 
415
  def batch_augment_images(self, image_json_pairs, aug_configs, num_augmentations):
 
109
  return simplified
110
 
111
  def create_donut_polygon(self, external_contour: np.ndarray, internal_contours: List[np.ndarray]) -> List[List[float]]:
112
+ """Create a donut/ring polygon by connecting external and internal contours with bridges"""
113
  external_points = external_contour.reshape(-1, 2).tolist()
114
  if not internal_contours:
115
  if self.debug:
116
  logger.info("[DEBUG] No internal contours found, returning external points.")
117
  return external_points
118
 
119
+ # Start with external contour points
120
  result_points = external_points.copy()
121
 
122
+ # Process each internal contour (hole)
123
+ for hole_idx, internal_contour in enumerate(internal_contours):
124
  internal_points = internal_contour.reshape(-1, 2).tolist()
125
+
126
+ # Find the closest point between external and internal contours
127
  min_dist = float('inf')
128
+ best_ext_idx = 0
129
+ best_int_idx = 0
130
 
131
+ # Check all combinations to find minimum distance
132
+ for i, ext_point in enumerate(result_points):
133
+ for j, int_point in enumerate(internal_points):
134
+ dist = np.sqrt((ext_point[0] - int_point[0])**2 + (ext_point[1] - int_point[1])**2)
135
  if dist < min_dist:
136
  min_dist = dist
137
+ best_ext_idx = i
138
+ best_int_idx = j
139
 
140
+ # Create bridge points
141
+ bridge_start = result_points[best_ext_idx]
142
+ connect_point = internal_points[best_int_idx]
143
 
144
  if self.debug:
145
+ logger.info(f"[DEBUG] Creating bridge for hole {hole_idx}: ext_idx={best_ext_idx}, int_idx={best_int_idx}, distance={min_dist:.2f}")
146
 
147
+ # Insert the internal contour into the result
148
+ # Order: external_points[:best_ext_idx+1] + internal_hole + back_to_external + external_points[best_ext_idx+1:]
149
+ new_result = (
150
+ result_points[:best_ext_idx+1] + # External points up to bridge
151
+ internal_points[best_int_idx:] + # Internal points from connection point to end
152
+ internal_points[:best_int_idx+1] + # Internal points from start to connection point
153
+ [bridge_start] + # Bridge back to external
154
+ result_points[best_ext_idx+1:] # Remaining external points
155
  )
156
+
157
+ result_points = new_result
158
+
159
+ if self.debug:
160
+ logger.info(f"[DEBUG] Created donut polygon with {len(result_points)} total points")
161
 
162
  return result_points
163
 
 
185
  "confidence": 1.0
186
  })
187
 
188
+ # Get actual dimensions from augmented image
189
+ aug_height, aug_width = aug_image.shape[:2]
190
+
191
  aug_data = {
192
  "version": original_data.get("version", "5.0.1"),
193
  "flags": original_data.get("flags", {}),
194
  "shapes": new_shapes,
195
  "imagePath": aug_img_name,
196
  "imageData": None,
197
+ "imageHeight": aug_height, # Use actual augmented image height
198
+ "imageWidth": aug_width # Use actual augmented image width
199
  }
200
 
201
+ if self.debug:
202
+ logger.info(f"[DEBUG] Created augmented data: {len(new_shapes)} shapes, size: {aug_width}x{aug_height}")
203
+
204
  return aug_data
205
 
206
  def polygons_to_masks(self, image: np.ndarray, polygons: List[List[List[float]]], labels: List[str]) -> Tuple[np.ndarray, List[str]]:
 
322
  logger.info(f"[DEBUG] Skipping contour {ext_idx} (area too small: {relative_area:.4f})")
323
  continue
324
 
325
+ # Check if this is a ring/donut shape or complex polygon
326
+ is_ring_shape = label.lower() in ['ring', 'donut', 'annulus', 'circle', 'round'] or len(internal_contours) > 0
327
  is_background = label.lower() in ['background', 'bg', 'back']
328
+
329
+ # Handle different polygon types
330
+ if (is_background or is_ring_shape) and internal_contours:
331
  try:
332
+ # Create donut polygon for rings, backgrounds, or shapes with holes
333
  donut_points = self.create_donut_polygon(external_contour, internal_contours)
334
  simplified_donut = self.simplify_polygon(donut_points, tolerance=tol, label=label)
335
+
336
  if len(simplified_donut) >= 3:
337
+ # Ensure all points are within image boundaries
338
+ poly_labelme = []
339
+ for x, y in simplified_donut:
340
+ clipped_x = round(max(0, min(float(x), width - 1)), 2)
341
+ clipped_y = round(max(0, min(float(y), height - 1)), 2)
342
+ poly_labelme.append([clipped_x, clipped_y])
343
+
344
  all_polygons.append(poly_labelme)
345
  all_labels.append(label)
346
+
347
  if self.debug:
348
+ logger.info(f"[DEBUG] Added {'ring' if is_ring_shape else 'background'} donut polygon with {len(poly_labelme)} points, {len(internal_contours)} holes")
349
+ else:
350
+ if self.debug:
351
+ logger.info(f"[DEBUG] Donut polygon too small after simplification, falling back to separate contours")
352
+ # Fallback to separate contours
353
+ self.process_contours(
354
+ external_contour, internal_contours, width, height,
355
+ label, all_polygons, all_labels, tol
356
+ )
357
+
358
  except Exception as e:
359
  if self.debug:
360
+ logger.info(f"[DEBUG] Error creating donut for {label}: {str(e)}, fallback to separate polygons.")
361
+ # Fallback to processing contours separately
362
  self.process_contours(
363
  external_contour, internal_contours, width, height,
364
  label, all_polygons, all_labels, tol
365
  )
366
  else:
367
+ # Handle regular polygons (no holes or simple shapes)
368
  self.process_contours(
369
  external_contour, internal_contours, width, height,
370
  label, all_polygons, all_labels, tol
 
384
  ) -> Tuple[np.ndarray, Dict[str, Any]]:
385
  logger.info(f"Applying augmentation: {aug_type} with parameter {aug_param}")
386
  height, width = image.shape[:2]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
+ # Setup augmentation based on type with proper parameters
389
+ if aug_type == "rotate":
390
+ # For rotation, use the parameter as degrees and make it more visible
391
+ rotation_angle = aug_param if abs(aug_param) >= 5 else (15 if aug_param >= 0 else -15)
392
+ aug_transform = A.Rotate(limit=abs(rotation_angle), p=1.0, border_mode=cv2.BORDER_CONSTANT, value=0)
393
+ logger.info(f"Applying rotation: {rotation_angle} degrees")
394
+ elif aug_type == "horizontal_flip":
395
+ aug_transform = A.HorizontalFlip(p=1.0 if aug_param == 1 else 0.0)
396
+ elif aug_type == "vertical_flip":
397
+ aug_transform = A.VerticalFlip(p=1.0 if aug_param == 1 else 0.0)
398
+ elif aug_type == "scale":
399
+ # Ensure scale parameter is reasonable
400
+ scale_factor = max(0.5, min(2.0, aug_param))
401
+ aug_transform = A.Affine(scale=scale_factor, p=1.0, keep_ratio=True)
402
+ logger.info(f"Applying scale: {scale_factor}")
403
+ elif aug_type == "brightness_contrast":
404
+ brightness_factor = max(-0.5, min(0.5, aug_param))
405
+ aug_transform = A.RandomBrightnessContrast(
406
+ brightness_limit=abs(brightness_factor),
407
+ contrast_limit=abs(brightness_factor),
408
+ p=1.0
409
+ )
410
+ elif aug_type == "pixel_dropout":
411
+ dropout_prob = min(max(aug_param, 0.0), 0.2)
412
+ aug_transform = A.PixelDropout(dropout_prob=dropout_prob, p=1.0)
413
+ else:
414
  raise ValueError(f"Unsupported augmentation type: {aug_type}")
415
 
416
+ # Create masks from polygons
 
 
 
 
417
  masks, mask_labels = self.polygons_to_masks(image, polygons, labels)
418
  if masks.shape[0] == 0:
419
  raise ValueError("No valid masks created from polygons")
 
424
  # Create additional targets for each mask
425
  additional_targets = {f'mask{i}': 'mask' for i in range(len(masks_list))}
426
 
427
+ # Create transform with proper mask handling
428
  transform = A.Compose([
429
+ aug_transform
 
430
  ], additional_targets=additional_targets)
431
 
432
  # Prepare input dictionary
 
438
  aug_result = transform(**input_dict)
439
  aug_image = aug_result['image']
440
 
441
+ # Collect augmented masks and ensure they match image dimensions
442
  aug_masks_list = []
443
+ aug_height, aug_width = aug_image.shape[:2]
444
+
445
  for i in range(len(masks_list)):
446
+ aug_mask = aug_result[f'mask{i}']
447
+ # Ensure mask dimensions match augmented image
448
+ if aug_mask.shape[:2] != (aug_height, aug_width):
449
+ aug_mask = cv2.resize(aug_mask, (aug_width, aug_height), interpolation=cv2.INTER_NEAREST)
450
+ aug_masks_list.append(aug_mask)
451
 
452
  aug_masks = np.array(aug_masks_list, dtype=np.uint8)
453
 
 
455
  if aug_image is None or aug_image.size == 0:
456
  raise ValueError("Augmented image is empty or invalid")
457
 
458
+ # Convert augmented masks back to polygons
459
  aug_polygons, aug_labels = self.masks_to_labelme_polygons(
460
  aug_masks, mask_labels, original_areas, self.area_threshold, self.tolerance
461
  )
462
 
463
+ # Apply random crop as post-processing to add variety
464
+ if random.random() < 0.3: # 30% chance of cropping
465
+ crop_scale = random.uniform(0.85, 0.95)
466
+ crop_height = int(aug_height * crop_scale)
467
+ crop_width = int(aug_width * crop_scale)
468
+
469
+ # Create crop transform
470
+ crop_transform = A.Compose([
471
+ A.RandomCrop(width=crop_width, height=crop_height, p=1.0)
472
+ ], additional_targets={f'mask{i}': 'mask' for i in range(len(aug_masks_list))})
473
+
474
+ # Apply crop
475
+ crop_input = {'image': aug_image}
476
+ for i, mask in enumerate(aug_masks_list):
477
+ crop_input[f'mask{i}'] = mask
478
+
479
+ crop_result = crop_transform(**crop_input)
480
+ aug_image = crop_result['image']
481
+
482
+ # Update masks after crop
483
+ cropped_masks = []
484
+ for i in range(len(aug_masks_list)):
485
+ cropped_masks.append(crop_result[f'mask{i}'])
486
+
487
+ aug_masks = np.array(cropped_masks, dtype=np.uint8)
488
+
489
+ # Re-convert masks to polygons after crop
490
+ aug_polygons, aug_labels = self.masks_to_labelme_polygons(
491
+ aug_masks, mask_labels, original_areas, self.area_threshold, self.tolerance
492
+ )
493
+
494
+ # Create augmented data with correct dimensions
495
  aug_data = self.save_augmented_data(aug_image, aug_polygons, aug_labels, original_data, "input")
496
 
497
+ logger.info(f"Augmentation completed: {len(aug_polygons)} polygons generated, final size: {aug_image.shape[:2]}")
498
  return aug_image, aug_data
499
 
500
  def batch_augment_images(self, image_json_pairs, aug_configs, num_augmentations):