# Grouper: follow the blueprint's drawn lines (plain Python: PIL + numpy) and write them as polylines in ORIGINAL pixels. # Used to read the seams, frames, door, cradle etc. out of the sheet; every polyline is checked by drawing it over the sheet # (reference/trace_lines_*.png). The numbers end up in gr_measure.py. # track_x : follow a line that runs left/right, one column at a time (centre of the nearest line run within +-win px) # track_y : the same for a line that runs up/down # ray : from a centre outward at an angle, the first / the last line pixel (closed shapes: frames, door, windows) import os, json, math import numpy as np from PIL import Image, ImageDraw HERE = os.path.dirname(os.path.abspath(__file__)) REF = os.path.normpath(os.path.join(HERE, "..", "reference")) IM = np.asarray(Image.open(os.path.join(REF, "user_blueprint.jpg")).convert("RGB")).astype(np.int16) R = IM[..., 0] T = 100 def runs_col(x, y0, y1, t=T): col = R[y0:y1, x] > t out, i = [], 0 while i < len(col): if col[i]: j = i while j < len(col) and col[j]: j += 1 out.append((y0 + i + (j - 1 - i) / 2.0, j - i)) i = j else: i += 1 return out def runs_row(y, x0, x1, t=T): row = R[y, x0:x1] > t out, i = [], 0 while i < len(row): if row[i]: j = i while j < len(row) and row[j]: j += 1 out.append((x0 + i + (j - 1 - i) / 2.0, j - i)) i = j else: i += 1 return out def track_x(x0, y0, x1, win=2.5, maxgap=4, step=1, maxrun=6): """follow a line from (x0, y0) to x1; returns [(x, y)]""" pts, y, gap, dy = [], float(y0), 0, 0.0 xs = range(x0, x1 + (1 if x1 >= x0 else -1), step if x1 >= x0 else -step) for x in xs: cands = [(c, n) for c, n in runs_col(x, int(y - win - 1), int(y + win + 2)) if abs(c - (y + dy)) <= win and n <= maxrun] if cands: c = min(cands, key=lambda cn: abs(cn[0] - (y + dy)))[0] if pts: dy = 0.5 * dy + 0.5 * (c - y) y = c pts.append((x, round(y, 1))) gap = 0 else: gap += 1 y += dy if gap > maxgap: break return pts def track_y(x0, y0, y1, win=2.5, maxgap=4, maxrun=6): pts, x, gap, dx = [], float(x0), 0, 0.0 for y in range(y0, y1 + (1 if y1 >= y0 else -1), 1 if y1 >= y0 else -1): cands = [(c, n) for c, n in runs_row(y, int(x - win - 1), int(x + win + 2)) if abs(c - (x + dx)) <= win and n <= maxrun] if cands: c = min(cands, key=lambda cn: abs(cn[0] - (x + dx)))[0] if pts: dx = 0.5 * dx + 0.5 * (c - x) x = c pts.append((round(x, 1), y)) gap = 0 else: gap += 1 x += dx if gap > maxgap: break return pts def ray(cx, cy, ang_deg, r0, r1, which="first", t=T): """walk from r0 to r1 along the angle (deg, 0 = +x, 90 = up); the first or last line run's centre""" a = math.radians(ang_deg) hits, inrun, start = [], False, 0.0 r = r0 while r <= r1: x, y = cx + r * math.cos(a), cy - r * math.sin(a) v = R[int(round(y)), int(round(x))] > t if v and not inrun: inrun, start = True, r if not v and inrun: inrun = False hits.append(0.5 * (start + r - 0.25)) r += 0.25 if not hits: return None rr = hits[0] if which == "first" else hits[-1] return (round(cx + rr * math.cos(a), 1), round(cy - rr * math.sin(a), 1)) def ray_shape(cx, cy, r0, r1, which="first", step=10): pts = [] for k in range(0, 360, step): p = ray(cx, cy, k, r0, r1, which) if p: pts.append(p) return pts def draw(name, box, polys, k=4): x0, y0, x1, y1 = box im = Image.open(os.path.join(REF, "user_blueprint.jpg")).convert("RGB").crop(box) im = im.resize(((x1 - x0) * k, (y1 - y0) * k), Image.LANCZOS) d = ImageDraw.Draw(im) cols = [(255, 40, 40), (255, 220, 0), (0, 255, 120), (255, 120, 255), (255, 150, 0), (120, 255, 255), (255, 255, 255)] for i, (nm, p) in enumerate(polys.items()): if len(p) < 2: continue c = cols[i % len(cols)] q = [((x - x0 + 0.5) * k, (y - y0 + 0.5) * k) for x, y in p] d.line(q, fill=c, width=2) d.text(q[0], nm, fill=c) im.save(os.path.join(REF, name))