File size: 4,457 Bytes
080728c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import cv2
import numpy as np
from rembg import remove


def remove_background_and_crop(image_bytes: bytes) -> np.ndarray:
    """

    Production-grade card isolation:

    1. Use rembg to remove background (produces alpha mask)

    2. Analyze contours in the alpha mask

    3. Keep ONLY the most card-like (rectangular) contour

    4. Discard all other objects (coins, clips, fingers, etc.)

    5. Return a tightly cropped BGRA image with clean transparent background

    

    Works for both vertical and horizontal card orientations.

    """
    # Step 1: Run rembg with alpha matting for clean edges
    bg_removed_bytes = remove(
        image_bytes,
        alpha_matting=True,
        alpha_matting_foreground_threshold=240,
        alpha_matting_background_threshold=10,
        alpha_matting_erode_size=10
    )
    
    # Decode result (BGRA)
    nparr = np.frombuffer(bg_removed_bytes, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED)
    
    if img is None:
        raise ValueError("Could not decode image")
    
    if len(img.shape) != 3 or img.shape[2] != 4:
        # No alpha channel — return as is
        return img
    
    # Step 2: Extract alpha and find contours
    alpha = img[:, :, 3]
    _, thresh = cv2.threshold(alpha, 127, 255, cv2.THRESH_BINARY)
    
    # Morphological close to fill small holes
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=3)
    
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    if not contours:
        return img
    
    # Step 3: Score each contour for "card-likeness"
    # A card is: (a) the largest object, (b) very rectangular
    img_area = img.shape[0] * img.shape[1]
    best_contour = None
    best_score = -1
    
    for contour in contours:
        area = cv2.contourArea(contour)
        
        # Skip tiny contours (noise)
        if area < img_area * 0.05:
            continue
        
        # Fit a minimum area rectangle
        rect = cv2.minAreaRect(contour)
        box = cv2.boxPoints(rect)
        rect_area = cv2.contourArea(box)
        
        if rect_area == 0:
            continue
        
        # Rectangularity score: how well the contour fills its bounding rectangle
        # A perfect rectangle scores 1.0; a circle scores ~0.78
        rectangularity = area / rect_area
        
        # Check aspect ratio — standard credit card is 85.6mm x 53.98mm ≈ 1.586
        # Allow range from 1.3 to 1.8 (and its inverse for vertical cards)
        w_rect, h_rect = rect[1]
        if min(w_rect, h_rect) == 0:
            continue
        aspect = max(w_rect, h_rect) / min(w_rect, h_rect)
        
        # Card-like aspect ratio bonus
        if 1.2 <= aspect <= 1.9:
            aspect_score = 1.0
        else:
            aspect_score = 0.3  # Penalize non-card shapes
        
        # Combined score: weighted by area, rectangularity, and aspect ratio
        score = (area / img_area) * rectangularity * aspect_score
        
        if score > best_score:
            best_score = score
            best_contour = contour
    
    if best_contour is None:
        # Fallback: use the largest contour
        best_contour = max(contours, key=cv2.contourArea)
    
    # Step 4: Create a clean mask from ONLY the best contour
    clean_mask = np.zeros(img.shape[:2], dtype=np.uint8)
    cv2.drawContours(clean_mask, [best_contour], -1, 255, -1)
    
    # Step 5: Apply the clean mask to the alpha channel
    # This removes all non-card objects
    new_alpha = cv2.bitwise_and(alpha, clean_mask)
    img[:, :, 3] = new_alpha
    
    # Step 6: Tight crop around the card only
    _, crop_thresh = cv2.threshold(new_alpha, 10, 255, cv2.THRESH_BINARY)
    crop_contours, _ = cv2.findContours(crop_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    if crop_contours:
        largest = max(crop_contours, key=cv2.contourArea)
        x, y, w, h = cv2.boundingRect(largest)
        
        # Minimal padding (just 2px to avoid border clipping)
        pad = 2
        x1 = max(0, x - pad)
        y1 = max(0, y - pad)
        x2 = min(img.shape[1], x + w + pad)
        y2 = min(img.shape[0], y + h + pad)
        
        return img[y1:y2, x1:x2]
    
    return img