Wall & Door Detection Algorithms: Implementation Guide for Indoor Mapping Pipelines

Wall and door detection forms the structural backbone of any indoor navigation system. When executed correctly, these algorithms convert raw facility drawings into topologically sound vector networks that power routing engines, space utilization analytics, and emergency egress modeling. This guide details the algorithmic foundations, production-ready Python implementations, and debugging workflows required to integrate detection logic into a robust Automated Floor Plan Parsing & Vectorization pipeline.

1. Pipeline Architecture & Input Normalization

Detection accuracy is directly proportional to input normalization. Raw architectural exports contain inconsistent line weights, overlapping polylines, annotation clutter, and varying coordinate origins. Before applying geometric extraction algorithms, the input must be standardized into a clean, scale-calibrated raster or vector representation.

Raster Normalization & Noise Suppression

Most legacy blueprints arrive as high-DPI TIFFs or scanned PDFs. The first pipeline stage must suppress non-structural elements (furniture, text, hatching) while preserving continuous wall linework. A combination of adaptive thresholding and morphological filtering yields the most reliable baseline for downstream geometric analysis. See the official OpenCV Adaptive Thresholding documentation for parameter tuning guidance.

import cv2
import numpy as np
import logging

logging.basicConfig(level=logging.INFO)

def normalize_floorplan_raster(
    image: np.ndarray, 
    block_size: int = 15, 
    c: int = 8,
    min_contour_area: int = 50
) -> np.ndarray:
    """
    Converts raw BGR/Grayscale floorplan to binary wall mask.
    Applies adaptive thresholding + morphological operations to remove noise.
    
    Args:
        image: Input BGR or grayscale image
        block_size: Size of pixel neighborhood for adaptive thresholding
        c: Constant subtracted from mean/weighted mean
        min_contour_area: Minimum area threshold to retain structural elements
        
    Returns:
        Cleaned binary mask (uint8, 0 or 255)
    """
    if len(image.shape) == 3:
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    elif len(image.shape) == 2:
        gray = image.copy()
    else:
        raise ValueError("Unsupported image dimensions. Expected 2D or 3D array.")

    # Adaptive thresholding handles uneven lighting/scanning artifacts
    binary = cv2.adaptiveThreshold(
        gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
        cv2.THRESH_BINARY_INV, block_size, c
    )

    # Remove isolated pixels and thin annotation lines
    kernel_open = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_open, iterations=2)
    
    # Reconnect minor wall breaks caused by scanning artifacts
    kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    final_mask = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel_close, iterations=1)
    
    # Filter out small noise components using contour area threshold
    contours, _ = cv2.findContours(final_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    mask_filtered = np.zeros_like(final_mask)
    for cnt in contours:
        if cv2.contourArea(cnt) > min_contour_area:
            cv2.drawContours(mask_filtered, [cnt], -1, 255, -1)
            
    logging.info(f"Raster normalization complete. Retained {len(contours)} structural components.")
    return mask_filtered

When processing vector-native exports (DXF, DWG, SVG), bypass rasterization entirely and route inputs through dedicated SVG/DWG Parsing Workflows to preserve sub-pixel coordinate precision and layer metadata.

Coordinate System Alignment & Scale Calibration

Detection thresholds (e.g., door width, wall thickness) must operate in real-world units, not pixels. Establish a pixel-to-meter ratio using known architectural references such as scale bars, standard door widths (0.8–0.9m), or grid spacing.

def calibrate_scale(
    known_length_meters: float,
    pixel_distance: float,
    tolerance_pct: float = 0.05
) -> float:
    """
    Computes pixel-to-meter ratio and validates against architectural standards.
    """
    if pixel_distance <= 0:
        raise ValueError("Pixel distance must be positive.")
        
    ppm = pixel_distance / known_length_meters
    
    # Validate against typical architectural scales (e.g., 1:100, 1:50)
    if ppm < 10 or ppm > 500:
        logging.warning(f"Unusual scale detected: {ppm:.2f} px/m. Verify reference measurement.")
        
    return ppm

2. Structural Wall Extraction

Once normalized, the binary mask must be converted into a vectorized representation. This involves contour tracing, line approximation, and geometric merging to produce a clean wall network.

Contour Tracing & Vectorization

We use cv2.findContours combined with cv2.approxPolyDP to extract wall boundaries. For production systems, it is critical to handle both single-line walls and double-line (hollow) walls common in CAD exports.

from shapely.geometry import LineString, MultiLineString, Polygon
from shapely.ops import unary_union
import math

def extract_wall_segments(
    mask: np.ndarray, 
    ppm: float, 
    epsilon_factor: float = 0.005,
    min_wall_length_m: float = 0.5
) -> list[LineString]:
    """
    Extracts wall centerlines from a binary mask and converts to Shapely LineStrings.
    """
    contours, _ = cv2.findContours(mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    lines = []
    
    for cnt in contours:
        # Approximate contour to reduce vertex count
        perimeter = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, epsilon_factor * perimeter, True)
        
        if len(approx) < 3:
            continue
            
        # Convert to Shapely polygon and extract medial axis approximation
        pts = approx.reshape(-1, 2)
        poly = Polygon(pts)
        
        # Skip non-structural fragments
        if poly.area < (min_wall_length_m * ppm) ** 2:
            continue
            
        # Extract boundary as line segments (simplified for production)
        for i in range(len(pts)):
            start = pts[i]
            end = pts[(i + 1) % len(pts)]
            line = LineString([start, end])
            lines.append(line)
            
    # Merge collinear/overlapping segments
    merged = unary_union(lines)
    return list(merged.geoms) if hasattr(merged, 'geoms') else [merged]

Refer to the Shapely geometry operations manual for advanced topology operations like buffer, intersection, and snap.

3. Door & Opening Detection

Doors appear as intentional gaps in the wall network. Detection relies on identifying discontinuities in wall lines, measuring gap widths, and validating against architectural standards.

Gap Analysis & Semantic Classification

The algorithm scans wall centerlines for breaks exceeding a minimum threshold but falling below a maximum structural opening limit. Gaps are then classified as doors, windows, or archways based on aspect ratio and contextual placement.

def detect_door_openings(
    wall_lines: list[LineString],
    ppm: float,
    min_door_width_m: float = 0.7,
    max_door_width_m: float = 1.5,
    proximity_threshold_m: float = 0.3
) -> list[dict]:
    """
    Identifies door openings by analyzing gaps between wall segments.
    Returns list of door dictionaries with coordinates and metadata.
    """
    doors = []
    proximity_px = proximity_threshold_m * ppm
    
    # Sort lines by proximity to find potential gap endpoints
    endpoints = []
    for line in wall_lines:
        coords = list(line.coords)
        endpoints.extend([coords[0], coords[-1]])
        
    # Pair nearby endpoints to form gaps
    for i, p1 in enumerate(endpoints):
        for j, p2 in enumerate(endpoints):
            if i >= j:
                continue
            dist = math.hypot(p1[0] - p2[0], p1[1] - p2[1])
            
            gap_m = dist / ppm
            if min_door_width_m <= gap_m <= max_door_width_m:
                midpoint = ((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)
                doors.append({
                    "id": f"door_{len(doors)+1}",
                    "start": p1,
                    "end": p2,
                    "midpoint": midpoint,
                    "width_m": gap_m,
                    "type": "standard_door"
                })
                
    logging.info(f"Detected {len(doors)} candidate door openings.")
    return doors

Once openings are identified, attach semantic properties (fire rating, swing direction, accessibility compliance) using the Attribute Mapping from Blueprints framework. This ensures downstream routing engines respect ADA clearances and egress requirements.

4. Topology Construction & Routing Integration

Raw wall and door vectors must be converted into a directed graph where nodes represent decision points (intersections, doors, room centroids) and edges represent traversable corridors.

Graph Generation & Connectivity Validation

  1. Node Generation: Place nodes at wall intersections, door midpoints, and dead ends.
  2. Edge Generation: Connect nodes along navigable paths, excluding wall interiors.
  3. Validation: Run breadth-first search (BFS) from every node to verify full connectivity. Isolated subgraphs indicate detection failures or architectural barriers (e.g., elevators, stairwells requiring vertical routing).

For CAD-native pipelines, Automating wall and door detection in CAD provides layer-specific extraction rules that reduce false positives from annotation blocks and viewport boundaries.

5. Production Troubleshooting & Diagnostics

Symptom Root Cause Diagnostic Step Resolution
Fragmented walls Scanning artifacts, low DPI, or inconsistent line weights Overlay cv2.findContours on original raster. Check min_contour_area. Increase MORPH_CLOSE kernel size; apply cv2.ximgproc.thinning() for skeletonization before vectorization.
False door detections Furniture gaps, hatching, or text blocks misclassified as openings Filter candidates against Attribute Mapping rules. Check proximity to known room centroids. Implement a secondary classifier using aspect ratio + context (doors align with room boundaries, not interior furniture).
Scale drift across floors Inconsistent PDF exports or missing reference markers Compare ppm values across floors. Check for embedded EXIF/DWG scale metadata. Enforce a global calibration step using known architectural constants (e.g., standard corridor width = 1.5m).
Topology loops/disconnects Overlapping wall lines or unmerged collinear segments Run unary_union + snap tolerance. Visualize graph with networkx.draw(). Increase epsilon_factor in approxPolyDP; apply shapely.ops.snap(lines, tolerance=0.5*ppm).

Automated Validation Pipeline

def validate_indoor_topology(
    walls: list[LineString],
    doors: list[dict],
    ppm: float
) -> dict:
    """
    Runs structural and navigational validation checks.
    """
    report = {"valid": True, "issues": []}
    
    # Check door alignment with walls
    for door in doors:
        mid = door["midpoint"]
        aligned = any(wall.distance(LineString([mid, (mid[0]+1, mid[1])])) < 0.5*ppm for wall in walls)
        if not aligned:
            report["issues"].append(f"Door {door['id']} not aligned with wall network.")
            report["valid"] = False
            
    # Check minimum corridor width (simplified buffer overlap)
    corridor_buffer = unary_union([w.buffer(0.75 * ppm) for w in walls])
    if corridor_buffer.is_empty:
        report["issues"].append("No navigable corridor space detected.")
        report["valid"] = False
        
    return report

Conclusion

Robust wall and door detection requires a disciplined pipeline: rigorous input normalization, geometrically sound vectorization, context-aware opening classification, and strict topological validation. By integrating these algorithms into your indoor mapping stack, you establish a reliable foundation for wayfinding, space analytics, and facility automation. Maintain version-controlled calibration parameters, log intermediate raster/vector states, and implement automated topology checks to ensure production resilience across diverse facility types.