initial commit

This commit is contained in:
Jiao77
2025-11-24 20:34:50 +08:00
commit 633749886e
15 changed files with 2665 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
vectorize_skeleton.py
Trace skeleton PNG to Manhattan polylines (simple 4-neighbor tracing) and export JSON.
"""
import argparse
from pathlib import Path
import numpy as np
from PIL import Image
import json
import cv2
from collections import deque
def load_skeleton(path):
im = Image.open(path).convert('L')
a = np.array(im)
return (a > 127).astype('uint8')
def neighbors4(y, x, h, w):
for dy, dx in ((0,1),(1,0),(0,-1),(-1,0)):
ny, nx = y+dy, x+dx
if 0 <= ny < h and 0 <= nx < w:
yield ny, nx
def trace_components(sk):
h, w = sk.shape
visited = np.zeros_like(sk, dtype=bool)
comps = []
for y in range(h):
for x in range(w):
if sk[y,x] and not visited[y,x]:
# BFS to collect component pixels
q = deque()
q.append((y,x))
visited[y,x] = True
pts = []
while q:
cy, cx = q.popleft()
pts.append((int(cx), int(cy)))
for ny, nx in neighbors4(cy, cx, h, w):
if sk[ny,nx] and not visited[ny,nx]:
visited[ny,nx] = True
q.append((ny,nx))
comps.append(pts)
return comps
def prune_segments(segments, min_len=5, merge_tol=3):
if not segments: return []
# 1. Merge consecutive same-direction segments (simple pass)
# Repeat until no more merges to handle multi-segment merges
while True:
merged = []
changed = False
if segments:
curr = segments[0]
for i in range(1, len(segments)):
next_seg = segments[i]
if curr[0] == next_seg[0] and abs(curr[1] - next_seg[1]) <= merge_tol:
# Merge: extend end_idx, re-calculate average val
len1 = curr[3] - curr[2] + 1
len2 = next_seg[3] - next_seg[2] + 1
new_val = int(round((curr[1]*len1 + next_seg[1]*len2) / (len1+len2)))
curr = (curr[0], new_val, curr[2], next_seg[3])
changed = True
else:
merged.append(curr)
curr = next_seg
merged.append(curr)
segments = merged
if not changed:
break
# 2. Remove short tails
# Check start
if len(segments) > 1:
s0 = segments[0]
l0 = s0[3] - s0[2] + 1
if l0 < min_len:
segments.pop(0)
# Check end
if len(segments) > 1:
s_last = segments[-1]
l_last = s_last[3] - s_last[2] + 1
if l_last < min_len:
segments.pop(-1)
# 3. Remove short internal bumps (Z-shape)
# H1 -> V(short) -> H2 => Merge H1, H2 if aligned
# We iterate and build a new list. If merge happens, we modify the next segment and skip.
final_segs = []
i = 0
while i < len(segments):
curr = segments[i]
merged_bump = False
if i + 2 < len(segments):
mid = segments[i+1]
next_seg = segments[i+2]
# Check pattern: A -> B(short) -> A
if curr[0] == next_seg[0] and mid[0] != curr[0]:
len_mid = mid[3] - mid[2] + 1
if len_mid < min_len:
# Check alignment of curr and next_seg
if abs(curr[1] - next_seg[1]) <= merge_tol:
# Merge all three: curr + mid + next_seg -> new_curr
len1 = curr[3] - curr[2] + 1
len3 = next_seg[3] - next_seg[2] + 1
new_val = int(round((curr[1]*len1 + next_seg[1]*len3) / (len1+len3)))
# New segment spans from curr.start to next_seg.end
new_seg = (curr[0], new_val, curr[2], next_seg[3])
# We effectively skip mid and next_seg, and replace curr with new_seg
# But we need to check if this new_seg can merge further?
# For simplicity, let's push new_seg to final_segs and skip 2.
# But wait, if we push to final_segs, we can't merge it with subsequent ones in this loop easily.
# Let's update segments[i+2] to be the merged one and continue loop from i+2.
segments[i+2] = new_seg
i += 2
merged_bump = True
continue
if not merged_bump:
final_segs.append(curr)
i += 1
return final_segs
def simplify_manhattan(pts, tolerance=2):
if not pts: return []
if len(pts) < 2: return pts
segments = []
n = len(pts)
i = 0
while i < n - 1:
start_pt = pts[i]
# Check Horizontal
k_h = i + 1
while k_h < n:
if abs(pts[k_h][1] - start_pt[1]) > tolerance:
break
k_h += 1
len_h = k_h - i
# Check Vertical
k_v = i + 1
while k_v < n:
if abs(pts[k_v][0] - start_pt[0]) > tolerance:
break
k_v += 1
len_v = k_v - i
if len_h >= len_v:
# Horizontal
segment_pts = pts[i:k_h]
avg_y = int(round(np.mean([p[1] for p in segment_pts])))
segments.append(('H', avg_y, i, k_h - 1))
i = k_h - 1
else:
# Vertical
segment_pts = pts[i:k_v]
avg_x = int(round(np.mean([p[0] for p in segment_pts])))
segments.append(('V', avg_x, i, k_v - 1))
i = k_v - 1
# --- Pruning / Refining ---
segments = prune_segments(segments, min_len=5, merge_tol=3)
if not segments:
return []
out_poly = []
# First point
first_seg = segments[0]
first_pt_orig = pts[first_seg[2]]
if first_seg[0] == 'H':
curr = (first_pt_orig[0], first_seg[1])
else:
curr = (first_seg[1], first_pt_orig[1])
out_poly.append(curr)
for idx in range(len(segments) - 1):
s1 = segments[idx]
s2 = segments[idx+1]
# Transition point index is s1[3] (which is same as s2[2])
# But after pruning, s1[3] might not be s2[2] - 1. Gaps might exist.
# We should just connect s1's end to s2's start via a Manhattan corner.
# s1 end point (projected)
if s1[0] == 'H':
p1_end = (pts[s1[3]][0], s1[1])
else:
p1_end = (s1[1], pts[s1[3]][1])
# s2 start point (projected)
if s2[0] == 'H':
p2_start = (pts[s2[2]][0], s2[1])
else:
p2_start = (s2[1], pts[s2[2]][1])
# Connect p1_end to p2_start
if s1[0] == 'H' and s2[0] == 'V':
# H(y1) -> V(x2)
# Intersection is (x2, y1)
corner = (s2[1], s1[1])
if out_poly[-1] != corner: out_poly.append(corner)
elif s1[0] == 'V' and s2[0] == 'H':
# V(x1) -> H(y2)
# Intersection is (x1, y2)
corner = (s1[1], s2[1])
if out_poly[-1] != corner: out_poly.append(corner)
else:
# Parallel segments (should have been merged, but if gap was large...)
# Or H -> H (gap)
# Just connect via midpoint or direct L-shape?
# Let's use p1_end -> p2_start directly? No, need Manhattan.
# H(y1) ... H(y2). Connect (x_end1, y1) -> (x_end1, y2) -> (x_start2, y2) ?
# Or (x_end1, y1) -> (x_start2, y1) -> (x_start2, y2) ?
# Let's use the first one (Vertical bridge).
if out_poly[-1] != p1_end: out_poly.append(p1_end)
if s1[0] == 'H':
# Bridge is Vertical
mid_x = (p1_end[0] + p2_start[0]) // 2
c1 = (mid_x, p1_end[1])
c2 = (mid_x, p2_start[1])
if out_poly[-1] != c1: out_poly.append(c1)
if c1 != c2: out_poly.append(c2)
if c2 != p2_start: out_poly.append(p2_start)
else:
# Bridge is Horizontal
mid_y = (p1_end[1] + p2_start[1]) // 2
c1 = (p1_end[0], mid_y)
c2 = (p2_start[0], mid_y)
if out_poly[-1] != c1: out_poly.append(c1)
if c1 != c2: out_poly.append(c2)
if c2 != p2_start: out_poly.append(p2_start)
# Last point
last_seg = segments[-1]
last_pt_orig = pts[last_seg[3]]
if last_seg[0] == 'H':
end = (last_pt_orig[0], last_seg[1])
else:
end = (last_seg[1], last_pt_orig[1])
if out_poly[-1] != end: out_poly.append(end)
return out_poly
def polyline_from_pixels(pts):
# pts: list of (x,y) unordered; produce simple ordering by greedy nearest neighbor
if not pts:
return []
pts_set = set(pts)
# start from leftmost-topmost
cur = min(pts, key=lambda p: (p[1], p[0]))
seq = [cur]
pts_set.remove(cur)
while pts_set:
# look for 4-neighbor next
x,y = seq[-1]
found = None
for nx, ny in ((x+1,y),(x-1,y),(x,y+1),(x,y-1)):
if (nx,ny) in pts_set:
found = (nx,ny)
break
if found is None:
# fallback: nearest
found = min(pts_set, key=lambda p: (abs(p[0]-x)+abs(p[1]-y), p[1], p[0]))
seq.append(found)
pts_set.remove(found)
return simplify_manhattan(seq, tolerance=2)
def main():
p = argparse.ArgumentParser()
p.add_argument('skel_png')
p.add_argument('outjson')
args = p.parse_args()
sk = load_skeleton(args.skel_png)
comps = trace_components(sk)
polylines = []
for pts in comps:
pl = polyline_from_pixels(pts)
if len(pl) > 1:
polylines.append(pl)
out = {'polylines': polylines, 'num': len(polylines), 'shape': sk.shape}
with open(args.outjson, 'w') as f:
json.dump(out, f)
print('Wrote', args.outjson)
if __name__ == '__main__':
main()