fix(ground): 지형이 아닌 일정 EL 객체(기호·윤곽선)를 높이 정합성으로 제외
- reject_height_outliers: 반경 15 m(부족 시 45 m) 내 서로 다른 높이 값의 중앙값과 비교 - |편차| > 10 m 이고 주변 높이 범위 밖으로 5 m 초과 시에만 제외, 최악 높이 묶음부터 반복 - 1 mm 이내 높이는 같은 묶음(CAD 부동소수 오차), 제외 수는 rejected_height_outliers로 기록 - 운곡교: EL 5.624(306)·-112.719(16)만 제외, 격자 최저 -85.98→21.50 m, 8 m 초과 튐 88→1 - 산외: -22.21·127.4 제외, 봉우리 후보 87.35 유지 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d740406a8c
commit
b2bebe47eb
2 files changed
+63
-3
No files matched your search
@@ -17,6 +17,45 @@ from pierzainer_ground.dtm import DTM
|
|||||||
ROOT=Path(__file__).resolve().parent.parent
|
ROOT=Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def reject_height_outliers(entities, radius=15.0, max_dz=10.0, min_levels=3, same=1e-3):
|
||||||
|
"""Drop height entities that disagree with the surrounding surveyed surface.
|
||||||
|
|
||||||
|
Drawings often keep plan symbols, hatching or design outlines at a stray
|
||||||
|
constant elevation (e.g. EL 5.624 or -112.719 among 20-40 m contours).
|
||||||
|
Each entity's height is compared with the median of the *distinct* nearby
|
||||||
|
height levels (each level votes once, so a dense stray group of short
|
||||||
|
ticks cannot outvote real contours); the worst height level is removed
|
||||||
|
first and the test is repeated, so a large stray group cannot mask itself.
|
||||||
|
Sparse areas fall back to a 3x search radius.
|
||||||
|
Heights within `same` (1 mm) form one level, absorbing CAD float noise.
|
||||||
|
Returns (keep flags, [(index, z, deviation)]).
|
||||||
|
"""
|
||||||
|
from scipy.spatial import cKDTree
|
||||||
|
n=len(entities)
|
||||||
|
keep=np.ones(n,bool)
|
||||||
|
if n<3:return keep.tolist(),[]
|
||||||
|
xy=np.vstack([e[0] for e in entities]);owner=np.concatenate([np.full(len(e[0]),i) for i,e in enumerate(entities)])
|
||||||
|
zs=np.array([float(e[1]) for e in entities]);sample_z=zs[owner]
|
||||||
|
tree=cKDTree(xy);rejected=[]
|
||||||
|
near=lambda e,r:np.unique(np.concatenate(tree.query_ball_point(e[0],r)) if len(e[0]) else np.array([],int)).astype(int)
|
||||||
|
neighbours=[near(e,radius) for e in entities];wide=[near(e,3*radius) for e in entities]
|
||||||
|
for _ in range(64):
|
||||||
|
alive=keep[owner];dev=np.zeros(n)
|
||||||
|
for i in np.flatnonzero(keep):
|
||||||
|
nb=neighbours[i];nb=nb[alive[nb]&(np.abs(sample_z[nb]-zs[i])>same)]
|
||||||
|
if len(np.unique(np.round(sample_z[nb]/same)))<min_levels:nb=wide[i];nb=nb[alive[nb]&(np.abs(sample_z[nb]-zs[i])>same)]
|
||||||
|
levels=np.unique(np.round(sample_z[nb]/same))*same
|
||||||
|
# Peaks/pits legitimately exceed the median; require the height to
|
||||||
|
# lie clearly outside the whole local range as well.
|
||||||
|
outside=max(levels[0]-zs[i],zs[i]-levels[-1]) if len(levels) else 0.0
|
||||||
|
if len(levels)>=min_levels and outside>max_dz/2:dev[i]=zs[i]-float(np.median(levels))
|
||||||
|
worst=int(np.argmax(np.abs(dev)))
|
||||||
|
if abs(dev[worst])<=max_dz:break
|
||||||
|
group=np.flatnonzero(keep&(np.abs(zs-zs[worst])<=same)&(np.abs(dev)>max_dz))
|
||||||
|
keep[group]=False;rejected.extend((int(i),float(zs[i]),float(dev[i])) for i in group)
|
||||||
|
return keep.tolist(),rejected
|
||||||
|
|
||||||
|
|
||||||
def read_survey_block(source, project, doc=None):
|
def read_survey_block(source, project, doc=None):
|
||||||
"""Read actual height geometry in an explicitly selected survey block."""
|
"""Read actual height geometry in an explicitly selected survey block."""
|
||||||
if doc is None:
|
if doc is None:
|
||||||
@@ -54,9 +93,11 @@ def read_survey_block(source, project, doc=None):
|
|||||||
if e.is_closed: xy.append(xy[0])
|
if e.is_closed: xy.append(xy[0])
|
||||||
contours.append((xy,float(a[0,2]),e.dxf.handle)); types['constant_height_polylines']+=1
|
contours.append((xy,float(a[0,2]),e.dxf.handle)); types['constant_height_polylines']+=1
|
||||||
# POINT entities have priority over densified contour vertices.
|
# POINT entities have priority over densified contour vertices.
|
||||||
for xy,z,handle in contours:
|
sampled=[(np.asarray(_densify(xy,project['contour_step_m']),dtype=float).reshape(-1,2),z) for xy,z,handle in contours]
|
||||||
samples=_densify(xy,project['contour_step_m'])
|
sampled=[(np.array([[x,y]]),z) for x,y,z in points]+sampled
|
||||||
points.extend((x,y,z) for x,y in samples)
|
keep,rejected=reject_height_outliers(sampled,project.get('outlier_radius_m',15.0),project.get('outlier_dz_m',10.0)) if project.get('reject_outliers',True) else ([True]*len(sampled),[])
|
||||||
|
if rejected:types['rejected_height_outliers']=len(rejected)
|
||||||
|
points=[(x,y,z) for (xy,z),ok in zip(sampled,keep) if ok for x,y in xy]
|
||||||
a=np.array(points,dtype=float)
|
a=np.array(points,dtype=float)
|
||||||
if len(a)<3:
|
if len(a)<3:
|
||||||
raise ValueError('선택한 블록에서 실제 표고점·등고선을 3점 이상 읽지 못했습니다.')
|
raise ValueError('선택한 블록에서 실제 표고점·등고선을 3점 이상 읽지 못했습니다.')
|
||||||
|
|||||||
@@ -25,4 +25,23 @@ class SurveyPolylineTests(unittest.TestCase):
|
|||||||
doc=ezdxf.new();doc.modelspace().add_polyline3d([(1,1,20),(2,2,30),(3,3,40)])
|
doc=ezdxf.new();doc.modelspace().add_polyline3d([(1,1,20),(2,2,30),(3,3,40)])
|
||||||
with self.assertRaises(ValueError):read_survey_block(Path('unused.dxf'),self.options(),doc=doc)
|
with self.assertRaises(ValueError):read_survey_block(Path('unused.dxf'),self.options(),doc=doc)
|
||||||
|
|
||||||
|
def contours(self,msp,levels):
|
||||||
|
for k,z in enumerate(levels):
|
||||||
|
msp.add_lwpolyline([(0,10*k),(200,10*k)],dxfattribs={'elevation':z})
|
||||||
|
|
||||||
|
def test_stray_constant_height_symbols_are_rejected(self):
|
||||||
|
# Ungok case: dense short ticks at EL 5.624 and a few at -112.719 among 20-40 m contours.
|
||||||
|
doc=ezdxf.new();msp=doc.modelspace();self.contours(msp,[28,29,30,31,32,33])
|
||||||
|
for i in range(60):msp.add_lwpolyline([(40+2*i,20),(40+2*i,22)],dxfattribs={'elevation':5.624})
|
||||||
|
for i in range(4):msp.add_lwpolyline([(100+i,25),(100+i,27)],dxfattribs={'elevation':-112.719})
|
||||||
|
points,counts,_=read_survey_block(Path('unused.dxf'),self.options(),doc=doc)
|
||||||
|
self.assertEqual(counts['rejected_height_outliers'],64)
|
||||||
|
self.assertGreaterEqual(points[:,2].min(),28)
|
||||||
|
|
||||||
|
def test_real_peak_above_contours_is_kept(self):
|
||||||
|
doc=ezdxf.new();msp=doc.modelspace();self.contours(msp,[70,72,74,76,78,80])
|
||||||
|
msp.add_lwpolyline([(100,25),(100,27)],dxfattribs={'elevation':84})
|
||||||
|
points,counts,_=read_survey_block(Path('unused.dxf'),self.options(),doc=doc)
|
||||||
|
self.assertNotIn('rejected_height_outliers',counts);self.assertEqual(points[:,2].max(),84)
|
||||||
|
|
||||||
if __name__=='__main__':unittest.main()
|
if __name__=='__main__':unittest.main()
|
||||||
Reference in new issue
Block a user