OCR for Scanned Floor Plan Labels

A companion to Attribute Mapping from Blueprints for the case where the labels are pixels rather than text entities. The technique is ordinary OCR; what makes it work on floor plans is everything around the recognition step.

Why Plan Labels Are Not Document Text

OCR engines are tuned for documents: horizontal lines of text, consistent size, generous white space. A floor plan violates all three. Labels are rotated to fit the room they name, they range from 6 pt room numbers to 24 pt zone headings on the same sheet, and they sit on top of hatching, dimension lines and wall geometry.

The OCR path for scanned floor-plan labels A four-stage pipeline. Text regions are cropped from the raster, each region is deskewed individually rather than relying on a page-level rotation, the crops are recognised with OCR restricted to the expected character set, and each result is accepted only if both its confidence and its pattern match pass. The output is a set of labelled anchor points. Crop, straighten each label, recognise, then decide whether to believe it boxes upright boxes text + confidence 1 Crop text regions from the raster 2 Deskew per region, not per page 3 Recognise OCR with a restricted charset 4 Accept confidence + pattern match labelled anchors

Per-region deskew is the step that changes the numbers. Room labels on a drawing are rotated to fit the room, so a page-level deskew leaves half of them at 90 degrees.

The pipeline that works is therefore mostly preparation. Text regions are located first — by connected-component analysis, or by taking the text layer if the source is a hybrid PDF — and each region is deskewed individually, because a page-level rotation leaves every vertically-set label on its side. Only then does recognition run, and it runs with the character set restricted to what the label is expected to contain.

Minimal Working Example

import logging
import re

import cv2
import numpy as np
import pytesseract

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

PATTERNS = {
    "room_number": (re.compile(r"^\d{1,2}[.\-]\d{2,3}$"), "0123456789.-"),
    "room_name": (re.compile(r"^[A-Za-z][A-Za-z '&\-]{2,}$"),
                  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz '&-"),
    "door_tag": (re.compile(r"^D-?\d{3,4}$"), "D0123456789-"),
}


def read_label(crop: np.ndarray, kind: str, *, min_conf: float = 0.8) -> tuple[str, float] | None:
    """OCR one text crop with a charset restricted to the expected pattern."""
    if kind not in PATTERNS:
        raise KeyError(f"unknown label kind {kind!r}")
    pattern, charset = PATTERNS[kind]

    angle = _dominant_text_angle(crop)
    if abs(angle) > 1.0:
        m = cv2.getRotationMatrix2D((crop.shape[1] / 2, crop.shape[0] / 2), angle, 1.0)
        crop = cv2.warpAffine(crop, m, (crop.shape[1], crop.shape[0]),
                              flags=cv2.INTER_CUBIC, borderValue=255)

    cfg = f"--psm 7 -c tessedit_char_whitelist={charset}"
    try:
        data = pytesseract.image_to_data(crop, config=cfg,
                                         output_type=pytesseract.Output.DICT)
    except pytesseract.TesseractError as exc:
        logger.warning("ocr failed on a %s crop: %s", kind, exc)
        return None

    words = [(t, int(c)) for t, c in zip(data["text"], data["conf"]) if t.strip() and int(c) >= 0]
    if not words:
        return None
    text = " ".join(w for w, _ in words).strip()
    conf = min(c for _, c in words) / 100.0

    if conf < min_conf or not pattern.match(text):
        logger.info("rejected %r (%s, conf %.2f)", text, kind, conf)
        return None
    return text, conf

--psm 7 tells Tesseract the crop is a single line, which matters: the default page-segmentation mode looks for document structure that a 20×60 pixel room-number crop does not have, and frequently returns nothing at all.

Thresholds and Patterns

Acceptance rate against error rate as the OCR confidence threshold rises Two curves against the OCR confidence threshold. The share of labels accepted falls from 96 percent at a threshold of 0.5 to 71 percent at 0.8 and 26 percent at 0.95. The share of accepted labels that are wrong falls much faster, from 14 percent at 0.5 to 1.1 percent at 0.8 and 0.1 percent at 0.95. Rejecting more costs coverage and buys correctness fast 0 25 50 75 100 0.5 0.6 0.7 0.8 0.9 OCR confidence threshold share (%) labels accepted (%) accepted labels that are wrong (%) 0.8: 71% accepted, ~1% of those wrong

A wrong room name is worse than a missing one. A null name is visible and countable; “Stroe 2.O4” is published, indexed and searched for.

Expected label patterns and the character sets they justify A table of five label kinds. A room number matches a pattern of one or two digits, a separator and two or three digits, and is recognised with a charset of digits and separators, which rejects letter confusions. A room name matches a letters-only pattern and rejects digit noise. An area figure matches digits with an optional decimal and a square-metre unit, whose presence validates the parse. A door tag matches D followed by three or four digits and is very high precision. Free text has no pattern and no charset restriction, so it should only be accepted above a confidence of 0.9. Five label kinds, four of them strongly patterned Label kind Pattern Charset Effect Room number ^\d{1,2}[.-]\d{2,3}$ digits . - ● rejects letter confusions Room name ^[A-Za-z][A-Za-z '&-]{2,}$ letters ● rejects digit noise Area figure ^\d+([.,]\d+)?\s?m²$ digits , . m² ● units validate the parse Door tag ^D-?\d{3,4}$ D digits - ● very high precision Free text △ full △ accept only above 0.9 A restricted charset per pattern typically halves the error rate at the same confidence.

Constraining the charset is free accuracy. Most OCR errors are confusions between visually similar characters — 0/O, 1/l, 5/S — and telling the engine that letters are impossible removes the whole class.

Two decisions do most of the work.

The confidence threshold trades coverage against correctness, and the trade is asymmetric. A rejected label leaves a room unnamed, which the resolution ladder reports as a countable gap. An accepted wrong label is published, indexed and searched for, and there is nothing downstream that can tell it is wrong. Setting the threshold at 0.8 accepts about seven labels in ten with roughly one error in a hundred, which is a defensible balance; going lower buys coverage at a rate of error the search index will notice.

The pattern and charset are free accuracy. Most OCR errors are confusions between visually similar characters, and a room number recognised with a digits-only charset simply cannot come back as 2.O4. Validating against a pattern afterwards catches the rest.

Common Errors & Fixes

Half the labels come back empty. Vertical text. Room names are routinely set at 90° to fit a narrow room, and a page-level deskew leaves them there. Detect each crop’s dominant text angle and rotate the crop, not the page.

Room numbers read as names. The wrong pattern was applied, usually because label kind was inferred from size rather than from the drawing layer. Where the source has layers — an A-ANNO-ROOM-NUMB layer is common — use them; where it does not, run both patterns and take whichever matches.

Confidence is high and the text is wrong. Hatching underneath the label is being read as strokes. Mask the geometry layers out of the crop before recognition where the source allows it, or threshold aggressively — plan text is almost always solid black on white and survives a hard binarisation that removes lighter hatch lines.

Accuracy collapses on one building. Check the DPI. Recognition needs roughly 20 pixels of cap height, which at 6 pt text means 300 dpi minimum — the same threshold the raster extraction topic identifies for wall detection, for the same reason.

Integration Point

OCR produces what a vector source gets for free: a text string with an anchor point. From there the path is identical — the anchor is resolved to a room polygon by the ladder in Attribute Mapping from Blueprints, with containment first and a nearest-centroid fallback.

One difference is worth carrying through: an OCR-derived name has a confidence, and a vector-derived one does not. Keeping that confidence on the feature lets the publish gate treat a 0.82-confidence name differently from a certain one, and lets a curator prioritise review by it.

Frequently Asked Questions

Which OCR engine works best on floor plans?

Tesseract with per-crop configuration is usually sufficient and is the easiest to control, which matters more here than raw accuracy. The wins on floor plans come from constraining the problem — single-line segmentation mode, a restricted character set, per-region deskew — and Tesseract exposes all three directly. Cloud OCR services are often more accurate on unconstrained text and give you much less control over exactly these constraints, so they tend to perform worse on 6 pt room numbers sitting on hatching, while costing per page.

Should low-confidence labels be discarded or kept for review?

Kept, with the confidence attached and the name left null on the published feature. Discarding loses information a curator could use, while publishing the guess is the failure this whole page is arranged to avoid. The practical arrangement is a review queue ordered by confidence: a human confirming a hundred 0.6-confidence labels in an hour recovers most of the coverage the threshold gave up, and the room stays honestly unnamed until they do.

Can I skip OCR by asking for a vector export?

Almost always worth asking, and frequently the answer is yes. A surprising share of “scans” are vector PDFs with a raster preview, where the text is extractable directly with no recognition at all — checking for a text layer costs one line and removes the entire pipeline. Where the drawing genuinely only exists on paper, a re-export is impossible and OCR is the only path; but for anything drawn in the last twenty years, the vector source usually exists somewhere in the facilities archive.

This page is a companion to Attribute Mapping from Blueprints, part of the Automated Floor Plan Parsing & Vectorization section.