Skip to content

Shared-Wall Normalisation

Shared wall-face detection and normalisation.

Architectural room polygons commonly follow opposite faces of the same physical wall. This module identifies compatible parallel overlaps and moves accepted pairs to one common midline. Rejected candidates are retained as diagnostic metadata so uncertain geometry can be reviewed without silently changing room boundaries.

SharedWallCandidate dataclass

A candidate pair of wall faces that may represent one physical wall.

Source code in src/floorplan_extractor/shared_walls.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
@dataclass(frozen=True)
class SharedWallCandidate:
    """A candidate pair of wall faces that may represent one physical wall."""

    first: WallSegment
    second: WallSegment
    gap: float
    overlap_length: float
    overlap_ratio: float
    overlap: tuple[float, float]
    first_interval: tuple[float, float]
    second_interval: tuple[float, float]
    strip: Polygon

SharedWallConfig dataclass

Configuration for optional shared-wall normalisation.

Attributes:

Name Type Description
enabled bool

Whether shared-wall normalisation should run.

min_gap float

Minimum distance between paired wall faces.

max_gap float

Maximum distance between paired wall faces.

angle_tolerance_degrees float

Maximum angle difference for wall faces to be considered parallel.

min_overlap_ratio float

Minimum projected overlap ratio required for pairing.

min_overlap_length float

Minimum projected overlap length required for pairing.

canonical_line str

Strategy for choosing the replacement shared wall line.

Source code in src/floorplan_extractor/shared_walls.py
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
@dataclass(frozen=True)
class SharedWallConfig:
    """
    Configuration for optional shared-wall normalisation.

    Attributes
    ----------
    enabled : bool
        Whether shared-wall normalisation should run.
    min_gap : float
        Minimum distance between paired wall faces.
    max_gap : float
        Maximum distance between paired wall faces.
    angle_tolerance_degrees : float
        Maximum angle difference for wall faces to be considered parallel.
    min_overlap_ratio : float
        Minimum projected overlap ratio required for pairing.
    min_overlap_length : float
        Minimum projected overlap length required for pairing.
    canonical_line : str
        Strategy for choosing the replacement shared wall line.

    """

    enabled: bool = False
    min_gap: float = 50.0
    max_gap: float = 130.0
    angle_tolerance_degrees: float = 2.0
    min_overlap_ratio: float = 0.75
    min_overlap_length: float = 250.0
    canonical_line: str = "midline"

SharedWallDetectionResult dataclass

Accepted and rejected shared-wall records for downstream diagnostics.

Source code in src/floorplan_extractor/shared_walls.py
133
134
135
136
137
138
@dataclass(frozen=True)
class SharedWallDetectionResult:
    """Accepted and rejected shared-wall records for downstream diagnostics."""

    candidates: list[SharedWallCandidate]
    rejections: list[SharedWallRejection]

SharedWallPairMetrics dataclass

Geometric measurements for a paired wall-face comparison.

Source code in src/floorplan_extractor/shared_walls.py
123
124
125
126
127
128
129
130
@dataclass(frozen=True)
class SharedWallPairMetrics:
    """Geometric measurements for a paired wall-face comparison."""

    gap: float
    overlap: tuple[float, float]
    overlap_length: float
    overlap_ratio: float

SharedWallRejection dataclass

A rejected wall-face pair with diagnostic context.

Source code in src/floorplan_extractor/shared_walls.py
110
111
112
113
114
115
116
117
118
119
120
@dataclass(frozen=True)
class SharedWallRejection:
    """A rejected wall-face pair with diagnostic context."""

    first: WallSegment
    second: WallSegment
    reason: str
    gap: float | None = None
    overlap_length: float | None = None
    overlap_ratio: float | None = None
    blocking_room_id: Hashable | None = None

WallSegment dataclass

A straight exterior segment from one room polygon.

Source code in src/floorplan_extractor/shared_walls.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@dataclass(frozen=True)
class WallSegment:
    """A straight exterior segment from one room polygon."""

    room_id: Hashable
    segment_index: int
    line: LineString
    start: XY
    end: XY
    length: float
    angle_radians: float

    @property
    def key(self) -> tuple[Hashable, int]:
        """Return a stable key for matching and ambiguity checks."""
        return (self.room_id, self.segment_index)

key property

Return a stable key for matching and ambiguity checks.

candidate_midline(candidate)

Return the canonical midline for an accepted shared-wall candidate.

Parameters:

Name Type Description Default
candidate SharedWallCandidate

Candidate whose paired wall-face overlap should be represented.

required

Returns:

Type Description
LineString

Line segment halfway between the paired overlapping wall faces.

Source code in src/floorplan_extractor/shared_walls.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
def candidate_midline(candidate: SharedWallCandidate) -> LineString:
    """
    Return the canonical midline for an accepted shared-wall candidate.

    Parameters
    ----------
    candidate : SharedWallCandidate
        Candidate whose paired wall-face overlap should be represented.

    Returns
    -------
    shapely.geometry.LineString
        Line segment halfway between the paired overlapping wall faces.

    """
    first_start, first_end, second_start, second_end = _candidate_overlap_points(
        candidate
    )

    return LineString(
        [
            _midpoint(first_start, second_start),
            _midpoint(first_end, second_end),
        ]
    )

detect_shared_wall_candidates(rooms, config)

Detect possible shared wall-face pairs without mutating room geometry.

Parameters:

Name Type Description Default
rooms GeoDataFrame

Room polygons to inspect. The GeoDataFrame index is used as the room identifier in returned records.

required
config SharedWallConfig

Geometric thresholds controlling candidate detection.

required

Returns:

Type Description
SharedWallDetectionResult

Accepted candidates and rejected pair records.

Source code in src/floorplan_extractor/shared_walls.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def detect_shared_wall_candidates(
    rooms: gpd.GeoDataFrame,
    config: SharedWallConfig,
) -> SharedWallDetectionResult:
    """
    Detect possible shared wall-face pairs without mutating room geometry.

    Parameters
    ----------
    rooms : geopandas.GeoDataFrame
        Room polygons to inspect. The GeoDataFrame index is used as the room
        identifier in returned records.
    config : SharedWallConfig
        Geometric thresholds controlling candidate detection.

    Returns
    -------
    SharedWallDetectionResult
        Accepted candidates and rejected pair records.

    """
    segments = _extract_wall_segments(rooms)
    pairs = _indexed_segment_pairs(segments, config.max_gap)
    room_index = _build_room_spatial_index(rooms)
    return _detect_shared_wall_candidates_from_pairs(
        config,
        pairs,
        room_index,
    )

normalise_shared_walls(rooms, config, detection=None)

Move accepted shared wall-face overlaps to a common midline.

Parameters:

Name Type Description Default
rooms GeoDataFrame

Labelled room polygons to normalise.

required
config SharedWallConfig

Shared-wall configuration. If disabled, geometries are not changed.

required
detection SharedWallDetectionResult or None

Candidate detection output. If omitted and config is enabled, detection is run using rooms and config.

None

Returns:

Type Description
GeoDataFrame

A copy of rooms with updated geometries and the columns shared_wall_count, shared_wall_rejections, and shared_wall_review. The complete detection result is also stored in result.attrs["shared_wall_detection"].

Source code in src/floorplan_extractor/shared_walls.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def normalise_shared_walls(
    rooms: gpd.GeoDataFrame,
    config: SharedWallConfig,
    detection: SharedWallDetectionResult | None = None,
) -> gpd.GeoDataFrame:
    """
    Move accepted shared wall-face overlaps to a common midline.

    Parameters
    ----------
    rooms : geopandas.GeoDataFrame
        Labelled room polygons to normalise.
    config : SharedWallConfig
        Shared-wall configuration. If disabled, geometries are not changed.
    detection : SharedWallDetectionResult or None
        Candidate detection output. If omitted and config is enabled, detection
        is run using ``rooms`` and ``config``.

    Returns
    -------
    geopandas.GeoDataFrame
        A copy of ``rooms`` with updated geometries and the columns
        ``shared_wall_count``, ``shared_wall_rejections``, and
        ``shared_wall_review``. The complete detection result is also stored in
        ``result.attrs["shared_wall_detection"]``.

    """
    result = rooms.copy()

    if detection is None:
        detection = (
            detect_shared_wall_candidates(rooms, config)
            if config.enabled
            else SharedWallDetectionResult(candidates=[], rejections=[])
        )

    if config.enabled and detection.candidates:
        result.geometry, applied_candidates, application_rejections = (
            _normalised_geometry(result, detection.candidates)
        )
        detection = SharedWallDetectionResult(
            candidates=applied_candidates,
            rejections=[*detection.rejections, *application_rejections],
        )

    return _attach_review_metadata(result, detection)

rejection_overlap_lines(rejection)

Return source-wall lines clipped to a rejected pair's projected overlap.

Parameters:

Name Type Description Default
rejection SharedWallRejection

Rejected wall-face pair to represent.

required

Returns:

Type Description
tuple[LineString, LineString]

Corresponding overlap portions on the first and second wall faces.

Source code in src/floorplan_extractor/shared_walls.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def rejection_overlap_lines(
    rejection: SharedWallRejection,
) -> tuple[LineString, LineString]:
    """
    Return source-wall lines clipped to a rejected pair's projected overlap.

    Parameters
    ----------
    rejection : SharedWallRejection
        Rejected wall-face pair to represent.

    Returns
    -------
    tuple[shapely.geometry.LineString, shapely.geometry.LineString]
        Corresponding overlap portions on the first and second wall faces.

    """
    overlap = _projected_overlap(rejection.first, rejection.second)
    axis = _unit_axis(rejection.first)

    return (
        LineString(
            [
                _point_at_axis_projection(rejection.first, axis, overlap[0]),
                _point_at_axis_projection(rejection.first, axis, overlap[1]),
            ]
        ),
        LineString(
            [
                _point_at_axis_projection(rejection.second, axis, overlap[0]),
                _point_at_axis_projection(rejection.second, axis, overlap[1]),
            ]
        ),
    )