File size: 3,866 Bytes
36fcf33 1243014 36fcf33 | 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 | import cv2
import numpy as np
import requests
from requests.exceptions import Timeout, RequestException
from skimage import measure
def load_image_from_url(url: str):
"""
Load image from URL and return as BGR numpy array.
Args:
url: Image URL string
Returns:
BGR image as numpy array
Raises:
ValueError: If image cannot be decoded
requests.RequestException: If URL request fails
Timeout: If request times out
"""
try:
# Use tuple for timeout: (connect_timeout, read_timeout)
# connect_timeout: time to establish connection (10 seconds)
# read_timeout: time to read data after connection (60 seconds)
# Increased timeouts to handle slow servers and large images
response = requests.get(url, timeout=(10, 60))
response.raise_for_status()
img = cv2.imdecode(
np.frombuffer(response.content, np.uint8),
cv2.IMREAD_COLOR
)
if img is None:
raise ValueError(f"Failed to decode image from URL: {url}")
return img
except Timeout as e:
raise Timeout(
f"Request to {url} timed out. The server may be slow or unreachable. "
f"Please try again or use a different image URL. Error: {str(e)}"
)
except RequestException as e:
raise RequestException(
f"Failed to fetch image from URL: {url}. Error: {str(e)}"
)
def mask_to_polygon(mask, scale_factors=(1.0, 1.0)):
"""
Convert binary mask to polygon coordinates (CVAT-style).
Uses cv2.findContours and cv2.approxPolyDP like CVAT does.
Includes post-processing to ensure complete polygon coverage.
Args:
mask: Binary mask (numpy array, uint8, 0 or 255)
scale_factors: Tuple (scale_x, scale_y) to scale coordinates FROM original TO display size
Returns:
List of coordinates in CVAT format: [x1, y1, x2, y2, x3, y3, ...]
"""
scale_x, scale_y = scale_factors
# Convert mask to binary format for cv2.findContours
if mask.dtype != np.uint8:
mask = mask.astype(np.uint8)
# Ensure binary mask (0 or 255)
if mask.max() > 1:
mask = (mask > 127).astype(np.uint8) * 255
# Additional smoothing to ensure complete coverage (CVAT-style)
# Small morphological closing to connect nearby regions
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=1)
# Find contours (CVAT-style)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return []
# Get the largest contour by area (most accurate for object shape)
largest_contour = max(contours, key=cv2.contourArea)
# Approximate polygon (CVAT-style, epsilon=1.0)
# Using epsilon relative to contour perimeter for better accuracy
epsilon = max(1.0, cv2.arcLength(largest_contour, True) * 0.001) # Adaptive epsilon
approx_contour = cv2.approxPolyDP(largest_contour, epsilon=epsilon, closed=True)
if approx_contour.shape[0] < 3:
return []
# Flatten and convert to list
polygon = approx_contour.reshape(-1, 2).astype(float)
# Scale coordinates FROM original image size TO display size (inverse of bbox scaling)
# If scale_x > 1, original is larger than display, so we divide
# If scale_x < 1, original is smaller than display, so we divide (still correct)
if scale_x != 1.0 or scale_y != 1.0:
polygon[:, 0] = polygon[:, 0] / scale_x # x coordinates: original -> display
polygon[:, 1] = polygon[:, 1] / scale_y # y coordinates: original -> display
# Flatten to CVAT format: [x1, y1, x2, y2, ...]
return polygon.flatten().tolist()
|