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 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
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.
Related
- Attribute Mapping from Blueprints — the resolution ladder these recognised labels feed into.
- Resolving Room Names to Polygons — what happens to a label once it has been read.
- PDF & Raster Floor Plan Extraction — the geometry half of the same raster pipeline.
This page is a companion to Attribute Mapping from Blueprints, part of the Automated Floor Plan Parsing & Vectorization section.