full_server_v2.py 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215
  1. """
  2. Full Floorplan Pipeline - 5-Stage Server
  3. Endpoints:
  4. POST /process — local folder mode (folder + rgb_pattern)
  5. POST /process-upload — upload image mode (multipart/form-data)
  6. GET /health — health check
  7. Pipeline (5 stages):
  8. 1. extract_initial_mask (BiRefNet)
  9. 2. inference_refine_mask (FLUX.2-klein)
  10. 3. pipeline → JSON
  11. 4. vis → visualization PNG
  12. 5. pixel_to_world → demo.json (world coordinates)
  13. """
  14. import os
  15. import json
  16. import subprocess
  17. import sys
  18. import uuid
  19. import tempfile
  20. import cv2
  21. import numpy as np
  22. import shutil
  23. from pixel_to_world import (
  24. build_vertices_and_segments as _build_vertices_and_segments,
  25. build_shapes as _build_shapes_world,
  26. )
  27. from fastapi import FastAPI, UploadFile, File
  28. from fastapi.responses import JSONResponse
  29. from pydantic import BaseModel, Field
  30. app = FastAPI(title="Floorplan Pipeline")
  31. # ========================================================================
  32. # Stage 1: extract_initial_mask (BiRefNet)
  33. # ========================================================================
  34. def generate_initial_mask(rgb_path, output_path, model_path, img_name):
  35. """Subprocess: red-edge + BiRefNet -> initial_mask"""
  36. result = subprocess.run([
  37. sys.executable, "-c", f'''
  38. import os, gc, torch, onnxruntime, cv2, numpy as np
  39. from PIL import Image
  40. def red_edge_generate(img_path, save_path):
  41. img = cv2.imread(img_path)
  42. img_2 = np.zeros_like(img)
  43. mask = (img[:, :, 0] == 0) * (img[:, :, 1] == 0) * (img[:, :, 2] == 0)
  44. img_2[~mask] = (255, 255, 255)
  45. edges = cv2.Canny(img_2, 50, 150)
  46. kernel = np.ones((3, 3), np.uint8)
  47. edges = cv2.dilate(edges, kernel, 1)
  48. mask = edges[:, :, None] / 255.0
  49. masks = np.concatenate([mask, mask, mask], axis=-1)
  50. img1 = (masks * (0.0, 0.0, 255.0)).clip(0, 255)
  51. alpha = 0.9
  52. img = img1 * alpha + img * (1 - masks * alpha)
  53. cv2.imwrite(save_path, img)
  54. def predict_birefnet_onnx(image_path, onnx_session, mask_dir, input_size=(1024, 1024)):
  55. orig_img = Image.open(image_path).convert("RGB")
  56. w_orig, h_orig = orig_img.size
  57. img_resized = orig_img.resize(input_size, resample=Image.BILINEAR)
  58. img_np = np.array(img_resized).astype(np.float32) / 255.0
  59. mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
  60. std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
  61. img_np = (img_np - mean) / std
  62. img_np = img_np.transpose(2, 0, 1)[np.newaxis, :]
  63. img_np = np.ascontiguousarray(img_np)
  64. input_name = onnx_session.get_inputs()[0].name
  65. outputs = onnx_session.run(None, {{input_name: img_np}})
  66. raw_preds = outputs[-1]
  67. pred_mask = 1 / (1 + np.exp(-raw_preds))
  68. pred_mask = pred_mask.squeeze()
  69. mask_resized = cv2.resize(pred_mask, (w_orig, h_orig), interpolation=cv2.INTER_LINEAR)
  70. mask_8bit = (mask_resized * 255).astype(np.uint8)
  71. kernel = np.ones((3, 3), np.uint8)
  72. mask_eroded = cv2.erode(mask_8bit, kernel, iterations=1)
  73. if not os.path.exists(mask_dir):
  74. os.makedirs(mask_dir)
  75. save_path = os.path.join(mask_dir, os.path.basename(image_path))
  76. cv2.imwrite(save_path, mask_eroded)
  77. red_edge_generate("{rgb_path}", "{rgb_path}")
  78. session = onnxruntime.InferenceSession("{model_path}", providers=[("CUDAExecutionProvider", {{"device_id": 0}})])
  79. temp_dir = "{os.path.dirname(output_path)}/_temp_masks"
  80. os.makedirs(temp_dir, exist_ok=True)
  81. predict_birefnet_onnx("{rgb_path}", session, temp_dir)
  82. temp_path = os.path.join(temp_dir, os.path.basename("{img_name}"))
  83. import shutil
  84. shutil.move(temp_path, "{output_path}")
  85. try:
  86. os.rmdir(temp_dir)
  87. except:
  88. pass
  89. del session
  90. torch.cuda.empty_cache()
  91. torch.cuda.synchronize()
  92. gc.collect()
  93. gc.collect()
  94. ''',
  95. ], capture_output=True, text=True, timeout=300)
  96. if result.returncode != 0:
  97. raise RuntimeError(f"initial_mask 生成失败: {result.stderr}")
  98. # ========================================================================
  99. # Stage 2: inference_refine_mask (FLUX.2-klein)
  100. # ========================================================================
  101. def generate_refine_mask(initial_mask_path, output_path, flux_model_path):
  102. """Subprocess: FLUX.2-klein initial_mask -> refine_mask"""
  103. result = subprocess.run([
  104. sys.executable, "-c", f'''
  105. import gc, torch
  106. import os as _os
  107. _os.environ["CUDA_VISIBLE_DEVICES"] = "1"
  108. from diffusers import Flux2KleinPipeline
  109. from diffusers.utils import load_image
  110. pipe = Flux2KleinPipeline.from_pretrained("{flux_model_path}", torch_dtype=torch.bfloat16)
  111. pipe = pipe.to("cuda")
  112. generator = torch.Generator(device="cuda").manual_seed(0)
  113. image = load_image("{initial_mask_path}")
  114. base_width, base_height = image.size
  115. target_width = (base_width // 8) * 8
  116. target_height = (base_height // 8) * 8
  117. prompt = """
  118. (best quality, 4k), architectural floor plan mask, instance segmentation,
  119. do not add extra blocks,
  120. distinct separate white blocks, clear black gaps between rooms,
  121. separated connected components, clean sharp edges, top-down view,
  122. binary mask style, white rooms on black background, no touching blocks,
  123. The image should be positioned exactly as it was in the original image; do not shift it.
  124. logical room separation
  125. """
  126. pipe_result = pipe(
  127. image=image, prompt=prompt,
  128. height=target_height, width=target_width,
  129. guidance_scale=4.0, num_inference_steps=4,
  130. generator=generator
  131. ).images[0]
  132. pipe_result.resize((base_width, base_height)).save("{output_path}")
  133. del pipe, generator
  134. torch.cuda.empty_cache()
  135. torch.cuda.synchronize()
  136. gc.collect()
  137. gc.collect()
  138. ''',
  139. ], capture_output=True, text=True, timeout=300)
  140. if result.returncode != 0:
  141. raise RuntimeError(f"refine_mask 生成失败: {result.stderr}")
  142. # ========================================================================
  143. # Stage 3: pipeline (core processing) - inline from pipeline.py
  144. # ========================================================================
  145. _model_cache = {}
  146. def _load_yolo(p):
  147. if p not in _model_cache:
  148. from ultralytics import YOLO
  149. _model_cache[p] = YOLO(p)
  150. return _model_cache[p]
  151. def _to_gray(mask):
  152. return mask if len(mask.shape) == 2 else cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
  153. def remove_pure_red(img):
  154. if img is None:
  155. return
  156. red = (img[:, :, 0] == 0) & (img[:, :, 1] == 0) & (img[:, :, 2] == 255)
  157. img[red] = [0, 0, 0]
  158. return img
  159. def remove_edge_regions_image(img):
  160. result = img.copy()
  161. img_2 = np.zeros_like(result)
  162. mask = (result[:, :, 0] == 0) & (result[:, :, 1] == 0) & (result[:, :, 2] == 0)
  163. img_2[~mask] = (255, 255, 255)
  164. edges = cv2.Canny(img_2, 50, 150)
  165. kernel = np.ones((9, 9), np.uint8)
  166. edges = cv2.dilate(edges, kernel, 1)
  167. result[edges > 0] = (0, 0, 0)
  168. return remove_pure_red(result)
  169. def extract_gaps_from_mask(mask):
  170. _, binary = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
  171. kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 25))
  172. stitched = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
  173. gaps = cv2.subtract(stitched, binary)
  174. rk = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
  175. gaps_d = cv2.dilate(gaps, rk, 1)
  176. return gaps, cv2.add(mask, gaps_d)
  177. def extract_mask_region_from_arrays(rgb, ori, full):
  178. _, bo = cv2.threshold(ori, 127, 255, cv2.THRESH_BINARY)
  179. _, bf = cv2.threshold(full, 127, 255, cv2.THRESH_BINARY)
  180. r1 = cv2.bitwise_and(rgb, rgb, mask=bo)
  181. r2 = cv2.bitwise_and(rgb, rgb, mask=bf)
  182. return cv2.subtract(r2, r1)
  183. def _expand_rect(x, y, w, h, ep, wmax, hmax):
  184. if ep <= 0:
  185. return int(x), int(y), int(w), int(h)
  186. return (max(0, int(x) - ep), max(0, int(y) - ep),
  187. max(1, min(wmax, int(x) + int(w) + ep) - max(0, int(x) - ep)),
  188. max(1, min(hmax, int(y) + int(h) + ep) - max(0, int(y) - ep)))
  189. def merge_gap_fillers_from_arrays(m1, m2, image_path="", dilation_kernel_size=5,
  190. center_threshold=50, expand_pixel=10, min_rect_short_side=30):
  191. m1 = _to_gray(m1)
  192. m2 = _to_gray(m2)
  193. if m1.shape != m2.shape:
  194. m2 = cv2.resize(m2, (m1.shape[1], m1.shape[0]))
  195. _, m1b = cv2.threshold(m1, 127, 255, cv2.THRESH_BINARY)
  196. _, m2b = cv2.threshold(m2, 0, 255, cv2.THRESH_BINARY)
  197. num1, bl = cv2.connectedComponents(m1b)
  198. num2, fl, _, _ = cv2.connectedComponentsWithStats(m2b)
  199. result = cv2.cvtColor(m1b, cv2.COLOR_GRAY2BGR)
  200. bridge = []
  201. for i in range(1, num2):
  202. sfm = (fl == i).astype(np.uint8) * 255
  203. k = np.ones((dilation_kernel_size, dilation_kernel_size), np.uint8)
  204. df = cv2.dilate(sfm, k, 1)
  205. tl = np.unique(bl[df > 0])
  206. nb = sorted(int(n) for n in tl if n > 0)
  207. if len(nb) == 2:
  208. pts = np.column_stack(np.where(sfm > 0))
  209. if len(pts) > 0:
  210. cy, cx = np.mean(pts, axis=0)
  211. bridge.append({'id': i, 'cx': cx, 'cy': cy,
  212. 'bp': tuple(int(n - 1) for n in nb), 'mask': sfm})
  213. groups = []
  214. for frag in bridge:
  215. assigned = False
  216. for g in groups:
  217. if g[0]['bp'] != frag['bp']:
  218. continue
  219. for ex in g:
  220. if abs(frag['cx'] - ex['cx']) < center_threshold or abs(frag['cy'] - ex['cy']) < center_threshold:
  221. g.append(frag)
  222. assigned = True
  223. break
  224. if assigned:
  225. break
  226. if not assigned:
  227. groups.append([frag])
  228. areas, rid = [], 0
  229. h, w = m1.shape[:2]
  230. for group in groups:
  231. plist, frects = [], []
  232. for frag in group:
  233. pts = np.column_stack(np.where(frag['mask'] > 0))
  234. if pts.size > 0:
  235. px = pts[:, ::-1]
  236. plist.append(px)
  237. rx, ry, rw, rh = cv2.boundingRect(px)
  238. frects.append((int(rx), int(ry), int(rw), int(rh), frag))
  239. if len(plist) < 1:
  240. continue
  241. ap = np.vstack(plist)
  242. if len(ap) < 3:
  243. continue
  244. rx, ry, rw, rh = cv2.boundingRect(ap)
  245. mss = min(rw, rh)
  246. if len(group) > 1 and mss > min_rect_short_side:
  247. for fx, fy, fw, fh, frag in frects:
  248. if min(fw, fh) <= min_rect_short_side:
  249. fx, fy, fw, fh = _expand_rect(fx, fy, fw, fh, expand_pixel, w, h)
  250. cv2.rectangle(result, (fx, fy), (fx + fw, fy + fh), (0, 255, 0), -1)
  251. if min(fw, fh) >= 25:
  252. areas.append({'id': rid, 'x': fx, 'y': fy, 'w': fw, 'h': fh,
  253. 'block_pair': [int(n) for n in frag['bp']], 'label': 'door'})
  254. rid += 1
  255. continue
  256. if mss <= min_rect_short_side:
  257. x, y, ww, hh = _expand_rect(rx, ry, rw, rh, expand_pixel, w, h)
  258. cv2.rectangle(result, (x, y), (x + ww, y + hh), (0, 255, 0), -1)
  259. if min(ww, hh) >= 25:
  260. areas.append({'id': rid, 'x': x, 'y': y, 'w': ww, 'h': hh,
  261. 'block_pair': [int(n) for n in group[0]['bp']], 'label': 'door'})
  262. rid += 1
  263. return {
  264. 'image_path': str(image_path),
  265. 'image_size': {'width': int(m1.shape[1]), 'height': int(m1.shape[0])},
  266. 'connect_area': areas
  267. }, result, {
  268. 'mask1_blocks': num1 - 1, 'mask2_fragments': num2 - 1,
  269. 'bridge_fragments': len(bridge), 'group_count': len(groups),
  270. 'connect_area_count': len(areas)
  271. }
  272. def build_block_data(rgb_img, block_mask, model_path="room_cls.pt"):
  273. model = _load_yolo(model_path)
  274. blocks = _to_gray(block_mask)
  275. if rgb_img.shape[:2] != blocks.shape[:2]:
  276. rgb_img = cv2.resize(rgb_img, (blocks.shape[1], blocks.shape[0]))
  277. _, bb = cv2.threshold(blocks, 127, 255, cv2.THRESH_BINARY)
  278. num_blocks, bl = cv2.connectedComponents(bb, connectivity=8)
  279. blist = []
  280. for b in range(1, num_blocks):
  281. ms = (bl == b).astype(np.uint8)
  282. cnt, _ = cv2.findContours(ms, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  283. pts, cx, cy = [], 0, 0
  284. if cnt:
  285. lc = max(cnt, key=cv2.contourArea)
  286. simp = cv2.approxPolyDP(lc, 2.0, True)
  287. for pt in simp:
  288. pts.extend([int(pt[0][0]), int(pt[0][1])])
  289. M = cv2.moments(simp)
  290. if M['m00']:
  291. cx, cy = int(M['m10'] / M['m00']), int(M['m01'] / M['m00'])
  292. else:
  293. cx, cy = int(np.mean(simp[:, 0, 0])), int(np.mean(simp[:, 0, 1]))
  294. x, y, w, h = cv2.boundingRect(ms)
  295. if not cnt:
  296. cx, cy = int(x + w / 2), int(y + h / 2)
  297. pad = 20
  298. y1, y2 = max(0, y - pad), min(rgb_img.shape[0], y + h + pad)
  299. x1, x2 = max(0, x - pad), min(rgb_img.shape[1], x + w + pad)
  300. roi = rgb_img[y1:y2, x1:x2].copy()
  301. roi[ms[y1:y2, x1:x2] == 0] = [0, 0, 0]
  302. label, conf = "other_room", 0.0
  303. if roi.shape[0] > 10 and roi.shape[1] > 10:
  304. res = model(roi, verbose=False)[0]
  305. if hasattr(res, 'probs') and res.probs is not None:
  306. tc = float(res.probs.top1conf.cpu().numpy())
  307. if tc >= 0.15:
  308. label, conf = model.names[int(res.probs.top1)], tc
  309. blist.append({"id": b - 1, "points": pts, "label": label, "center": [cx, cy]})
  310. return blist
  311. def detect_furniture(rgb_img, model_path='furniture_detect.onnx'):
  312. model = _load_yolo(model_path)
  313. res = model(rgb_img, conf=0.25, verbose=False)[0]
  314. allowed = {'sofa', 'chair', 'desk', 'bed', 'window'}
  315. fl = []
  316. if len(res.boxes) > 0:
  317. bx = res.boxes.xyxy.cpu().numpy()
  318. bc = res.boxes.cls.cpu().numpy()
  319. for i in range(len(bx)):
  320. b1, y1, b2, y2 = [int(v) for v in bx[i]]
  321. lb = model.names[int(bc[i])]
  322. if lb not in allowed:
  323. continue
  324. fl.append({'id': len(fl), 'label': lb,
  325. 'center': [(b1 + b2) // 2, (y1 + y2) // 2],
  326. 'points': {'x1': b1, 'y1': y1, 'x2': b2, 'y2': y1,
  327. 'x3': b2, 'y3': y2, 'x4': b1, 'y4': y2}})
  328. return fl
  329. # ── Refinement / normalization / merge ──────────────────────────────────
  330. def refine_blocks_in_data(data):
  331. blocks = data.get("block", [])
  332. total = 0
  333. for idx, block in enumerate(blocks):
  334. pd2 = block.get("points", [])
  335. if not pd2:
  336. continue
  337. if isinstance(pd2[0], list) and len(pd2[0]) == 4:
  338. segs = [[float(v) for v in s] for s in pd2]
  339. else:
  340. pts = [[float(pd2[i]), float(pd2[i + 1])] for i in range(0, len(pd2), 2) if i + 1 < len(pd2)]
  341. segs = [[p[0], p[1], pts[(j + 1) % len(pts)][0], pts[(j + 1) % len(pts)][1]] for j, p in enumerate(pts)]
  342. refined = refine_single_block_segments(segs) or segs
  343. segs_int = [[int(round(s[0])), int(round(s[1])), int(round(s[2])), int(round(s[3]))] for s in refined]
  344. block["points"] = segs_int
  345. block["refined"] = True
  346. block["format"] = "segments"
  347. block["segment_count"] = len(segs_int)
  348. total += len(segs_int)
  349. data["format_version"] = "segments_v1"
  350. data["total_segments"] = total
  351. def refine_single_block_segments(segments):
  352. if not segments:
  353. return []
  354. g = orthogonalize_and_move_nodes([segments], 15)[0]
  355. r = apply_user_refinement(g, 30, 30)
  356. r = merge_collinear(r, 2)
  357. r = merge_parallel(r, 12)
  358. return r
  359. def to_key(p):
  360. return (round(float(p[0]), 1), round(float(p[1]), 1))
  361. def is_orthogonal(seg, t=1e-1):
  362. return abs(seg[2] - seg[0]) < t or abs(seg[3] - seg[1]) < t
  363. def orthogonalize_and_move_nodes(sgs, ath):
  364. out = []
  365. for grp in sgs:
  366. np2 = {}
  367. def gsn(p):
  368. pk = to_key(p)
  369. for ep, o in np2.items():
  370. if np.linalg.norm(np.array(pk) - np.array(ep)) < 2.5:
  371. return o
  372. nn = np.array(p, dtype=np.float32)
  373. np2[pk] = nn
  374. return nn
  375. gs = [(gsn(s[:2]), gsn(s[2:])) for s in grp]
  376. for _ in range(3):
  377. for p1, p2 in gs:
  378. dx, dy = abs(p2[0] - p1[0]), abs(p2[1] - p1[1])
  379. a = np.degrees(np.arctan2(dy, dx))
  380. if a < ath or a > (180 - ath):
  381. ay = (p1[1] + p2[1]) / 2
  382. p1[1] = p2[1] = ay
  383. elif abs(a - 90) < ath:
  384. ax = (p1[0] + p2[0]) / 2
  385. p1[0] = p2[0] = ax
  386. out.append([[p1[0], p1[1], p2[0], p2[1]] for p1, p2 in gs])
  387. return out
  388. def apply_user_refinement(grp, lt=30, at=30):
  389. if len(grp) < 2:
  390. return grp
  391. atr = np.radians(at)
  392. si = next((i for i, s in enumerate(grp) if is_orthogonal(s)), -1)
  393. if si == -1:
  394. return grp
  395. wl = [list(s) for s in (grp[si:] + grp[:si])]
  396. ref, i = [], 0
  397. while i < len(wl):
  398. cs = wl[i]
  399. ref.append(cs)
  400. ni = i + 1
  401. if ni >= len(wl):
  402. break
  403. if not is_orthogonal(wl[ni]):
  404. flp, tgl, gs = False, 0, []
  405. pi = ni
  406. while pi < len(wl):
  407. if is_orthogonal(wl[pi]):
  408. flp = True
  409. break
  410. s = wl[pi]
  411. tgl += np.sqrt((s[2] - s[0]) ** 2 + (s[3] - s[1]) ** 2)
  412. gs.append(s)
  413. pi += 1
  414. if flp:
  415. lp = wl[pi]
  416. if tgl < lt:
  417. pe, ls = [cs[2], cs[3]], [lp[0], lp[1]]
  418. ih1, ih2 = abs(cs[3] - cs[1]) < 1e-1, abs(lp[3] - lp[1]) < 1e-1
  419. pm = [pe[0], ls[1]] if ih1 == ih2 else [ls[0], pe[1]] if ih1 else [pe[0], ls[1]]
  420. lp[0], lp[1] = pm[0], pm[1]
  421. ref.append([pe[0], pe[1], lp[0], lp[1]])
  422. i = pi
  423. else:
  424. gs[0][0], gs[0][1] = cs[2], cs[3]
  425. mg, ts = [], list(gs[0])
  426. for k in range(1, len(gs)):
  427. ns = gs[k]
  428. v1, v2 = (ts[2] - ts[0], ts[3] - ts[1]), (ns[2] - ns[0], ns[3] - ns[1])
  429. m1, m2 = np.sqrt(v1[0]**2 + v1[1]**2), np.sqrt(v2[0]**2 + v2[1]**2)
  430. if m1 > 1e-6 and m2 > 1e-6:
  431. ct = abs(v1[0]*v2[0] + v1[1]*v2[1]) / (m1 * m2)
  432. if np.arccos(max(-1, min(1, ct))) < atr:
  433. ts[2], ts[3] = ns[2], ns[3]
  434. continue
  435. mg.append(ts)
  436. ts = list(ns)
  437. ts[0], ts[1] = mg[-1][2], mg[-1][3]
  438. mg.append(ts)
  439. mg[-1][2], mg[-1][3] = lp[0], lp[1]
  440. ref.extend(mg)
  441. i = pi
  442. else:
  443. ref.extend(wl[ni:])
  444. break
  445. else:
  446. i += 1
  447. return ref
  448. def merge_collinear(og, dt=0.5):
  449. if len(og) < 2:
  450. return og
  451. mg, cs = [], list(og[0])
  452. for i in range(1, len(og)):
  453. ns = og[i]
  454. ih = abs(cs[1] - cs[3]) < dt and abs(ns[1] - ns[3]) < dt and abs(cs[3] - ns[1]) < dt
  455. iv = abs(cs[0] - cs[2]) < dt and abs(ns[0] - ns[2]) < dt and abs(cs[2] - ns[0]) < dt
  456. if ih or iv:
  457. cs[2], cs[3] = ns[2], ns[3]
  458. else:
  459. mg.append(cs)
  460. cs = list(ns)
  461. mg.append(cs)
  462. return mg
  463. def merge_parallel(grp, dt=15.0):
  464. if not grp:
  465. return grp
  466. sg = [list(s) for s in grp]
  467. it = 0
  468. while True:
  469. mir, mi = False, set()
  470. for i in range(len(sg)):
  471. if i in mi:
  472. continue
  473. for j in range(i + 1, len(sg)):
  474. if j in mi:
  475. continue
  476. s1, s2 = sg[i], sg[j]
  477. ih1, ih2 = abs(s1[1] - s1[3]) < 1e-1, abs(s2[1] - s2[3]) < 1e-1
  478. iv1, iv2 = abs(s1[0] - s1[2]) < 1e-1, abs(s2[0] - s2[2]) < 1e-1
  479. if ih1 and ih2:
  480. d = abs(s1[1] - s2[1])
  481. ol = min(max(s1[0], s1[2]), max(s2[0], s2[2])) - max(min(s1[0], s1[2]), min(s2[0], s2[2]))
  482. if d < dt and ol > 0:
  483. np2 = (s1[1] + s2[1]) / 2
  484. for s in sg:
  485. if abs(s[1] - s1[1]) < 1e-1 or abs(s[1] - s2[1]) < 1e-1:
  486. s[1] = np2
  487. if abs(s[3] - s1[1]) < 1e-1 or abs(s[3] - s2[1]) < 1e-1:
  488. s[3] = np2
  489. s1[0], s1[2] = min(s1[0], s1[2], s2[0], s2[2]), max(s1[0], s1[2], s2[0], s2[2])
  490. s1[1], s1[3] = min(s1[1], s1[3], s2[1], s2[3]), max(s1[1], s1[3], s2[1], s2[3])
  491. mi.add(j)
  492. mir = True
  493. break
  494. elif iv1 and iv2:
  495. d = abs(s1[0] - s2[0])
  496. ol = min(max(s1[1], s1[3]), max(s2[1], s2[3])) - max(min(s1[1], s1[3]), min(s2[1], s2[3]))
  497. if d < dt and ol > 0:
  498. np2 = (s1[0] + s2[0]) / 2
  499. for s in sg:
  500. if abs(s[0] - s1[0]) < 1e-1 or abs(s[0] - s2[0]) < 1e-1:
  501. s[0] = np2
  502. if abs(s[2] - s1[0]) < 1e-1 or abs(s[2] - s2[0]) < 1e-1:
  503. s[2] = np2
  504. s1[0], s1[2] = min(s1[0], s1[2], s2[0], s2[2]), max(s1[0], s1[2], s2[0], s2[2])
  505. s1[1], s1[3] = min(s1[1], s1[3], s2[1], s2[3]), max(s1[1], s1[3], s2[1], s2[3])
  506. mi.add(j)
  507. mir = True
  508. break
  509. if mir:
  510. break
  511. if mi:
  512. sg = [s for idx, s in enumerate(sg) if idx not in mi]
  513. if not mir:
  514. break
  515. it += 1
  516. if it > 100:
  517. break
  518. return sg
  519. def normalize_segment(seg, angle=10):
  520. x1, y1, x2, y2 = seg
  521. dx, dy = x2 - x1, y2 - y1
  522. if dx == 0 and dy == 0:
  523. return seg
  524. theta = abs(np.degrees(np.arctan2(abs(dy), abs(dx))))
  525. if theta <= angle:
  526. my = round((y1 + y2) / 2)
  527. return [x1, my, x2, my]
  528. if theta >= 90 - angle:
  529. mx = round((x1 + x2) / 2)
  530. return [mx, y1, mx, y2]
  531. return seg
  532. def normalize_all(data, angle=10):
  533. for b in data.get('block', []):
  534. b['points'] = [normalize_segment(s, angle) for s in b['points']]
  535. def segment_orientation(seg):
  536. return 'H' if seg[1] == seg[3] else ('V' if seg[0] == seg[2] else None)
  537. def segments_overlap(sa, sb, ori):
  538. if ori == 'H':
  539. a1, a2, b1, b2 = min(sa[0], sa[2]), max(sa[0], sa[2]), min(sb[0], sb[2]), max(sb[0], sb[2])
  540. else:
  541. a1, a2, b1, b2 = min(sa[1], sa[3]), max(sa[1], sa[3]), min(sb[1], sb[3]), max(sb[1], sb[3])
  542. return a1 <= b2 and b1 <= a2
  543. def cluster_and_merge(segs, ori, thresh):
  544. idxs = [i for i, s in enumerate(segs) if segment_orientation(s) == ori]
  545. if not idxs:
  546. return False
  547. parent = {i: i for i in idxs}
  548. def find(x):
  549. while parent[x] != x:
  550. parent[x] = parent[parent[x]]
  551. x = parent[x]
  552. return x
  553. def union(x, y):
  554. parent[find(x)] = find(y)
  555. for ii in range(len(idxs)):
  556. for jj in range(ii + 1, len(idxs)):
  557. i, j = idxs[ii], idxs[jj]
  558. si, sj = segs[i], segs[j]
  559. ci = si[1] if ori == 'H' else si[0]
  560. cj = sj[1] if ori == 'H' else sj[0]
  561. if abs(ci - cj) <= thresh and segments_overlap(si, sj, ori):
  562. union(i, j)
  563. from collections import defaultdict
  564. clusters = defaultdict(list)
  565. for i in idxs:
  566. clusters[find(i)].append(i)
  567. changed = False
  568. for members in clusters.values():
  569. if len(members) < 2:
  570. continue
  571. coords = [segs[i][1] if ori == 'H' else segs[i][0] for i in members]
  572. ov = list(set(coords))
  573. if len(ov) == 1:
  574. continue
  575. nv = round(sum(coords) / len(coords))
  576. os2 = set(ov)
  577. for k, s in enumerate(segs):
  578. so = segment_orientation(s)
  579. if ori == 'H':
  580. if so == 'H' and s[1] in os2:
  581. segs[k][1] = segs[k][3] = nv
  582. elif so != 'H':
  583. if s[1] in os2:
  584. segs[k][1] = nv
  585. if s[3] in os2:
  586. segs[k][3] = nv
  587. else:
  588. if so == 'V' and s[0] in os2:
  589. segs[k][0] = segs[k][2] = nv
  590. elif so != 'V':
  591. if s[0] in os2:
  592. segs[k][0] = nv
  593. if s[2] in os2:
  594. segs[k][2] = nv
  595. changed = True
  596. return changed
  597. def merge_all_blocks(data, threshold=6):
  598. blocks = data.get('block', [])
  599. all_segs, counts = [], []
  600. for b in blocks:
  601. ss = [list(s) for s in b['points']]
  602. all_segs.extend(ss)
  603. counts.append(len(ss))
  604. ch = True
  605. while ch:
  606. ch = cluster_and_merge(all_segs, 'H', threshold) or cluster_and_merge(all_segs, 'V', threshold)
  607. idx = 0
  608. seen = set()
  609. for b, cnt in zip(blocks, counts):
  610. bs = []
  611. for s in all_segs[idx:idx + cnt]:
  612. k = (min((s[0], s[1]), (s[2], s[3])), max((s[0], s[1]), (s[2], s[3])))
  613. if not (s[0] == s[2] and s[1] == s[3]) and k not in seen:
  614. seen.add(k)
  615. bs.append(s)
  616. b['points'] = bs
  617. b['segment_count'] = len(bs)
  618. idx += cnt
  619. data['total_segments'] = sum(len(x['points']) for x in blocks)
  620. # ── Core pipeline ────────────────────────────────────────────────────────
  621. def run_pipeline(rgb_img, block_mask, room_model, furniture_model,
  622. merge_threshold, angle, dilation_kernel_size,
  623. center_threshold, expand_pixel, min_rect_short_side):
  624. clean = remove_edge_regions_image(rgb_img)
  625. _, gap_mask = extract_gaps_from_mask(block_mask)
  626. conn = extract_mask_region_from_arrays(clean, block_mask, gap_mask)
  627. jd, _, stats = merge_gap_fillers_from_arrays(
  628. block_mask, conn, "", dilation_kernel_size,
  629. center_threshold, expand_pixel, min_rect_short_side)
  630. jd["connect_area_stats"] = stats
  631. print(f"[DEBUG] Stage3门检测: bridge_fragments={stats.get('bridge_fragments', 0)}, "
  632. f"group_count={stats.get('group_count', 0)}, "
  633. f"connect_area_count={stats.get('connect_area_count', 0)}, "
  634. f"mask1_blocks={stats.get('mask1_blocks', 0)}, "
  635. f"mask2_fragments={stats.get('mask2_fragments', 0)}")
  636. jd["block"] = build_block_data(rgb_img, block_mask, room_model)
  637. jd["furniture"] = detect_furniture(rgb_img, furniture_model)
  638. refine_blocks_in_data(jd)
  639. normalize_all(jd, angle)
  640. merge_all_blocks(jd, merge_threshold)
  641. return jd
  642. # ========================================================================
  643. # Stage 4: vis - inline from vis.py
  644. # ========================================================================
  645. COLOR_MAP = {
  646. "living_room": (255, 180, 100),
  647. "bed_room": (100, 255, 100),
  648. "bath_room": (255, 100, 255),
  649. "kitchen_room": (100, 255, 255),
  650. "other_room": (180, 180, 180),
  651. "balcony": (200, 150, 100),
  652. }
  653. DEFAULT_COLOR = (200, 200, 200)
  654. def get_image_size(data):
  655. if 'image_size' in data:
  656. return data['image_size']['width'], data['image_size']['height']
  657. max_x, max_y = 0, 0
  658. for block in data.get('block', []):
  659. segments = block.get('points', [])
  660. if isinstance(segments, list) and len(segments) > 0:
  661. if isinstance(segments[0], list) and len(segments[0]) == 4:
  662. for seg in segments:
  663. max_x = max(max_x, seg[0], seg[2])
  664. max_y = max(max_y, seg[1], seg[3])
  665. for area in data.get('connect_area', []):
  666. max_x = max(max_x, area.get('x', 0) + area.get('w', 0))
  667. max_y = max(max_y, area.get('y', 0) + area.get('h', 0))
  668. return int(max_x) + 100, int(max_y) + 100
  669. def get_points_from_segments(segments):
  670. if not segments:
  671. return []
  672. points_set = set()
  673. for seg in segments:
  674. if len(seg) == 4:
  675. points_set.add((int(seg[0]), int(seg[1])))
  676. points_set.add((int(seg[2]), int(seg[3])))
  677. return list(points_set)
  678. def visualize_json(json_data, output_path, rgb_path=None, vis_door=False):
  679. """从内存 JSON 数据绘制可视化 PNG。"""
  680. width, height = get_image_size(json_data)
  681. if rgb_path and os.path.exists(rgb_path):
  682. canvas = cv2.imread(rgb_path)
  683. else:
  684. canvas = np.zeros((height, width, 3), dtype=np.uint8)
  685. blocks = json_data.get('block', [])
  686. for block_idx, block in enumerate(blocks):
  687. block_id = block.get('id', block_idx)
  688. label = block.get('label', 'door')
  689. segments = block.get('points', [])
  690. center = block.get('center', None)
  691. color = COLOR_MAP.get(label.lower(), DEFAULT_COLOR)
  692. if not (isinstance(segments, list) and len(segments) > 0):
  693. continue
  694. if not (isinstance(segments[0], list) and len(segments[0]) == 4):
  695. continue
  696. points = get_points_from_segments(segments)
  697. if len(points) < 2:
  698. continue
  699. for seg in segments:
  700. p1 = (int(seg[0]), int(seg[1]))
  701. p2 = (int(seg[2]), int(seg[3]))
  702. cv2.line(canvas, p1, p2, color, 2, cv2.LINE_AA)
  703. for pt in points:
  704. cv2.circle(canvas, pt, 4, (255, 255, 255), -1)
  705. if center and len(center) == 2:
  706. cx, cy = int(center[0]), int(center[1])
  707. else:
  708. pts = np.array(points, dtype=np.int32)
  709. M = cv2.moments(pts)
  710. if M["m00"] != 0:
  711. cx = int(M["m10"] / M["m00"])
  712. cy = int(M["m01"] / M["m00"])
  713. else:
  714. cx, cy = int(np.mean(pts[:, 0])), int(np.mean(pts[:, 1]))
  715. font = cv2.FONT_HERSHEY_SIMPLEX
  716. font_scale = 0.6
  717. thickness = 1
  718. (tw, th), _ = cv2.getTextSize(label, font, font_scale, thickness)
  719. cx = max(tw // 2 + 5, min(cx, width - tw // 2 - 5))
  720. cy = max(th // 2 + 5, min(cy, height - th // 2 - 5))
  721. cv2.rectangle(canvas, (cx - tw // 2 - 3, cy - th - 3),
  722. (cx + tw // 2 + 3, cy + 3), (255, 255, 255), -1)
  723. cv2.putText(canvas, label, (cx - tw // 2, cy),
  724. font, font_scale, (0, 0, 0), thickness)
  725. id_text = f"ID:{block_id}"
  726. (iw, ih), _ = cv2.getTextSize(id_text, font, 0.4, 1)
  727. cv2.putText(canvas, id_text, (cx - iw // 2, cy + 15),
  728. font, 0.4, (100, 100, 100), 1)
  729. connect_areas = json_data.get('connect_area', [])
  730. for area_idx, area in enumerate(connect_areas):
  731. x = area.get('x', 0)
  732. y = area.get('y', 0)
  733. w = area.get('w', 0)
  734. h = area.get('h', 0)
  735. label = area.get('label', 'door').lower()
  736. x = max(0, min(x, width - 1))
  737. y = max(0, min(y, height - 1))
  738. w = max(1, min(w, width - x))
  739. h = max(1, min(h, height - y))
  740. if vis_door and "door" in label:
  741. cv2.rectangle(canvas, (x, y), (x + w, y + h), (0, 0, 255), -1)
  742. cv2.rectangle(canvas, (x, y), (x + w, y + h), (255, 255, 255), 1)
  743. furniture_list = json_data.get('furniture', [])
  744. FURNITURE_COLOR = (0, 165, 255)
  745. for item in furniture_list:
  746. label = item.get('label', '')
  747. center = item.get('center', None)
  748. pts = item.get('points', {})
  749. if pts:
  750. bx1, by1 = pts['x1'], pts['y1']
  751. bx2, by2 = pts['x3'], pts['y3']
  752. cv2.rectangle(canvas, (bx1, by1), (bx2, by2), FURNITURE_COLOR, 2)
  753. if center and len(center) == 2:
  754. cx, cy = int(center[0]), int(center[1])
  755. font = cv2.FONT_HERSHEY_SIMPLEX
  756. (tw, th), _ = cv2.getTextSize(label, font, 0.5, 1)
  757. cv2.rectangle(canvas, (cx - tw // 2 - 3, cy - th - 3),
  758. (cx + tw // 2 + 3, cy + 3), FURNITURE_COLOR, -1)
  759. cv2.putText(canvas, label, (cx - tw // 2, cy),
  760. font, 0.5, (255, 255, 255), 1)
  761. cv2.imwrite(output_path, canvas)
  762. return output_path
  763. # ========================================================================
  764. # Stage 5: pixel_to_world - inline from pixel_to_world.py
  765. # ========================================================================
  766. PIXEL_SIZE = 0.01
  767. def _pw(px, py, W, H):
  768. return (round(PIXEL_SIZE * (2 * px - W), 6),
  769. round(PIXEL_SIZE * (H - 2 * py), 6))
  770. def convert_to_demo_format(json_data, output_path):
  771. """将 pipeline JSON 转换为 demo.json格式(世界坐标 + 图结构)。"""
  772. W = json_data["image_size"]["width"]
  773. H = json_data["image_size"]["height"]
  774. raw_blocks = []
  775. pixel_segments_raw = []
  776. seen_px_segs = set()
  777. for block in json_data.get("block", []):
  778. raw_segs = []
  779. pixel_polygon = []
  780. seen_p = set()
  781. for seg in block["points"]:
  782. wx1, wy1 = _pw(seg[0], seg[1], W, H)
  783. wx2, wy2 = _pw(seg[2], seg[3], W, H)
  784. raw_segs.append([wx1, wy1, wx2, wy2])
  785. # 记录像素线段(去重)用于合并
  786. px_key = (round(seg[0], 1), round(seg[1], 1), round(seg[2], 1), round(seg[3], 1))
  787. if px_key not in seen_px_segs:
  788. seen_px_segs.add(px_key)
  789. pixel_segments_raw.append((seg[0], seg[1], seg[2], seg[3]))
  790. for px, py in [(seg[0], seg[1]), (seg[2], seg[3])]:
  791. key = (round(px, 2), round(py, 2))
  792. if key not in seen_p:
  793. seen_p.add(key)
  794. pixel_polygon.append([px, py])
  795. world_polygon = []
  796. seen_w = set()
  797. for seg in raw_segs:
  798. for pt in [(seg[0], seg[1]), (seg[2], seg[3])]:
  799. key = (round(pt[0], 4), round(pt[1], 4))
  800. if key not in seen_w:
  801. seen_w.add(key)
  802. world_polygon.append(list(pt))
  803. raw_blocks.append({
  804. "id": block["id"],
  805. "label": block.get("label", "other_room"),
  806. "raw_segments": raw_segs,
  807. "raw_polygon": pixel_polygon,
  808. "points": world_polygon,
  809. })
  810. furniture = []
  811. windows_from_furniture = []
  812. for furn in json_data.get("furniture", []):
  813. raw = furn.get("points", {})
  814. if isinstance(raw, dict):
  815. pixel_pts = [(raw["x1"], raw["y1"]), (raw["x2"], raw["y2"]),
  816. (raw["x3"], raw["y3"]), (raw["x4"], raw["y4"])]
  817. else:
  818. pixel_pts = raw
  819. world_pts = [list(_pw(p[0], p[1], W, H)) for p in pixel_pts]
  820. item = {
  821. "id": furn.get("id", 0),
  822. "label": furn.get("label", "unknown"),
  823. "points": world_pts,
  824. "raw_pts": pixel_pts,
  825. "score": furn.get("score", 0.9),
  826. }
  827. if furn.get("label", "").lower() == "window":
  828. windows_from_furniture.append(item)
  829. else:
  830. furniture.append(item)
  831. connect_areas = []
  832. for area in json_data.get("connect_area", []):
  833. world_area = {
  834. "id": area.get("id", 0),
  835. "label": area.get("label", "door"),
  836. "block_pair": area.get("block_pair", []),
  837. "_W": W, "_H": H,
  838. }
  839. if "category" in area:
  840. world_area["category"] = area["category"]
  841. if "score" in area:
  842. world_area["score"] = area["score"]
  843. if "x" in area and "w" in area:
  844. # 保留像素坐标用于 bbox 贯穿检测
  845. world_area["_px"] = area["x"]
  846. world_area["_py"] = area["y"]
  847. world_area["_pw"] = area["w"]
  848. world_area["_ph"] = area["h"]
  849. wx, wy = _pw(area["x"], area["y"], W, H)
  850. wx2, wy2 = _pw(area["x"] + area["w"], area["y"] + area["h"], W, H)
  851. world_area["x"] = wx
  852. world_area["y"] = wy
  853. world_area["w"] = abs(wx2 - wx)
  854. world_area["h"] = abs(wy2 - wy)
  855. world_area["points"] = [
  856. [world_area["x"], world_area["y"]],
  857. [world_area["x"] + world_area["w"], world_area["y"]],
  858. [world_area["x"] + world_area["w"], world_area["y"] + world_area["h"]],
  859. [world_area["x"], world_area["y"] + world_area["h"]],
  860. ]
  861. elif "points" in area:
  862. world_area["points"] = [list(_pw(p[0], p[1], W, H)) for p in area["points"]]
  863. connect_areas.append(world_area)
  864. door_areas = [a for a in connect_areas if a.get("label", "").lower() == "door"]
  865. vertices, segments, split_pixel_segments = _build_vertices_and_segments(raw_blocks, W, H, pixel_segments_raw)
  866. print(f"[DEBUG] Stage5转换: connect_area总数={len(connect_areas)}, 门区域={len(door_areas)}, "
  867. f"pixel_segments_raw={len(pixel_segments_raw)}, split_pixel_segments={len(split_pixel_segments) if split_pixel_segments else 0}, "
  868. f"blocks={len(raw_blocks)}")
  869. shapes = _build_shapes_world(raw_blocks, furniture, connect_areas, windows_from_furniture, vertices, segments, W, H, split_pixel_segments)
  870. output = {
  871. "image_size": {"width": W, "height": H},
  872. "floors": [{
  873. "id": 0,
  874. "name": "1楼",
  875. "vertex-xy": [{"id": i, "x": v[0], "y": v[1]} for i, v in enumerate(vertices)],
  876. "segment": segments,
  877. "shapes": shapes,
  878. "cadInfo": {
  879. "cadBoundingBox": {"z_max": "1.5", "z_min": "-1.5"}
  880. },
  881. "subgroup": 0,
  882. "tagging": [],
  883. }]
  884. }
  885. with open(output_path, "w", encoding="utf-8") as f:
  886. json.dump(output, f, indent=2, ensure_ascii=False)
  887. print(f"输出: {output_path}")
  888. print(f" 顶点数: {len(vertices)}")
  889. print(f" 线段数: {len(segments)}")
  890. print(f" 物体数: {len(shapes)} (家具 {len([s for s in shapes if not s['category'].startswith('Tag') and s['category'] not in ('SingleDoor', 'SingleWindow')])}, "
  891. f"房间 {len([s for s in shapes if s['category'].startswith('Tag')])}, "
  892. f"门 {len([s for s in shapes if s['category'] == 'SingleDoor'])}, "
  893. f"窗 {len([s for s in shapes if s['category'] == 'SingleWindow'])})")
  894. return output
  895. # ========================================================================
  896. # Shared pipeline runner (5 stages)
  897. # ========================================================================
  898. def run_5stage(work_dir, rgb_name, rgb_path, im_path, rm_path, base_name,
  899. initial_model, refine_model, room_model, furniture_model,
  900. merge_threshold, angle, dilation_kernel_size,
  901. center_threshold, expand_pixel, min_rect_short_side):
  902. """执行完整的 5 步管线,返回结果 JSON。"""
  903. results = {}
  904. # ── Stage 1: initial_mask ──────────────────────────────────
  905. if os.path.exists(im_path):
  906. print(f"[1/5] initial_mask 已存在,跳过")
  907. else:
  908. print(f"[1/5] 生成 initial_mask ...")
  909. generate_initial_mask(rgb_path, im_path, initial_model, rgb_name)
  910. print(f" OK {im_path}")
  911. # ── Stage 2: refine_mask ───────────────────────────────────
  912. if os.path.exists(rm_path):
  913. print(f"[2/5] refine_mask 已存在,跳过")
  914. else:
  915. print(f"[2/5] 生成 refine_mask ...")
  916. generate_refine_mask(im_path, rm_path, refine_model)
  917. print(f" OK {rm_path}")
  918. # ── Stage 3: pipeline → JSON ───────────────────────────────
  919. print(f"[3/5] 运行管线 ...")
  920. rgb_img = cv2.imread(rgb_path)
  921. block_mask = cv2.imread(rm_path, cv2.IMREAD_GRAYSCALE)
  922. if rgb_img is None:
  923. raise RuntimeError(f"无法读取 RGB: {rgb_path}")
  924. if block_mask is None:
  925. raise RuntimeError(f"无法读取掩码: {rm_path}")
  926. result_json = run_pipeline(
  927. rgb_img, block_mask,
  928. room_model, furniture_model,
  929. merge_threshold, angle,
  930. dilation_kernel_size, center_threshold,
  931. expand_pixel, min_rect_short_side,
  932. )
  933. out_json = os.path.join(work_dir, f"{base_name}.json")
  934. with open(out_json, "w", encoding="utf-8") as f:
  935. json.dump(result_json, f, indent=2, ensure_ascii=False)
  936. print(f" OK {out_json}")
  937. # ── Stage 4: vis → PNG ─────────────────────────────────────
  938. print(f"[4/5] 生成可视化 ...")
  939. vis_path = os.path.join(work_dir, f"{base_name}_vis.png")
  940. visualize_json(result_json, vis_path, rgb_path=rgb_path, vis_door=True)
  941. print(f" OK {vis_path}")
  942. # ── Stage 5: pixel_to_world → demo.json ────────────────────
  943. print(f"[5/5] 像素坐标转世界坐标 ...")
  944. demo_path = os.path.join(work_dir, f"{base_name}_demo.json")
  945. demo_data = convert_to_demo_format(result_json, demo_path)
  946. print(f" OK {demo_path}")
  947. results["task_id"] = work_dir.split("/")[-1]
  948. results["output_dir"] = work_dir
  949. results["vis_path"] = vis_path
  950. results["demo_path"] = demo_path
  951. results["json_path"] = out_json
  952. return demo_data, results
  953. # ========================================================================
  954. # Endpoints
  955. # ========================================================================
  956. class ProcessRequest(BaseModel):
  957. folder: str = Field(..., description="包含 RGB 图片的文件夹路径")
  958. rgb_pattern: str | None = Field(None, description="RGB 文件名前缀")
  959. initial_model: str = Field("initial_mask.onnx")
  960. refine_model: str = Field("./black-forest-labs/FLUX.2-klein-4B")
  961. room_model: str = Field("room_cls.pt")
  962. furniture_model: str = Field("furniture_detect.onnx")
  963. merge_threshold: int = Field(15, ge=1)
  964. angle: int = Field(10, ge=0, le=45)
  965. dilation_kernel_size: int = Field(5, ge=1)
  966. center_threshold: int = Field(50, ge=1)
  967. expand_pixel: int = Field(10, ge=0)
  968. min_rect_short_side: int = Field(30, ge=1)
  969. @app.get("/health")
  970. def health():
  971. return {"status": "ok"}
  972. @app.post("/process-upload")
  973. async def process_upload(
  974. image: UploadFile = File(..., description="上传的 RGB 图片"),
  975. initial_model: str = "initial_mask.onnx",
  976. refine_model: str = "./black-forest-labs/FLUX.2-klein-4B",
  977. room_model: str = "room_cls.pt",
  978. furniture_model: str = "furniture_detect.onnx",
  979. merge_threshold: int = 15,
  980. angle: int = 10,
  981. dilation_kernel_size: int = 5,
  982. center_threshold: int = 50,
  983. expand_pixel: int = 10,
  984. min_rect_short_side: int = 30,
  985. ):
  986. """上传图片并运行完整 5 步管线。
  987. 调用示例:
  988. curl -F "image=@floor.jpg" http://SERVER_IP:8070/process-upload -o result.json
  989. """
  990. task_id = uuid.uuid4().hex[:12]
  991. work_dir = os.path.join(tempfile.gettempdir(), f"floorplan_{task_id}")
  992. os.makedirs(work_dir, exist_ok=True)
  993. original_name = image.filename or "upload.jpg"
  994. safe_name = os.path.basename(original_name)
  995. rgb_path = os.path.join(work_dir, safe_name)
  996. with open(rgb_path, "wb") as f:
  997. content = await image.read()
  998. f.write(content)
  999. base_name = os.path.splitext(safe_name)[0]
  1000. im_path = os.path.join(work_dir, f"initial_mask_{safe_name}")
  1001. rm_path = os.path.join(work_dir, f"refine_mask_{safe_name}")
  1002. try:
  1003. demo_data, _ = run_5stage(
  1004. work_dir, safe_name, rgb_path, im_path, rm_path, base_name,
  1005. initial_model, refine_model, room_model, furniture_model,
  1006. merge_threshold, angle, dilation_kernel_size,
  1007. center_threshold, expand_pixel, min_rect_short_side,
  1008. )
  1009. print(f"\n===== 完成 =====")
  1010. return JSONResponse(content=demo_data)
  1011. except Exception as e:
  1012. import traceback
  1013. traceback.print_exc()
  1014. return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
  1015. @app.post("/process")
  1016. def process(req: ProcessRequest):
  1017. """本地文件夹模式,运行完整 5 步管线。
  1018. Returns: 直接将完整的 JSON 数据返回给客户端
  1019. """
  1020. folder = req.folder
  1021. if not os.path.isdir(folder):
  1022. return JSONResponse(status_code=400, content={"success": False, "error": f"文件夹不存在: {folder}"})
  1023. prefix = req.rgb_pattern or os.path.basename(folder.rstrip("/"))
  1024. rgbs = [f for f in os.listdir(folder)
  1025. if f.startswith(prefix)
  1026. and not f.startswith("initial_mask_")
  1027. and not f.startswith("refine_mask_")
  1028. and f.lower().endswith((".png", ".jpg", ".jpeg"))]
  1029. if not rgbs:
  1030. return JSONResponse(status_code=400, content={"success": False, "error": f"未找到 RGB 图片 (前缀={prefix})"})
  1031. rgb_name = rgbs[0]
  1032. rgb_path = os.path.join(folder, rgb_name)
  1033. base_name = os.path.splitext(rgb_name)[0]
  1034. out_dir = os.path.join(folder, base_name)
  1035. os.makedirs(out_dir, exist_ok=True)
  1036. rgb_in_out = os.path.join(out_dir, rgb_name)
  1037. if not os.path.exists(rgb_in_out):
  1038. shutil.copy2(rgb_path, rgb_in_out)
  1039. print(f"\n===== 开始处理 =====")
  1040. print(f"RGB: {rgb_name}")
  1041. print(f"输出目录: {out_dir}")
  1042. im_path = os.path.join(out_dir, f"initial_mask_{rgb_name}")
  1043. rm_path = os.path.join(out_dir, f"refine_mask_{rgb_name}")
  1044. try:
  1045. demo_data, _ = run_5stage(
  1046. out_dir, rgb_name, rgb_in_out, im_path, rm_path, base_name,
  1047. req.initial_model, req.refine_model, req.room_model, req.furniture_model,
  1048. req.merge_threshold, req.angle, req.dilation_kernel_size,
  1049. req.center_threshold, req.expand_pixel, req.min_rect_short_side,
  1050. )
  1051. print(f"\n===== 完成 =====")
  1052. return JSONResponse(content=demo_data)
  1053. except Exception as e:
  1054. import traceback
  1055. traceback.print_exc()
  1056. return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
  1057. if __name__ == "__main__":
  1058. import uvicorn
  1059. uvicorn.run(app, host="0.0.0.0", port=8070)