Skip to content
Wolren edited this page Apr 30, 2026 · 4 revisions

BCRS Algorithms

BCRS computes the largest axis-aligned rectangle in an arbitrarily-oriented polygon using a two-phase approach:

  1. Vertex-coordinate grid solve (Stage 4)
  2. SDF-guided expansion (Stage 5)
  3. SDF-based certification (Stage 6)

Algorithm Flow

flowchart TD
    A[Stage 1: Geometry preparation] --> B[Stage 2: Heuristic candidates]
    B --> C[Stage 3: Angle refinement]
    C --> D[Stage 4: BCRS boundary-coordinate solve]
    D --> E[Stage 5: SDF-guided expansion]
    E --> F[Stage 6: SDF-based containment certification]
    F --> G{Certification passes?}
    G -- Yes --> H[Stage 7: Select best + output]
    G -- No --> I{Fallback enabled?}
    I -- Yes --> J[Best-effort symmetric shrink]
    I -- No --> K[No rectangle]
    J --> H
Loading

Variants

Variant Description TOP_K
Standard Full BCRS workflow 3
Fast Trial ranking, reduced expensive runs 3

Mathematical Background

Theorem [1]: The largest axis-aligned rectangle inscribed in a simple polygon always has at least two sides aligned with polygon vertex x- or y-coordinates.

Implication: Searching on the vertex-coordinate grid guarantees finding the global optimum.

Corollary: If the polygon has >300 unique coordinates on either axis, the vertex-coordinate approach becomes O(n²) in grid cells, making uniform grid seed preferable.


Stage-by-Stage

Stage 1: Geometry Preparation

  1. Validate polygon: check is_valid and is_empty
  2. If multipart: extract largest component by area
  3. Extract coordinate bounds: minx, miny, maxx, maxy

Stage 2: Edge-Oriented Candidate Generation

  1. Extract all polygon edges from exterior and interiors
  2. Compute edge orientation: atan2(dy, dx) for each edge
  3. Map each orientation to mod 90 to handle symmetry
  4. Bin orientations into histogram (unique edge angles)
  5. Compute convex hull upper bound on max area
  6. Prune angles with bound below current best × 0.90
  7. Run coarse uniform grid (40×40) at each remaining angle
  8. Sort candidates by area, keep top-K

Stage 3: Angle Refinement (Brent Optimization)

For each candidate angle θ from Stage 2:

  1. Set search bounds: [θ - 3°, θ + 3°]
  2. Objective function: run grid solve at angle a, return area
  3. Apply bounded Brent minimization to find local maximum
  4. Refine grid at polished angle (120×120)
  5. Evaluate at polished angle + ±0.5° delta

Stage 4: BCRS Solve

flowchart TD
    A[Extract vertex X coordinates] --> B[Extract vertex Y coordinates]
    B --> C[Add min or max bounds]
    C --> D[Sort unique to X, Y arrays]
    D --> E{len X > 300 or len Y > 300?}
    E -- Yes --> F[Abort BCRS, use uniform grid seed]
    E -- No --> G[Build occupancy mask at cell centers]
    G --> H[For each row: compute heights cumulative mask]
    H --> I[Stack-based LRH on heights array]
    I --> J[For each span compute width, height, depth]
    J --> K[Return rectangle with maximum area]
Loading

This stage finds the maximum-area axis-aligned rectangle at vertex-coordinate precision.

Step 1: Extract coordinate sets

Input: polygon P
Output: sorted unique X, Y arrays

1. Collect all vertex x-coordinates from exterior
2. Collect all vertex x-coordinates from each interior ring
3. Add minx, maxx to the set
4. Remove duplicates, sort → X = [x0, x1, ..., xm]
5. Repeat for y-coordinates → Y = [y0, y1, ..., yn]

If len(X) > 300 or len(Y) > 300: abort BCRS, use uniform grid seed instead.

Step 2: Build occupancy mask

For each grid cell [Xi, Xi+1] × [Yj, Yj+1]:
    Compute cell center: ((Xi + Xi+1)/2, (Yj + Yj+1)/2)
    Test if center is inside polygon using shapely prep.covers
    Set mask[j][i] = 1 if inside, 0 otherwise

Step 3: Variable-pitch LRH

For each row j from 0 to n-1:

1. For each column i:
   heights[i] += mask[j][i]    // accumulate
   heights[i] *= mask[j][i]   // zero out if not occupied

2. Stack-based largest rectangle:
   Initialize empty stack

   For i from 0 to n_cols:
       h = heights[i] (or 0 if i == n_cols)
       start = i
       
       While stack not empty and stack.top.height > h:
           pop top → (start_idx, height)
           width = X[i+1] - X[start_idx]
           depth = Y[j+1] - Y[j - height + 1]  // if height > 0
           area = width × depth
           update best rectangle if area larger
           start = start_idx
       
       push (start, h) onto stack

The key difference from standard LRH: width = X[i+1] - X[start_idx] uses variable pitch from actual vertex coordinates, not a uniform Δx.

Step 4: Return best rectangle

best_rect = box(x0, y0, x1, y1) where bounds are from best area
best_area = (x1 - x0) × (y1 - y0)

Stage 5: SDF-Guided Expansion

flowchart TD
    A[Query SDF at left side midpoint] --> B[Compute d_max]
    B --> C{SDF less than 0?}
    C -- Yes --> D[d_max equals min x0-minx or absolute SDF]
    C -- No --> E[d_max equals x0 minus minx]
    D --> F[Binary search 10 steps]
    E --> F
    F --> G{covers expanded rect?}
    G -- Yes --> H[lo equals mid]
    G -- No --> I[hi equals mid]
    H --> J[Update left side x0]
    I --> J
    J --> K[Repeat for right bottom top]
    K --> L[3 coordinate ascent iterations]
    L --> M[Return expanded bounds]
Loading

This stage expands the rectangle outward while maintaining containment.

Step 1: Define SDF query

SDF(x, y) returns:
    - positive distance if point outside polygon
    - 0 if point on boundary
    - negative distance (inside) to nearest boundary (exterior or hole)

Implementation:

polygon_sdf(point):
    d = polygon.distance(point)
    if d > 0: return d                    // outside
    d_ext = polygon.exterior.distance(point)
    if polygon.contains(point):
        min_d = d_ext
        for hole in polygon.interiors:
            min_d = min(min_d, hole.distance(point))
        return -min_d                      // inside, negative
    if d_ext < 1e-12: return 0            // on exterior
    for hole in polygon.interiors:
        if hole.contains(point): return hole.exterior.distance(point)
    return 0

Step 2: Expansion for each side

For the left side (x0):

1. Query midpoint: s = SDF(x0, (y0 + y1)/2)

2. Compute upper bound:
   If s < 0 (inside): d_max = min(x0 - minx, abs(s))
   Else (outside/on boundary): d_max = x0 - minx

3. Binary search for maximum d:
   lo = 0, hi = d_max
   For step in 1 to 10:
       mid = (lo + hi) / 2
       if covers(P, box(x0 - mid, y0, x1, y1)):
           lo = mid
       else:
           hi = mid
   
   d_left = lo

Repeat for right side (x1), bottom (y0), top (y1).

Step 3: Coordinate ascent iterations

For iteration in 1 to 3:
    Expand left side (compute d_left)
    Expand right side (compute d_right)
    Expand bottom (compute d_bottom)
    Expand top (compute d_top)
    
    If no side expanded: break

The expansion is bounded by SDF, not by vertex coordinates. This provides a tighter bound because SDF uses actual distance to boundary.

Stage 6: Containment Certification

flowchart TD
    A[SDF at corner 1] --> B[SDF at corners 2,3,4]
    B --> C[SDF at 4 edge midpoints]
    C --> D[SDF_max equals maximum of 8 values]
    D --> E{SDF_max less than or equal to epsilon?}
    E -- Yes --> F[Rectangle certified - return]
    E -- No --> G{shrink less than or equal to 20 percent of min side?}
    G -- No --> H[Reject - return None]
    G -- Yes --> I[Apply symmetric shrink]
    I --> J{SDF_max after shrink less than or equal to epsilon?}
    J -- Yes --> K[Return shrunk rectangle]
    J -- No --> L[Reject - return None]
Loading

Step 1: Compute maximum SDF within rectangle

SDF_max = -infinity
For each corner:
    SDF_max = max(SDF_max, SDF(corner))
For each edge midpoint:
    SDF_max = max(SDF_max, SDF(midpoint))

Eight SDF evaluations total (4 corners + 4 midpoints).

Step 2: Certification decision

If SDF_max <= epsilon (1e-7):
    Rectangle is certified
    Return rectangle as-is
Else:
    // Attempt symmetric shrink
    shrink = SDF_max + 2*epsilon
    
    new_half_width = a - shrink
    new_half_height = b - shrink
    
    If new_half_width <= 0 or new_half_height <= 0:
        Return None (cannot certify)
    
    If shrink > min(a, b) × 0.20:
        Return None (excessive shrink)
    
    If max_ratio > 0 and new_half_width/new_half_height > max_ratio:
        new_half_width = new_half_height × max_ratio
    
    Rebuild rectangle with shrunk dimensions
    Re-verify SDF_max <= epsilon
    If passes: return shrunk rectangle
    Else: return None

Stage 7: Selection

  1. For each angle candidate, keep best certified rectangle
  2. Sort by area descending
  3. Return highest-area rectangle with diagnostics:
    • area, angle_deg, ratio
    • cand_rank (0 = best)
    • s2_gain (Stage 3 improvement)
    • s4_gain (BCRS over coarse grid)
    • s5_gain (SDF expansion)
    • best_effort (1 if shrink fallback used)

Algorithm Parameters

Parameter Default Description
GRID_COARSE 40 Coarse uniform grid size
GRID_FINE 120 Vertex-coordinate grid fallback size
TOP_K 3 Number of angle candidates for refinement
MAX_RATIO 0 Max aspect ratio (0 = unlimited)
ALWAYS_RETURN True Enable best-effort shrink fallback
ANGLE_STEP 5.0 Fallback sweep angle step

Performance

Variant TOP_K Time @290 (s) Time @5406 (s)
Standard 3 42.35 TBD
Fast 3 23.61 445.01

Best mode: N_WORKERS=1 (parallelization overhead exceeds per-feature time).


Output Fields

Field Description
area Rectangle area in CRS units
angle_deg Rotation angle in degrees (0-90)
ratio Aspect ratio (long:short)
cand_rank Rank of selected candidate (0 = best)
s2_gain Area gain from Stage 3 (angle polishing)
s4_gain Area gain from Stage 4 (BCRS over coarse)
s5_gain Area gain from Stage 5 (SDF expansion)
best_effort 1 if shrink fallback used

References

[1] Daniels, K., Milenkovic, V., & Roel, G. (1997). Finding the largest axis-aligned rectangle in a polygon. Proceedings of the 13th Canadian Conference on Computational Geometry (CCCG). https://cgm.cs.mcgill.ca/~athena/workshop/2.html

[2] Brent, R.P. (1973). Algorithms for Finding Zeros and Extrema of Functions Without Calculating Derivatives. Prentice-Hall.

[3] Preparata, F.P., & Shamos, M.I. (1985). Computational Geometry: An Introduction. Springer-Verlag.


Navigation

Clone this wiki locally