@
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:
@@ -8,16 +8,17 @@ import numpy as np
|
||||
import torch
|
||||
|
||||
# Add server to path so we can import sam3 locally
|
||||
server_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "X-AnyLabeling-Server"))
|
||||
models_path = os.path.join(server_path, "app", "models")
|
||||
server_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sam31server"))
|
||||
if server_path not in sys.path:
|
||||
sys.path.insert(0, server_path)
|
||||
if models_path not in sys.path:
|
||||
sys.path.insert(0, models_path)
|
||||
|
||||
|
||||
SAM3_CHECKPOINT = (
|
||||
"C:/Users/nbright/.cache/huggingface/hub/models--facebook--sam3.1/"
|
||||
"snapshots/daa63191845a41281374e725f4c9e51c7a824460/sam3.1_multiplex.pt"
|
||||
)
|
||||
|
||||
from sam3.model_builder import build_sam3_image_model
|
||||
from sam3.model.sam3_image_processor import Sam3Processor
|
||||
from app.models.segment_anything_3 import SegmentAnything3
|
||||
|
||||
def build_point_grid(n_per_side: int) -> np.ndarray:
|
||||
"""Generates a 2D grid of points evenly spaced in [0, 1] x [0, 1]."""
|
||||
@@ -142,6 +143,10 @@ def main():
|
||||
parser.add_argument("--input", required=True, help="Input image path")
|
||||
parser.add_argument("--output", required=True, help="Output vis image path")
|
||||
parser.add_argument("--points", type=int, default=32, help="Points per side")
|
||||
parser.add_argument("--conf", type=float, default=0.7, help="Confidence threshold")
|
||||
parser.add_argument("--nms", type=float, default=0.7, help="NMS IoU threshold")
|
||||
parser.add_argument("--split", action="store_true",
|
||||
help="겹치지 않는 마스크끼리 묶어 여러 장으로 저장")
|
||||
args = parser.parse_args()
|
||||
|
||||
buf = np.fromfile(args.input, dtype=np.uint8)
|
||||
@@ -159,27 +164,65 @@ def main():
|
||||
else:
|
||||
image_proc = image.copy()
|
||||
|
||||
model_path = os.path.join(server_path, "sam3.pt")
|
||||
model_path = SAM3_CHECKPOINT
|
||||
|
||||
results = segment_everything(image_proc, model_path, points_per_side=args.points, conf_thresh=0.7, nms_thresh=0.7)
|
||||
results = segment_everything(image_proc, model_path, points_per_side=args.points,
|
||||
conf_thresh=args.conf, nms_thresh=args.nms)
|
||||
|
||||
vis = image_proc.copy()
|
||||
np.random.seed(42)
|
||||
colors = [np.random.randint(0, 255, (3,)).tolist() for _ in results]
|
||||
H, W = image_proc.shape[:2]
|
||||
regions = []
|
||||
for res in results:
|
||||
poly = res["polygon"]
|
||||
pts = np.array(poly, dtype=np.int32)
|
||||
color = np.random.randint(0, 255, (3,)).tolist()
|
||||
|
||||
overlay = vis.copy()
|
||||
cv2.fillPoly(overlay, [pts], color)
|
||||
cv2.addWeighted(overlay, 0.4, vis, 0.6, 0, vis)
|
||||
cv2.polylines(vis, [pts], True, color, 1)
|
||||
|
||||
# Fix unicode paths in output
|
||||
is_success, im_buf_arr = cv2.imencode(".jpg", vis)
|
||||
if is_success:
|
||||
im_buf_arr.tofile(args.output)
|
||||
print(f"Saved visualization to {args.output}")
|
||||
region = np.zeros((H, W), dtype=np.uint8)
|
||||
cv2.fillPoly(region, [np.array(res["polygon"], dtype=np.int32)], 255)
|
||||
regions.append(region)
|
||||
|
||||
if args.split:
|
||||
# 겹치지 않는 것끼리 묶어 레이어 분리 (점수 높은 순 greedy first-fit)
|
||||
layers = [] # [(누적마스크, [인덱스...])]
|
||||
for i, region in enumerate(regions):
|
||||
for canvas, members in layers:
|
||||
if not np.any(cv2.bitwise_and(canvas, region)):
|
||||
cv2.bitwise_or(canvas, region, canvas)
|
||||
members.append(i)
|
||||
break
|
||||
else:
|
||||
layers.append((region.copy(), [i]))
|
||||
print(f"Split into {len(layers)} non-overlapping layers.")
|
||||
groups = [members for _, members in layers]
|
||||
else:
|
||||
groups = [list(range(len(results)))]
|
||||
|
||||
out_path = Path(args.output)
|
||||
for n, members in enumerate(groups, 1):
|
||||
vis = image_proc.copy()
|
||||
for i in members:
|
||||
pts = np.array(results[i]["polygon"], dtype=np.int32)
|
||||
color = colors[i]
|
||||
|
||||
overlay = vis.copy()
|
||||
cv2.fillPoly(overlay, [pts], color)
|
||||
cv2.addWeighted(overlay, 0.4, vis, 0.6, 0, vis)
|
||||
|
||||
# 테두리: 선을 그린 뒤 마스크 내부만 남겨 안쪽 1px로 만듦
|
||||
edge = np.zeros((H, W), dtype=np.uint8)
|
||||
cv2.polylines(edge, [pts], True, 255, 2, cv2.LINE_8)
|
||||
vis[cv2.bitwise_and(edge, regions[i]) > 0] = [int(c * 0.35) for c in color]
|
||||
|
||||
path = (out_path if len(groups) == 1
|
||||
else out_path.with_name(f"{out_path.stem}_L{n}{out_path.suffix}"))
|
||||
is_success, im_buf_arr = cv2.imencode(out_path.suffix, vis)
|
||||
if is_success:
|
||||
im_buf_arr.tofile(str(path))
|
||||
print(f"Saved {len(members)} masks to {path}")
|
||||
|
||||
try: # 완료 알림음
|
||||
import winsound
|
||||
winsound.Beep(880, 150)
|
||||
winsound.Beep(1175, 250)
|
||||
except Exception:
|
||||
print("\a", end="")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user