2a7f51a202
- Building3D.jsx:React Three Fiber 双良节能大楼 3D 模型 - 按平面图 60×42 网格重建:玻璃顶中庭/成长区/国际区/加速区/楼道/会议室 - 房间编号 A1-A9/I1-I7/G1-G8 + 入驻企业标签 + 门开向过道 - 共享墙/整体地面/一楼挑空大厅 - scripts/parse_layout.py:纯标准库 PNG 像素解析脚本
120 lines
4.3 KiB
Python
120 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
按像素精确解析 PNG 平面图 —— 精确颜色分类 + 高分辨率网格化
|
|
颜色规则(依据实际采样校准):
|
|
W 黑色(0,0,0) = 墙壁
|
|
O 灰色(176,176,176) = 办公室
|
|
P 蓝色(64,176,240) = 过道
|
|
R 绿色(80,176,96) = 休息厅
|
|
Y 橙色(240,112,16) = 共享工位区 / 楼道
|
|
C 青绿(80,224,192) = 玻璃顶
|
|
. 白色(240,240,240) = 空白/底
|
|
"""
|
|
import zlib, struct, sys, collections
|
|
|
|
def read_png(path):
|
|
with open(path, 'rb') as f:
|
|
data = f.read()
|
|
assert data[:8] == b'\x89PNG\r\n\x1a\n', 'Not a PNG'
|
|
pos = 8
|
|
width = height = None
|
|
bit_depth = color_type = None
|
|
idat = b''
|
|
while pos < len(data):
|
|
ln = struct.unpack('>I', data[pos:pos+4])[0]
|
|
typ = data[pos+4:pos+8]
|
|
chunk = data[pos+8:pos+8+ln]
|
|
pos += 12 + ln
|
|
if typ == b'IHDR':
|
|
width, height, bit_depth, color_type, _, _, _ = struct.unpack('>IIBBBBB', chunk)
|
|
elif typ == b'IDAT':
|
|
idat += chunk
|
|
elif typ == b'IEND':
|
|
break
|
|
assert bit_depth == 8 and color_type in (2, 6) and width and height
|
|
raw = zlib.decompress(idat)
|
|
channels = 4 if color_type == 6 else 3
|
|
bpp = channels
|
|
stride = width * bpp
|
|
rows = []
|
|
off = 0
|
|
prev = bytearray(stride)
|
|
for y in range(height):
|
|
ft = raw[off]; off += 1
|
|
line = bytearray(raw[off:off+stride]); off += stride
|
|
if ft == 1:
|
|
for i in range(bpp, stride): line[i] = (line[i] + line[i-bpp]) & 0xFF
|
|
elif ft == 2:
|
|
for i in range(stride): line[i] = (line[i] + prev[i]) & 0xFF
|
|
elif ft == 3:
|
|
for i in range(stride):
|
|
a = line[i-bpp] if i >= bpp else 0
|
|
line[i] = (line[i] + ((a + prev[i]) >> 1)) & 0xFF
|
|
elif ft == 4:
|
|
for i in range(stride):
|
|
a = line[i-bpp] if i >= bpp else 0
|
|
b = prev[i]
|
|
c = prev[i-bpp] if i >= bpp else 0
|
|
p = a + b - c
|
|
pa, pb, pc = abs(p-a), abs(p-b), abs(p-c)
|
|
pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
|
|
line[i] = (line[i] + pr) & 0xFF
|
|
rows.append(bytes(line))
|
|
prev = line
|
|
rgba = []
|
|
for line in rows:
|
|
if channels == 4:
|
|
rgba.append([(line[i], line[i+1], line[i+2], line[i+3]) for i in range(0, stride, 4)])
|
|
else:
|
|
rgba.append([(line[i], line[i+1], line[i+2], 255) for i in range(0, stride, 3)])
|
|
return width, height, rgba
|
|
|
|
# 精确色板(中心色 + 容差)
|
|
PALETTE = [
|
|
('W', (0, 0, 0), 70), # 黑 = 墙壁
|
|
('O', (176, 176, 176), 40), # 灰 = 办公室
|
|
('P', (64, 176, 240), 45), # 蓝 = 过道
|
|
('R', (80, 176, 96), 40), # 绿 = 休息厅
|
|
('Y', (240, 112, 16), 55), # 橙 = 共享工位/楼道
|
|
('C', (80, 224, 192), 55), # 青绿 = 玻璃顶
|
|
('.', (240, 240, 240), 40), # 白 = 空白
|
|
]
|
|
|
|
def classify(rgb):
|
|
r, g, b = rgb[:3]
|
|
best, best_d = '?', 1e9
|
|
for tag, (cr, cg, cb), tol in PALETTE:
|
|
d = abs(r-cr) + abs(g-cg) + abs(b-cb)
|
|
if d <= tol * 3 and d < best_d:
|
|
best, best_d = tag, d
|
|
return best
|
|
|
|
def main(path, grid_cols=48, grid_rows=28, sub=3):
|
|
w, h, rgba = read_png(path)
|
|
print(f'PNG: {w}x{h} 网格: {grid_cols}x{grid_rows}')
|
|
cw, ch = w / grid_cols, h / grid_rows
|
|
grid = []
|
|
for gy in range(grid_rows):
|
|
row = []
|
|
for gx in range(grid_cols):
|
|
cx = int((gx + 0.5) * cw)
|
|
cy = int((gy + 0.5) * ch)
|
|
votes = collections.Counter()
|
|
rad_x, rad_y = max(2, int(cw*0.3)), max(2, int(ch*0.3))
|
|
for dy in range(-rad_y, rad_y+1, sub):
|
|
for dx in range(-rad_x, rad_x+1, sub):
|
|
yy, xx = cy+dy, cx+dx
|
|
if 0 <= yy < h and 0 <= xx < w:
|
|
votes[classify(rgba[yy][xx])] += 1
|
|
row.append(votes.most_common(1)[0][0])
|
|
grid.append(row)
|
|
# 打印
|
|
print(' ' + ''.join(str(c % 10) for c in range(grid_cols)))
|
|
for gy, row in enumerate(grid):
|
|
print(f'{gy:2d} ' + ''.join(row))
|
|
return grid
|
|
|
|
if __name__ == '__main__':
|
|
path = sys.argv[1] if len(sys.argv) > 1 else '详细的布局.png'
|
|
main(path)
|