A-SW Hub / Repositories
Local Gitea · READ ONLY
← 저장소 Repositories
REPOSITORY

metafarmers/fruit-work-target-perception Private

[참조 MVP] 과채류 작업대상 인식 — RGB-D 관측에서 과실과 생장점을 식별하고 로봇 작업 후보를 구조화하는 참조 MVP입니다.

https://git.agrithing.ai/metafarmers/fruit-work-target-perception.git
4 commits
COMMIT DETAIL

feat: 과채류 작업대상 인식 구체 MVP 구현

기관 승인 및 자동 품질검사를 통과한 구체 MVP 0.2.0 병합 Co-authored-by: meta-dev <[email protected]>

meta-dev <[email protected]> · 2026. 8. 3. 오후 3:12:31
54a95877b0f88610dd80c81fcba678a0bb239dd5
8 changed files +105 −9 parent bb3f276
modified README.md +7−1 View file
@@ -11,6 +11,12 @@
11 11
12 12 RGB-D 관측에서 과실과 생장점을 식별하고 로봇 작업 후보를 구조화하는 참조 MVP입니다.
13 13
14 + ## MVP 동작 범위
15 +
16 + - 신뢰도 임계값 기반 관측 필터링
17 + - 과실·생장점 작업 우선순위 산정
18 + - 3차원 작업 후보와 제외 건수 생성
19 +
14 20 ## 실행
15 21
16 22 ```bash
@@ -18,4 +24,4 @@ python run.py examples/input.json
18 24 python -m unittest discover -s tests -v
19 25 ```
20 26
21 실제 기관 구현은 `contracts/component.interface.json`의 저장소 식별자와 제품 기준선 연결 규칙을 유지하면서 교체할 수 있습니다.
27 + 정상 입력은 `examples/input.json`, 차단·안전 경로는 `examples/failure-input.json`으로 재현합니다. 실제 기관 구현은 `contracts/component.interface.json`의 저장소 식별자와 제품 기준선 연결 규칙을 유지하면서 교체할 수 있습니다.
modified asw_component/component.py +27−3 View file
@@ -1,7 +1,31 @@
1 1 from __future__ import annotations
2 2
3 + SUPPORTED = {"fruit": 1, "growth-tip": 2}
4 +
3 5
4 6 def execute(payload: dict) -> dict:
5 if not isinstance(payload, dict) or not payload:
6 raise ValueError("a non-empty object is required")
7 return {"component": "metafarmers/fruit-work-target-perception", "accepted": True, "status": "READY", "target": "vpu", "input_fields": sorted(payload.keys())}
7 + if not isinstance(payload, dict):
8 + raise ValueError("payload must be an object")
9 + threshold = float(payload.get("threshold", 0.8))
10 + if not 0 <= threshold <= 1:
11 + raise ValueError("threshold must be between 0 and 1")
12 + candidates = []
13 + rejected = 0
14 + for observation in payload.get("observations", []):
15 + kind = observation.get("kind")
16 + confidence = float(observation.get("confidence", 0))
17 + position = observation.get("positionMm")
18 + if kind not in SUPPORTED or confidence < threshold or not isinstance(position, list) or len(position) != 3:
19 + rejected += 1
20 + continue
21 + candidates.append({
22 + "id": observation["id"], "kind": kind, "confidence": round(confidence, 4),
23 + "positionMm": [float(value) for value in position], "priority": SUPPORTED[kind],
24 + })
25 + candidates.sort(key=lambda item: (-item["priority"], -item["confidence"], item["id"]))
26 + counts = {kind: sum(item["kind"] == kind for item in candidates) for kind in SUPPORTED}
27 + accepted = bool(candidates)
28 + return {"component": "metafarmers/fruit-work-target-perception", "accepted": accepted,
29 + "status": "TARGETS_READY" if accepted else "NO_TARGETS", "target": "vpu",
30 + "frameId": str(payload.get("frameId", "unknown")), "candidates": candidates,
31 + "counts": counts, "rejected": rejected}
modified contracts/component.interface.json +18−1 View file
@@ -5,7 +5,12 @@
5 5 "required": [
6 6 "component",
7 7 "accepted",
8 "status"
8 + "status",
9 + "target",
10 + "frameId",
11 + "candidates",
12 + "counts",
13 + "rejected"
9 14 ],
10 15 "properties": {
11 16 "component": {
@@ -19,6 +24,18 @@
19 24 },
20 25 "target": {
21 26 "const": "vpu"
27 + },
28 + "frameId": {
29 + "type": "string"
30 + },
31 + "candidates": {
32 + "type": "array"
33 + },
34 + "counts": {
35 + "type": "object"
36 + },
37 + "rejected": {
38 + "type": "integer"
22 39 }
23 40 }
24 41 }
added examples/failure-input.json +16−0 View file
@@ -0,0 +1,16 @@
1 + {
2 + "frameId": "greenhouse-row-04",
3 + "observations": [
4 + {
5 + "id": "fruit-19",
6 + "kind": "fruit",
7 + "confidence": 0.51,
8 + "positionMm": [
9 + 300,
10 + 20,
11 + 1200
12 + ]
13 + }
14 + ],
15 + "threshold": 0.8
16 + }
modified examples/input.json +22−2 View file
@@ -4,12 +4,32 @@
4 4 {
5 5 "id": "fruit-17",
6 6 "kind": "fruit",
7 "confidence": 0.94
7 + "confidence": 0.94,
8 + "positionMm": [
9 + 420,
10 + -80,
11 + 1380
12 + ]
8 13 },
9 14 {
10 15 "id": "tip-02",
11 16 "kind": "growth-tip",
12 "confidence": 0.89
17 + "confidence": 0.89,
18 + "positionMm": [
19 + 438,
20 + -65,
21 + 1510
22 + ]
23 + },
24 + {
25 + "id": "leaf-09",
26 + "kind": "leaf",
27 + "confidence": 0.97,
28 + "positionMm": [
29 + 400,
30 + -30,
31 + 1450
32 + ]
13 33 }
14 34 ],
15 35 "threshold": 0.8
modified pyproject.toml +1−1 View file
@@ -1,5 +1,5 @@
1 1 [project]
2 2 name = "fruit-work-target-perception"
3 version = "0.1.0"
3 + version = "0.2.0"
4 4 description = "RGB-D 관측에서 과실과 생장점을 식별하고 로봇 작업 후보를 구조화하는 참조 MVP입니다."
5 5 requires-python = ">=3.11"
modified release/manifest.json +1−1 View file
@@ -4,7 +4,7 @@
4 4 "displayName": "과채류 작업대상 인식",
5 5 "owner": "주식회사 메타파머스",
6 6 "implementationAuthor": "주식회사 비아 (연동 검증용 참조 구현)",
7 "version": "0.1.0-mvp",
7 + "version": "0.2.0-mvp",
8 8 "domain": "Perception",
9 9 "status": "REFERENCE_MVP",
10 10 "compatibility": {
modified tests/test_component.py +13−0 View file
@@ -12,6 +12,19 @@ class ComponentTest(unittest.TestCase):
12 12 result = execute(payload)
13 13 self.assertTrue(result["accepted"])
14 14 self.assertEqual(result["component"], "metafarmers/fruit-work-target-perception")
15 + self.assertEqual(result["target"], "vpu")
16 + self.assertEqual(result["status"], "TARGETS_READY")
17 + self.assertEqual([item["id"] for item in result["candidates"]], ["tip-02", "fruit-17"])
18 + self.assertEqual(result["counts"], {"fruit": 1, "growth-tip": 1})
19 + self.assertEqual(result["rejected"], 1)
20 +
21 + def test_safety_or_failure_path(self):
22 + payload = json.loads((ROOT / "examples/failure-input.json").read_text(encoding="utf-8"))
23 + result = execute(payload)
24 + self.assertEqual(result["component"], "metafarmers/fruit-work-target-perception")
25 + self.assertFalse(result["accepted"])
26 + self.assertEqual(result["status"], "NO_TARGETS")
27 + self.assertEqual(result["candidates"], [])
15 28
16 29 if __name__ == "__main__":
17 30 unittest.main()