Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
| 121 |
internal_points = internal_contour.reshape(-1, 2).tolist()
|
|
|
|
|
|
|
| 122 |
min_dist = float('inf')
|
| 123 |
-
|
| 124 |
-
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
| 129 |
if dist < min_dist:
|
| 130 |
min_dist = dist
|
| 131 |
-
|
| 132 |
-
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
|
|
|
| 136 |
|
| 137 |
if self.debug:
|
| 138 |
-
logger.info(f"[DEBUG] Creating bridge
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
[
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
| 145 |
)
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 181 |
-
"imageWidth":
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
raise ValueError(f"Unsupported augmentation type: {aug_type}")
|
| 364 |
|
| 365 |
-
|
| 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 |
-
#
|
| 381 |
transform = A.Compose([
|
| 382 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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):
|