Introduction: Beyond Heavy Deep Learning Models

In the era of deep convolutional neural networks (YOLO, Faster R-CNN, SAM), developers frequently default to training multi-gigabyte deep learning models for even basic object detection tasks. However, in resource-constrained environments—such as real-time browser applications, embedded devices, or mobile web apps—running large neural networks incurs steep penalties: high battery drain, thermal throttling, large bundle sizes, and noticeable frame latency.

When dealing with structured geometric objects (such as playing cards, board game tiles, industrial tags, or barcodes), classical computer vision techniques powered by OpenCV offer superior advantages:

The 5-Stage Vision Pipeline Architecture

To achieve reliable tile detection from a dynamic video feed (such as the system built for Okey Helper), the incoming image stream undergoes five sequential mathematical transformations:

PIPELINE STAGES Architecture
Stage 1: Frame Acquisition & Bilateral Smoothing
Stage 2: Adaptive Thresholding & Canny Edge Detection
Stage 3: Contour Extraction & Aspect-Ratio Filtering
Stage 4: 4-Point Homography Perspective Warp
Stage 5: Dual-Channel HSV Color Segmentation & Template OCR

Step 1: Edge Preservation & Adaptive Binarization

Standard Gaussian blurring smooths out image noise, but it also blurs sharp tile boundaries. A Bilateral Filter (cv2.bilateralFilter) is preferred because it averages pixels based on both spatial proximity and radiometric color similarity, preserving crisp geometric edges while eliminating sensor noise:

preprocessing.py Python
import cv2
import numpy as np

def prepare_frame(frame: np.ndarray) -> np.ndarray:
    # 1. Convert BGR color space to Grayscale
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # 2. Apply Bilateral Filter (diameter=9, sigmaColor=75, sigmaSpace=75)
    filtered = cv2.bilateralFilter(gray, 9, 75, 75)
    
    # 3. Adaptive Thresholding with Gaussian Window
    binary = cv2.adaptiveThreshold(
        filtered,
        255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY_INV,
        15,
        3
    )
    return binary

Step 2: Contour Approximation & Aspect-Ratio Filtering

Once binarized, candidate contours are extracted. To isolate rectangular game tiles from background noise:

  1. Compute the perimeter using cv2.arcLength(cnt, True).
  2. Approximate the polygon using the Ramer-Douglas-Peucker algorithm (cv2.approxPolyDP) with an epsilon factor of $0.03 \times \text{perimeter}$.
  3. Verify that the approximated polygon contains exactly 4 vertices.
  4. Calculate the aspect ratio ($1.3 \le \text{height}/\text{width} \le 1.8$) and reject non-conforming contours.
tile_detector.py Python
def find_tile_quads(binary_image: np.ndarray, min_area: float = 2500.0):
    contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    valid_tiles = []
    
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area < min_area:
            continue
            
        perimeter = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.03 * perimeter, True)
        
        # Must have exactly 4 vertices and be convex
        if len(approx) == 4 and cv2.isContourConvex(approx):
            pts = approx.reshape(4, 2)
            valid_tiles.append(pts)
            
    return valid_tiles

Step 3: Homography & Perspective Rectification

Because cameras view objects at oblique angles, tiles appear skewed as general trapezoids. To perform reliable character and color classification, we compute a planar perspective transform matrix $M$ using cv2.getPerspectiveTransform to warp each detected quad into an orthogonal $128 \times 192$ patch:

warp_perspective.py Python
def rectify_tile(image: np.ndarray, corners: np.ndarray, target_w=128, target_h=192) -> np.ndarray:
    # Sort corner points: Top-Left, Top-Right, Bottom-Right, Bottom-Left
    rect = order_points(corners)
    
    dst = np.array([
        [0, 0],
        [target_w - 1, 0],
        [target_w - 1, target_h - 1],
        [0, target_h - 1]
    ], dtype="float32")
    
    M = cv2.getPerspectiveTransform(rect, dst)
    warped = cv2.warpPerspective(image, M, (target_w, target_h))
    return warped

def order_points(pts: np.ndarray) -> np.ndarray:
    rect = np.zeros((4, 2), dtype="float32")
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]  # Top-left has smallest sum
    rect[2] = pts[np.argmax(s)]  # Bottom-right has largest sum
    
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)] # Top-right has smallest diff
    rect[3] = pts[np.argmax(diff)] # Bottom-left has largest diff
    return rect

Step 4: HSV Color Space Isolation

A common mistake in computer vision is performing thresholding directly in RGB color space. Lighting variations alter all three R, G, and B channels proportionally. In contrast, the HSV (Hue, Saturation, Value) space decouples chromatic color content (Hue) from illumination intensity (Value).

Color Class Hue Interval (OpenCV 0–180) Saturation Range Value Range
Red (Wrap-around) [0, 10] ∪ [170, 180] 120 – 255 70 – 255
Blue [100, 130] 90 – 255 60 – 255
Yellow [20, 35] 110 – 255 100 – 255
Black [0, 180] (Any) 0 – 255 0 – 55 (Low Intensity)

Conclusion & Next Steps

By linking edge-preserving filters, polygonal geometric constraints, perspective homography, and HSV color masking into an integrated pipeline, developers can build lightning-fast, highly accurate visual recognition systems. This approach consumes a fraction of the computational power demanded by neural networks while achieving real-time 60 FPS performance on standard hardware.

AS

Ataberk Susam

Software Developer & Engineering Student

METU Mechanical Engineering student specializing in OpenCV pipelines, real-time vision algorithms, and desktop software engineering.