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()