@
feat: SAM3.1 다중 프롬프트 배치 검출 및 라벨 병합 후처리
sam3_multi_prompt.py: 프롬프트 N개를 forward 1회에 배치 처리한다.
서버(프롬프트당 forward 1회) 대비 forward 횟수가 1/N로 줄고, 이미지
임베딩과 텍스트 인코딩을 타일 전체에서 재사용한다. RTX 3060 12GB
기준 배치 16이 최적(32 이상은 VRAM 압박으로 4배 이상 느려짐).
타일보다 큰 대상(숲, 도로, 주차장)은 타일 경계에서 잘려 사각형
마스크가 되므로 --wide-prompts 로 지정해 이미지 전체를 한 장으로
처리한다.
merge_labels.py: 검출 결과 후처리. 같은 병합 그룹에 속한 라벨끼리
외곽선이 gap px 이내로 인접하면 하나로 합치고 대표 라벨을 붙인다
(building + building rooftop + blue roof -> building). 병합 전
claim 규칙으로 소유권을 재배정해, 차량 위에 잡힌 *roof 폴리곤이
건물이 아니라 차량에 합쳐지도록 한다.
make_viewer.py: 라벨별 on/off 가능한 단독 HTML 뷰어 생성.
프롬프트 파일의 그룹 주석 기준으로 라벨을 묶어 나열하고, 줌/팬과
선택 상태 저장을 지원한다.
cut_tiles.py: R{행}C{열} 격자 오버레이 + 원본 배율 타일 저장.
전역 규칙 반영:
- CUDA 사용 불가 시 CPU로 폴백하지 않고 즉시 에러
- set_per_process_memory_fraction 으로 드라이버의 시스템 메모리
폴백(10배 이상 느려짐) 전에 OOM 발생
- 체크포인트/BPE 경로 하드코딩 제거. CLI 인자 -> 환경변수 ->
sam31server 설정파일 순으로 해결
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
@@ -39,26 +39,34 @@ def encode_image(image_bgr: np.ndarray, max_size: int = 1280) -> tuple:
|
||||
return base64.b64encode(buf).decode("utf-8"), scale
|
||||
|
||||
|
||||
# 탐색용 넓은 프롬프트 — 철도 현장에서 흔히 보이는 모든 요소 포함
|
||||
# 탐색용 프롬프트 — 색상·객체 조합 67개
|
||||
DISCOVERY_PROMPT = (
|
||||
"railroad track, railway rail, "
|
||||
"catenary pole, overhead line pole, electric pole, "
|
||||
"overhead wire, catenary wire, power line cable, "
|
||||
"railway sleeper, concrete tie, "
|
||||
"guardrail, highway barrier, road fence, "
|
||||
"bridge, viaduct, overpass, "
|
||||
"vegetation, tree, bush, grass, "
|
||||
"building, structure, roof, wall, "
|
||||
"vehicle, car, truck, "
|
||||
"road, asphalt, pavement, "
|
||||
"slope, embankment, retaining wall, "
|
||||
"noise barrier, sound wall, "
|
||||
"signal, sign board, "
|
||||
"small dark object on ballast, small dark object on railway, "
|
||||
"small square metal box on ground, control box on ballast, "
|
||||
"gray square lid on gravel, flat metal cover on ground, "
|
||||
"small bright object on ballast, small white box on ballast, "
|
||||
"small gray box on ground, bright square object on gravel"
|
||||
# 일반 카테고리
|
||||
"building, building rooftop, building facade, residential house, "
|
||||
"warehouse, storage container, factory structure, "
|
||||
"asphalt road, concrete pavement, parking lot, pedestrian sidewalk, "
|
||||
"road lane marking, crosswalk, "
|
||||
"bare ground, dirt field, grass lawn, "
|
||||
"tree canopy, dense forest, green hedge, cast shadow, "
|
||||
# 지붕·구조물 (색상별)
|
||||
"blue roof, red roof, green roof, gray roof, dark gray roof, black roof, "
|
||||
"white roof, silver metal roof, orange roof, brown roof, yellow roof, "
|
||||
# 차량 (색상별)
|
||||
"white vehicle, black vehicle, silver vehicle, gray vehicle, red vehicle, "
|
||||
"blue vehicle, yellow vehicle, orange vehicle, green vehicle, "
|
||||
"white cargo truck, blue cargo truck, yellow school bus, commercial bus, "
|
||||
# 산업 자재·적치물 (색상별)
|
||||
"blue plastic drum, orange plastic barrel, yellow plastic container, "
|
||||
"white industrial tank, metallic storage tank, "
|
||||
"blue waterproof tarp, green waterproof tarp, black protective sheet, "
|
||||
"white canvas canopy, gray metal fence, green wire fence, "
|
||||
"red roadside barrier, yellow safety cone, orange traffic cone, "
|
||||
# 노면 표시
|
||||
"yellow center dividing line, white solid lane marking, "
|
||||
"white dashed lane marking, white directional arrow marking, "
|
||||
"yellow parking stall line, white parking stall line, "
|
||||
"blue handicap parking space, pink pedestrian safety marking, "
|
||||
"green bike lane marking"
|
||||
)
|
||||
|
||||
|
||||
@@ -178,6 +186,84 @@ def detect_everything_tiled(image_bgr, cols, rows, overlap, conf, workers, promp
|
||||
return all_shapes
|
||||
|
||||
|
||||
# ── 인접 폴리곤 병합 ──────────────────────────────────────────────────────────
|
||||
def _polys_touch(pa, pb, gap):
|
||||
"""두 폴리곤이 gap px 이내로 닿는지 (겹침 포함) 국소 캔버스에서 판정."""
|
||||
xs = [p[0] for p in pa] + [p[0] for p in pb]
|
||||
ys = [p[1] for p in pa] + [p[1] for p in pb]
|
||||
x0, y0 = int(min(xs)) - gap - 1, int(min(ys)) - gap - 1
|
||||
x1, y1 = int(max(xs)) + gap + 1, int(max(ys)) + gap + 1
|
||||
if (x1 - x0) * (y1 - y0) > 40_000_000:
|
||||
return False
|
||||
ca = np.zeros((y1 - y0, x1 - x0), np.uint8)
|
||||
cb = np.zeros_like(ca)
|
||||
cv2.fillPoly(ca, [np.array(pa, np.int32) - (x0, y0)], 255)
|
||||
cv2.fillPoly(cb, [np.array(pb, np.int32) - (x0, y0)], 255)
|
||||
if gap > 0:
|
||||
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (gap * 2 + 1, gap * 2 + 1))
|
||||
ca = cv2.dilate(ca, k)
|
||||
return bool(np.any(cv2.bitwise_and(ca, cb)))
|
||||
|
||||
|
||||
def merge_adjacent(shapes, gap=8, epsilon=2.0):
|
||||
"""같은 라벨끼리 gap px 이내로 인접·겹치는 폴리곤을 하나로 병합."""
|
||||
if not shapes:
|
||||
return []
|
||||
|
||||
parent = list(range(len(shapes)))
|
||||
|
||||
def find(i):
|
||||
while parent[i] != i:
|
||||
parent[i] = parent[parent[i]]
|
||||
i = parent[i]
|
||||
return i
|
||||
|
||||
boxes = [_bbox(s["points"]) for s in shapes]
|
||||
for i in range(len(shapes)):
|
||||
for j in range(i + 1, len(shapes)):
|
||||
if shapes[i].get("label") != shapes[j].get("label"):
|
||||
continue
|
||||
if find(i) == find(j):
|
||||
continue
|
||||
ax0, ay0, ax1, ay1 = boxes[i]
|
||||
bx0, by0, bx1, by1 = boxes[j]
|
||||
if ax1 + gap < bx0 or bx1 + gap < ax0 or ay1 + gap < by0 or by1 + gap < ay0:
|
||||
continue # bbox조차 안 닿음
|
||||
if _polys_touch(shapes[i]["points"], shapes[j]["points"], gap):
|
||||
parent[find(j)] = find(i)
|
||||
|
||||
groups = {}
|
||||
for i in range(len(shapes)):
|
||||
groups.setdefault(find(i), []).append(i)
|
||||
|
||||
merged = []
|
||||
for members in groups.values():
|
||||
if len(members) == 1:
|
||||
merged.append(shapes[members[0]])
|
||||
continue
|
||||
pts_all = [p for i in members for p in shapes[i]["points"]]
|
||||
x0 = int(min(p[0] for p in pts_all)) - gap - 1
|
||||
y0 = int(min(p[1] for p in pts_all)) - gap - 1
|
||||
x1 = int(max(p[0] for p in pts_all)) + gap + 1
|
||||
y1 = int(max(p[1] for p in pts_all)) + gap + 1
|
||||
canvas = np.zeros((y1 - y0, x1 - x0), np.uint8)
|
||||
for i in members:
|
||||
cv2.fillPoly(canvas, [np.array(shapes[i]["points"], np.int32) - (x0, y0)], 255)
|
||||
if gap > 0:
|
||||
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (gap * 2 + 1, gap * 2 + 1))
|
||||
canvas = cv2.morphologyEx(canvas, cv2.MORPH_CLOSE, k)
|
||||
contours, _ = cv2.findContours(canvas, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
base = shapes[max(members, key=lambda i: float(shapes[i].get("score", 0)))]
|
||||
for cnt in contours:
|
||||
approx = cv2.approxPolyDP(cnt, epsilon, True)
|
||||
if len(approx) < 3:
|
||||
continue
|
||||
merged.append({**base,
|
||||
"points": [[float(p[0][0] + x0), float(p[0][1] + y0)]
|
||||
for p in approx]})
|
||||
return merged
|
||||
|
||||
|
||||
# ── 시각화 ────────────────────────────────────────────────────────────────────
|
||||
def draw_everything(image_bgr, shapes, cols, rows):
|
||||
vis = image_bgr.copy()
|
||||
@@ -190,6 +276,10 @@ def draw_everything(image_bgr, shapes, cols, rows):
|
||||
bx1, by1 = int((c + 1) * W / cols), int((r + 1) * H / rows)
|
||||
cv2.rectangle(vis, (bx0, by0), (bx1, by1), (60, 60, 60), 1)
|
||||
|
||||
# 라벨 글자 크기는 이미지 크기 비례 (출력이 4096으로 축소되는 것 감안)
|
||||
font_scale = max(0.8, min(W, H) / 2200)
|
||||
font_thick = max(2, int(font_scale * 1.5))
|
||||
|
||||
rng = np.random.default_rng(42)
|
||||
for s in shapes:
|
||||
pts = np.array(s["points"], dtype=np.int32)
|
||||
@@ -197,15 +287,26 @@ def draw_everything(image_bgr, shapes, cols, rows):
|
||||
overlay = vis.copy()
|
||||
cv2.fillPoly(overlay, [pts], color)
|
||||
cv2.addWeighted(overlay, 0.30, vis, 0.70, 0, vis)
|
||||
cv2.polylines(vis, [pts], True, color, 1)
|
||||
|
||||
# 외곽선: 선을 그린 뒤 마스크 내부만 남겨 안쪽 1px로 만듦
|
||||
region = np.zeros((H, W), dtype=np.uint8)
|
||||
cv2.fillPoly(region, [pts], 255)
|
||||
edge = np.zeros((H, W), dtype=np.uint8)
|
||||
cv2.polylines(edge, [pts], True, 255, 2, cv2.LINE_8)
|
||||
vis[cv2.bitwise_and(edge, region) > 0] = [int(c * 0.35) for c in color]
|
||||
|
||||
# 라벨 표시 (있을 경우)
|
||||
label = s.get("label", "")
|
||||
if label:
|
||||
cx = int(np.mean([p[0] for p in s["points"]]))
|
||||
cy = int(np.mean([p[1] for p in s["points"]]))
|
||||
cv2.putText(vis, label, (cx, cy),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1, cv2.LINE_AA)
|
||||
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX,
|
||||
font_scale, font_thick)
|
||||
cv2.rectangle(vis, (cx - tw // 2 - 4, cy - th - 4),
|
||||
(cx + tw // 2 + 4, cy + 6), (0, 0, 0), -1)
|
||||
cv2.putText(vis, label, (cx - tw // 2, cy),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, font_scale, color,
|
||||
font_thick, cv2.LINE_AA)
|
||||
|
||||
cv2.putText(vis, f"total segments: {len(shapes)}",
|
||||
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2)
|
||||
@@ -246,6 +347,8 @@ def main():
|
||||
ap.add_argument("--workers", type=int, default=4, help="병렬 스레드 수 (기본 4)")
|
||||
ap.add_argument("--nms", type=float, default=0.40, help="NMS IoU 임계값 (기본 0.40)")
|
||||
ap.add_argument("--prompt-extra", default="", help="DISCOVERY_PROMPT 뒤에 추가할 어휘 (콤마 구분)")
|
||||
ap.add_argument("--merge", action="store_true", help="같은 라벨끼리 인접 폴리곤 병합")
|
||||
ap.add_argument("--merge-gap", type=int, default=8, help="병합 판정 간격 px (기본 8)")
|
||||
ap.add_argument("--zone", type=int, nargs=4, metavar=("X1","Y1","X2","Y2"), default=None,
|
||||
help="처리 zone 제한 (이 범위와 겹치는 타일만 처리)")
|
||||
args = ap.parse_args()
|
||||
@@ -280,7 +383,13 @@ def main():
|
||||
)
|
||||
print(f"검출 {len(shapes)}개 → NMS(iou={args.nms})...")
|
||||
shapes = nms_shapes(shapes, iou_thresh=args.nms)
|
||||
print(f"NMS 후 {len(shapes)}개 ({time.time()-t0:.0f}초)\n")
|
||||
print(f"NMS 후 {len(shapes)}개 ({time.time()-t0:.0f}초)")
|
||||
|
||||
if args.merge:
|
||||
before = len(shapes)
|
||||
shapes = merge_adjacent(shapes, gap=args.merge_gap)
|
||||
print(f"병합(gap={args.merge_gap}px) {before} → {len(shapes)}개")
|
||||
print()
|
||||
|
||||
analyze_labels(shapes)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user