From a5de76e0e7053ceecc366e9abf1a3578deb32902 Mon Sep 17 00:00:00 2001 From: Daekeun Date: Tue, 30 Jun 2026 17:09:50 +0900 Subject: [PATCH] dk - edit --- README.md | 32 ++++++++++++++++---- main.py | 88 ++++++++++--------------------------------------------- start.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 78 deletions(-) create mode 100644 start.py diff --git a/README.md b/README.md index fb8e1fa..1b16e48 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Normal Object Detection -YOLO 기반 일반 객체 탐지 스크립트입니다. 원격 이미지 URL을 받아 추론하고, 결과를 JSON으로 출력하며 ClearML에 기록합니다. +YOLO 기반 일반 객체 탐지 스크립트입니다. 원격 이미지 URL을 받아 추론하고, 결과를 JSON으로 출력합니다. ## 요구 사항 @@ -16,7 +16,7 @@ uv sync ## 사용법 ```bash -uv run main.py --image_url "https://example.com/image.jpg" +uv run start.py --image_url "https://example.com/image.jpg" ``` - `--image_url`: 추론할 이미지 URL (필수) @@ -36,12 +36,34 @@ uv run main.py --image_url "https://example.com/image.jpg" | `detections` | 탐지 객체 목록 (클래스, 신뢰도, `xywh` 박스) | | `image_url` | 이미지 경로 | -## ClearML +--- -스크립트 실행 시 ClearML Task가 초기화되고, 추론 완료 후 결과가 아티팩트로 업로드됩니다. +## ClearML 자동 연동 (시스템 생성) + +> 아래 내용은 커스텀 UI·ClearML Agent 연동 시 **시스템이 자동으로 처리**하는 부분입니다. +> 개발자는 `start.py`만 유지하면 되며, `main.py`는 UI 입력에 따라 자동 생성됩니다. + +### 파일 역할 + +| 파일 | 역할 | 관리 | +|------|------|------| +| `start.py` | 추론·결과 처리 (`main(image_url)`) | 개발자가 Git에 커밋 | +| `main.py` | ClearML Task 초기화, input/output 연동 | 시스템이 자동 생성 | + +### Agent 실행 + +```bash +uv run main.py --image_url "https://example.com/image.jpg" +``` + +실행 시 ClearML Task가 초기화되고, `start.main()` 결과가 아티팩트로 업로드됩니다. - **Project**: `Normal_Object_Detection` - **Task**: `model-yolo26-human` - **Artifact**: `final_result` (`output`, `status: PASS`) -ClearML 서버 연결 설정이 필요합니다 (`clearml-init`). +Agent 머신에는 ClearML 서버 연결 설정이 필요합니다 (`clearml-init`). + +### 워크플로우 + +Git 업로드, UI 입력, `main.py` 생성, ClearML Task/Agent 연동까지의 순차 절차는 [WORKFLOW.md](./WORKFLOW.md)를 참고하세요. diff --git a/main.py b/main.py index 302a20f..09aedd0 100644 --- a/main.py +++ b/main.py @@ -1,86 +1,28 @@ -import json -import math -from pathlib import Path -from ultralytics import YOLO -import requests +# 시스템 생성 — 이 파일은 커스텀 UI 입력에 따라 자동 생성됩니다. 직접 수정하지 마세요. + import argparse -from clearml import Task # 1. ClearML 임포트 +from clearml import Task -# 방법 A) 가장 최신 트렌드인 NMS-Free 임베디드 특화 모델 로드 -model = YOLO("./weights/yolo26m.pt") # Medium 크기 가중치 자동 다운로드 -# 방법 B) 복잡한 구도나 정밀 식별에 강한 어텐션 기반 모델 로드 -# model = YOLO("./weights/yolo12m.pt") +from start import main - -def xyxy_to_xywh(box: dict) -> dict: - x1 = math.floor(box["x1"]) - y1 = math.floor(box["y1"]) - x2 = math.ceil(box["x2"]) - y2 = math.ceil(box["y2"]) - return {"x": x1, "y": y1, "w": x2 - x1, "h": y2 - y1} - - -def main(image_path: str): - # uv run main.py --image_url "https://acai.ketidev.kr:20443/detect/image/202606/20260619_145116_image.jpg" - # 이미지 원격 추론 테스트 (COCO 80종 기본 탐지 가능) - # results = model("https://acai.ketidev.kr:20443/detect/image/202606/20260619_145116_image.jpg") - results = model(image_path) - - result = results[0] - - image_path = Path(result.path) - marked_path = image_path.with_name(f"{image_path.stem}_marked{image_path.suffix}") - result.save(filename=str(marked_path)) - - detections = [] - for det in result.summary(): - detections.append({**det, "box": xyxy_to_xywh(det["box"])}) - - output = { - "path": result.path, - "marked_path": str(marked_path), - "shape": {"height": result.orig_shape[0], "width": result.orig_shape[1]}, - "speed_ms": result.speed, - "detections": detections, - "image_url": image_path, - } - print(json.dumps(output, indent=2, ensure_ascii=False)) - - return output - - - - - - -task = Task.init( - project_name="Normal_Object_Detection", - task_name="model-yolo26-human" -) -def init_acai_task(output): +def init_acai_task(task: Task, output: dict) -> None: + """UI output 정의에 맞춰 ClearML 아티팩트를 업로드한다.""" result_data = {"output": output, "status": "PASS"} task.upload_artifact(name="final_result", artifact_object=result_data) + if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--image_url", type=str) + parser.add_argument("--image_url", type=str, required=True) args = parser.parse_args() - image_url = args.image_url + task = Task.init( + project_name="Normal_Object_Detection", + task_name="model-yolo26-human", + ) + task.connect(parser) - if image_url is None: - print("Image path is required") - exit(1) - - response = requests.get(image_url) - response.raise_for_status() - image_data = response.content - with open("image.jpg", "wb") as f: - f.write(image_data) - image_path = "image.jpg" - - output = main(image_path) - - init_acai_task(output) \ No newline at end of file + output = main(args.image_url) + init_acai_task(task, output) diff --git a/start.py b/start.py new file mode 100644 index 0000000..06e15e9 --- /dev/null +++ b/start.py @@ -0,0 +1,75 @@ +import argparse +import json +import math +from pathlib import Path + +import requests +from ultralytics import YOLO + +# 방법 A) 가장 최신 트렌드인 NMS-Free 임베디드 특화 모델 로드 +model = YOLO("./weights/yolo26m.pt") +# 방법 B) 복잡한 구도나 정밀 식별에 강한 어텐션 기반 모델 로드 +# model = YOLO("./weights/yolo12m.pt") + + +def xyxy_to_xywh(box: dict) -> dict: + x1 = math.floor(box["x1"]) + y1 = math.floor(box["y1"]) + x2 = math.ceil(box["x2"]) + y2 = math.ceil(box["y2"]) + return {"x": x1, "y": y1, "w": x2 - x1, "h": y2 - y1} + + +def run(image_path: str) -> dict: + """이미지 경로를 받아 추론하고 결과 dict를 반환한다.""" + results = model(image_path) + result = results[0] + + image_path_obj = Path(result.path) + marked_path = image_path_obj.with_name( + f"{image_path_obj.stem}_marked{image_path_obj.suffix}" + ) + result.save(filename=str(marked_path)) + + detections = [] + for det in result.summary(): + detections.append({**det, "box": xyxy_to_xywh(det["box"])}) + + output = { + "path": result.path, + "marked_path": str(marked_path), + "shape": {"height": result.orig_shape[0], "width": result.orig_shape[1]}, + "speed_ms": result.speed, + "detections": detections, + "image_url": str(image_path_obj), + } + return output + + +def download_image(image_url: str, save_path: str = "image.jpg") -> str: + """원격 이미지 URL을 로컬 파일로 저장하고 경로를 반환한다.""" + # 이미지 URL 체크 + if not image_url.startswith("http"): + raise ValueError("Invalid image URL") + response = requests.get(image_url) + response.raise_for_status() + + with open(save_path, "wb") as f: + f.write(response.content) + return save_path + + +def main(image_url: str) -> dict: + image_path = download_image(image_url) + return run(image_path) + +if __name__ == "__main__": + # 추론 결과를 출력하기 위한 테스트 코드 + # uv run start.py --image_url "https://acai.ketidev.kr:20443/detect/image/202606/20260619_145116_image.jpg" + parser = argparse.ArgumentParser() + parser.add_argument("--image_url", type=str, required=True) + args = parser.parse_args() + + output = main(args.image_url) + + print(json.dumps(output, indent=2, ensure_ascii=False))