robomation 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robomation/__init__.py +77 -0
- robomation/ai/__init__.py +42 -0
- robomation/ai/_internal/__init__.py +16 -0
- robomation/ai/_internal/_camera.py +335 -0
- robomation/ai/_internal/_display.py +85 -0
- robomation/ai/_internal/_model.py +84 -0
- robomation/ai/aruco_marker.py +189 -0
- robomation/ai/aruco_marker_roboid.py +398 -0
- robomation/ai/asr.py +121 -0
- robomation/ai/asr_roboid.py +319 -0
- robomation/ai/body_detection.py +291 -0
- robomation/ai/body_detection_roboid.py +450 -0
- robomation/ai/color_detection.py +197 -0
- robomation/ai/color_detection_roboid.py +467 -0
- robomation/ai/detailed_face_detection.py +268 -0
- robomation/ai/detailed_face_detection_roboid.py +508 -0
- robomation/ai/face_detection.py +231 -0
- robomation/ai/face_detection_roboid.py +458 -0
- robomation/ai/face_expression.py +206 -0
- robomation/ai/face_expression_roboid.py +500 -0
- robomation/ai/hand_detection.py +269 -0
- robomation/ai/hand_detection_roboid.py +495 -0
- robomation/ai/object_detection.py +244 -0
- robomation/ai/object_detection_roboid.py +483 -0
- robomation/ai/self_driving.py +239 -0
- robomation/ai/self_driving_roboid.py +416 -0
- robomation/cheesestick_ext/__init__.py +18 -0
- robomation/cheesestick_ext/csd01.py +68 -0
- robomation/cheesestick_ext/csd02.py +96 -0
- robomation/cheesestick_ext/csd03.py +77 -0
- robomation/cheesestick_ext/csd07.py +63 -0
- robomation/cheesestick_ext/csd09.py +178 -0
- robomation/cheesestick_ext/csd10.py +64 -0
- robomation/cheesestick_ext/hat010.py +193 -0
- robomation/cheesestick_ext/hat022.py +91 -0
- robomation/cheesestick_ext/neopixel.py +219 -0
- robomation/cheesestick_ext/pid10.py +48 -0
- robomation/cheesestick_ext/pid13.py +67 -0
- robomation/cheesestick_ext/pid26.py +51 -0
- robomation/core/__init__.py +18 -0
- robomation/core/_internal/__init__.py +16 -0
- robomation/core/_internal/_log.py +71 -0
- robomation/core/_internal/_scope.py +271 -0
- robomation/core/_internal/_sound.py +142 -0
- robomation/core/_internal/_tts.py +118 -0
- robomation/core/error.py +28 -0
- robomation/core/keyboard.py +100 -0
- robomation/core/model.py +559 -0
- robomation/core/permission.py +48 -0
- robomation/core/runner.py +337 -0
- robomation/core/scanner.py +36 -0
- robomation/core/serial_connector.py +355 -0
- robomation/core/utils.py +135 -0
- robomation/py.typed +0 -0
- robomation/roboids/__init__.py +36 -0
- robomation/roboids/beagle.py +436 -0
- robomation/roboids/beagle_roboid.py +714 -0
- robomation/roboids/cheesestick.py +557 -0
- robomation/roboids/cheesestick_roboid.py +1392 -0
- robomation/roboids/hamster.py +518 -0
- robomation/roboids/hamster_roboid.py +370 -0
- robomation/roboids/hamster_s.py +809 -0
- robomation/roboids/hamster_s_roboid.py +497 -0
- robomation/roboids/pio.py +546 -0
- robomation/roboids/pio_roboid.py +536 -0
- robomation/roboids/raccoonbot.py +763 -0
- robomation/roboids/raccoonbot_roboid.py +609 -0
- robomation/roboids/turtle.py +624 -0
- robomation/roboids/turtle_roboid.py +467 -0
- robomation-0.1.0.dist-info/METADATA +179 -0
- robomation-0.1.0.dist-info/RECORD +76 -0
- robomation-0.1.0.dist-info/WHEEL +5 -0
- robomation-0.1.0.dist-info/licenses/LICENSE +501 -0
- robomation-0.1.0.dist-info/scm_file_list.json +79 -0
- robomation-0.1.0.dist-info/scm_version.json +8 -0
- robomation-0.1.0.dist-info/top_level.txt +1 -0
robomation/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Based on the ROBOID project - http://hamster.school
|
|
2
|
+
# Copyright (c) 2016 Kwang-Hyun Park (akaii@kw.ac.kr)
|
|
3
|
+
#
|
|
4
|
+
# Modified by Robomation in 2026.
|
|
5
|
+
# Copyright (c) 2026 Robomation
|
|
6
|
+
#
|
|
7
|
+
# This library is free software; you can redistribute it and/or
|
|
8
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
9
|
+
# License as published by the Free Software Foundation; either
|
|
10
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This library is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
15
|
+
# Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General
|
|
18
|
+
# Public License along with this library; if not, write to the
|
|
19
|
+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
|
20
|
+
# Boston, MA 02111-1307 USA
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
import signal
|
|
24
|
+
|
|
25
|
+
from robomation.core.scanner import Scanner
|
|
26
|
+
# from robomation.core.keyboard import Keyboard
|
|
27
|
+
from robomation.core.utils import Utils
|
|
28
|
+
from robomation.core.runner import Runner
|
|
29
|
+
# from robomation.core.model import DeviceType, DataType
|
|
30
|
+
from robomation.roboids import HamsterS, Hamster, Pio, Turtle, Beagle, RaccoonBot, CheeseStick
|
|
31
|
+
from robomation.ai import ASR, FaceDetection, DetailedFaceDetection, FaceExpression, HandDetection, BodyDetection, ObjectDetection, ColorDetection, ArucoMarker, SelfDriving
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
# __author__ =
|
|
35
|
+
# __email__ =
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Utils",
|
|
39
|
+
"HamsterS", "Hamster", "Pio", "Turtle", "Beagle", "RaccoonBot", "CheeseStick",
|
|
40
|
+
"ASR", "FaceDetection", "DetailedFaceDetection", "FaceExpression", "HandDetection", "BodyDetection", "ObjectDetection", "ColorDetection", "ArucoMarker", "SelfDriving",
|
|
41
|
+
# "scan", "dispose", "set_executable", "when_do", "while_do",
|
|
42
|
+
# "wait", "wait_until_ready", "wait_until", "parallel",
|
|
43
|
+
# "DeviceType", "DataType",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def _handle_signal(signal, frame):
|
|
47
|
+
Runner.shutdown()
|
|
48
|
+
raise SystemExit
|
|
49
|
+
|
|
50
|
+
signal.signal(signal.SIGINT, _handle_signal)
|
|
51
|
+
|
|
52
|
+
# def scan():
|
|
53
|
+
# Scanner.scan()
|
|
54
|
+
|
|
55
|
+
# def dispose():
|
|
56
|
+
# Runner.dispose_all()
|
|
57
|
+
|
|
58
|
+
# def set_executable(execute):
|
|
59
|
+
# Runner.set_executable(execute)
|
|
60
|
+
|
|
61
|
+
# def wait(milliseconds):
|
|
62
|
+
# Runner.wait(milliseconds)
|
|
63
|
+
|
|
64
|
+
# def wait_until_ready():
|
|
65
|
+
# Runner.wait_until_ready()
|
|
66
|
+
|
|
67
|
+
# def wait_until(condition, args=None):
|
|
68
|
+
# Runner.wait_until(condition, args)
|
|
69
|
+
|
|
70
|
+
# def when_do(condition, do, args=None):
|
|
71
|
+
# Runner.when_do(condition, do, args)
|
|
72
|
+
|
|
73
|
+
# def while_do(condition, do, args=None):
|
|
74
|
+
# Runner.while_do(condition, do, args)
|
|
75
|
+
|
|
76
|
+
# def parallel(*functions):
|
|
77
|
+
# Runner.parallel(functions)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Copyright (c) 2026 Robomation
|
|
2
|
+
#
|
|
3
|
+
# This library is free software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
5
|
+
# License as published by the Free Software Foundation; either
|
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
11
|
+
# Lesser General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU Lesser General
|
|
14
|
+
# Public License along with this library; if not, write to the
|
|
15
|
+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
|
16
|
+
# Boston, MA 02111-1307 USA
|
|
17
|
+
|
|
18
|
+
# robomation ai ext internals
|
|
19
|
+
|
|
20
|
+
from robomation.ai.asr import ASR
|
|
21
|
+
from robomation.ai.aruco_marker import ArucoMarker
|
|
22
|
+
from robomation.ai.body_detection import BodyDetection
|
|
23
|
+
from robomation.ai.color_detection import ColorDetection
|
|
24
|
+
from robomation.ai.detailed_face_detection import DetailedFaceDetection
|
|
25
|
+
from robomation.ai.face_detection import FaceDetection
|
|
26
|
+
from robomation.ai.face_expression import FaceExpression
|
|
27
|
+
from robomation.ai.hand_detection import HandDetection
|
|
28
|
+
from robomation.ai.object_detection import ObjectDetection
|
|
29
|
+
from robomation.ai.self_driving import SelfDriving
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ASR",
|
|
33
|
+
"ArucoMarker",
|
|
34
|
+
"BodyDetection",
|
|
35
|
+
"ColorDetection",
|
|
36
|
+
"DetailedFaceDetection",
|
|
37
|
+
"FaceDetection",
|
|
38
|
+
"FaceExpression",
|
|
39
|
+
"HandDetection",
|
|
40
|
+
"ObjectDetection",
|
|
41
|
+
"SelfDriving",
|
|
42
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright (c) 2026 Robomation
|
|
2
|
+
#
|
|
3
|
+
# This library is free software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
5
|
+
# License as published by the Free Software Foundation; either
|
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
11
|
+
# Lesser General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU Lesser General
|
|
14
|
+
# Public License along with this library; if not, write to the
|
|
15
|
+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
|
16
|
+
# Boston, MA 02111-1307 USA
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Copyright (c) 2026 Robomation
|
|
2
|
+
#
|
|
3
|
+
# This library is free software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
5
|
+
# License as published by the Free Software Foundation; either
|
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
11
|
+
# Lesser General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU Lesser General
|
|
14
|
+
# Public License along with this library; if not, write to the
|
|
15
|
+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
|
16
|
+
# Boston, MA 02111-1307 USA
|
|
17
|
+
|
|
18
|
+
# AI 확장 모듈 - 카메라(Camera)
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
import threading
|
|
23
|
+
import subprocess
|
|
24
|
+
|
|
25
|
+
import cv2
|
|
26
|
+
|
|
27
|
+
from robomation.core.runner import Runner
|
|
28
|
+
from robomation.ai._internal import _display
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── 카메라 이름 열거 + 식별자 해석 (cross-OS) ────────────────────
|
|
32
|
+
|
|
33
|
+
def list_cameras():
|
|
34
|
+
"""OS별 사용 가능한 카메라 목록 [(index, name), ...].
|
|
35
|
+
같은 label 이 둘 이상이면 deviceID(index) 순으로 ' #1', ' #2' 접미사를 붙여 고유화한다.
|
|
36
|
+
(예: 동일 모델 카메라 2대 → 'HAMSTER_AI_UVC (303a:8003) #1' / '... #2')
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
if sys.platform == 'darwin':
|
|
40
|
+
cams = _list_macos()
|
|
41
|
+
elif sys.platform.startswith('win'):
|
|
42
|
+
cams = _list_windows()
|
|
43
|
+
else:
|
|
44
|
+
cams = _list_linux()
|
|
45
|
+
except Exception:
|
|
46
|
+
return []
|
|
47
|
+
return _labeling(cams)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _labeling(cams):
|
|
51
|
+
# 같은 이름이 여러 개면 deviceID(index) 오름차순으로 #1, #2 ... 를 붙여 고유 label 생성.
|
|
52
|
+
# 이름이 하나뿐이면 접미사 없이 그대로 둔다.
|
|
53
|
+
counts = {}
|
|
54
|
+
for _, name in cams:
|
|
55
|
+
counts[name] = counts.get(name, 0) + 1
|
|
56
|
+
seen = {}
|
|
57
|
+
result = []
|
|
58
|
+
for idx, name in sorted(cams, key=lambda c: c[0]):
|
|
59
|
+
if counts[name] > 1:
|
|
60
|
+
seen[name] = seen.get(name, 0) + 1
|
|
61
|
+
name = f"{name} #{seen[name]}"
|
|
62
|
+
result.append((idx, name))
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve(x):
|
|
67
|
+
"""device() 인자 → 카메라 인덱스(int).
|
|
68
|
+
- int(또는 숫자문자열) : 카메라 리스트에서 index 가 그 값인 카메라
|
|
69
|
+
- str(이름) : 그 이름의 카메라 (부분/양방향 매칭)
|
|
70
|
+
"""
|
|
71
|
+
if isinstance(x, bool):
|
|
72
|
+
return 0
|
|
73
|
+
cams = list_cameras()
|
|
74
|
+
# 숫자 → 인덱스: 리스트에서 index 가 그 값인 카메라
|
|
75
|
+
if isinstance(x, int) or (isinstance(x, str) and x.strip().isdigit()):
|
|
76
|
+
idx = int(x)
|
|
77
|
+
for i, _ in cams:
|
|
78
|
+
if i == idx:
|
|
79
|
+
return idx
|
|
80
|
+
if not cams: # 카메라 열거가 안 되는 환경 → 인덱스 그대로 시도
|
|
81
|
+
return idx
|
|
82
|
+
print(f"[Camera] 인덱스 {idx} 카메라가 목록에 없습니다. "
|
|
83
|
+
f"인덱스(0, 1, ...) 또는 카메라 이름으로 지정하세요.", file=sys.stderr)
|
|
84
|
+
return idx # cv2 인덱스로는 유효할 수 있으니 그대로 시도
|
|
85
|
+
# 이름 → 카메라 찾기
|
|
86
|
+
if isinstance(x, str):
|
|
87
|
+
s = x.strip()
|
|
88
|
+
for i, name in cams: # 정확히 일치 우선
|
|
89
|
+
if s == name:
|
|
90
|
+
return i
|
|
91
|
+
# 부분 일치(양방향): 브라우저 'FaceTime HD 카메라 (3A71:F4B5)' 처럼 접미사가 붙어도 매칭
|
|
92
|
+
sl = s.lower()
|
|
93
|
+
for i, name in cams:
|
|
94
|
+
nl = name.lower()
|
|
95
|
+
if nl in sl or sl in nl:
|
|
96
|
+
return i
|
|
97
|
+
# 이름으로 지정했는데 못 찾으면 다른 카메라로 폴백하지 않고 연결하지 않음(-1).
|
|
98
|
+
print(f"[Camera] 선택한 카메라 '{s}' 를 사용할 수 없습니다. (연결 안 함)\n"
|
|
99
|
+
f" 인덱스(0, 1, ...) 또는 정확한 카메라 이름으로 지정하세요.", file=sys.stderr)
|
|
100
|
+
return -1
|
|
101
|
+
return -1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _list_macos():
|
|
105
|
+
# system_profiler SPCameraDataType 출력 파싱.
|
|
106
|
+
# 카메라 이름 라인: 들여쓰기 후 ':' 로 끝남 ('Camera:' 헤더 제외)
|
|
107
|
+
# Unique ID 라인 : 'Unique ID: 47B4...-AE273A71F4B5'
|
|
108
|
+
# 브라우저(Chrome)는 라벨에 Unique ID 끝 8자리를 '(3A71:F4B5)' 로 붙인다 → 동일하게 맞춤.
|
|
109
|
+
out = subprocess.run(
|
|
110
|
+
['system_profiler', 'SPCameraDataType'],
|
|
111
|
+
capture_output=True, text=True, timeout=5,
|
|
112
|
+
).stdout
|
|
113
|
+
cams = [] # [[name, uid], ...]
|
|
114
|
+
for line in out.splitlines():
|
|
115
|
+
s = line.strip()
|
|
116
|
+
if not s or s == 'Camera:':
|
|
117
|
+
continue
|
|
118
|
+
if s.startswith('Unique ID:'):
|
|
119
|
+
if cams:
|
|
120
|
+
cams[-1][1] = s.split(':', 1)[1].strip()
|
|
121
|
+
elif s.endswith(':'):
|
|
122
|
+
cams.append([s[:-1].strip(), None])
|
|
123
|
+
result = []
|
|
124
|
+
for idx, (name, uid) in enumerate(cams):
|
|
125
|
+
if uid:
|
|
126
|
+
clean = uid.replace('-', '')
|
|
127
|
+
if len(clean) >= 8:
|
|
128
|
+
name = f"{name} ({clean[-8:-4]}:{clean[-4:]})" # 브라우저 포맷
|
|
129
|
+
result.append((idx, name))
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _list_windows():
|
|
134
|
+
# DirectShow 장치 이름 (pygrabber 필요). 미설치 시 빈 목록.
|
|
135
|
+
try:
|
|
136
|
+
from pygrabber.dshow_graph import FilterGraph
|
|
137
|
+
return list(enumerate(FilterGraph().get_input_devices()))
|
|
138
|
+
except Exception:
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _list_linux():
|
|
143
|
+
# /sys/class/video4linux/video*/name 에서 이름 추출.
|
|
144
|
+
import glob, os, re
|
|
145
|
+
result = []
|
|
146
|
+
for path in sorted(glob.glob('/sys/class/video4linux/video*')):
|
|
147
|
+
m = re.search(r'video(\d+)$', path)
|
|
148
|
+
if not m:
|
|
149
|
+
continue
|
|
150
|
+
idx = int(m.group(1))
|
|
151
|
+
try:
|
|
152
|
+
with open(os.path.join(path, 'name')) as f:
|
|
153
|
+
name = f.read().strip()
|
|
154
|
+
except Exception:
|
|
155
|
+
name = f'video{idx}'
|
|
156
|
+
result.append((idx, name))
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# 모든 카메라 프레임을 동일한 기준 폭으로 정규화(가로세로비 유지)한다.
|
|
161
|
+
# → 카메라 해상도와 무관하게 표시 크기·오버레이 크기·좌표(face.area 등)가 일관됨.
|
|
162
|
+
_FRAME_WIDTH = 800
|
|
163
|
+
|
|
164
|
+
class _CaptureSource:
|
|
165
|
+
"""실제 카메라 캡처(cv2.VideoCapture + 백그라운드 스레드). 프레임은 기준 폭으로 정규화."""
|
|
166
|
+
_instances = {}
|
|
167
|
+
_reg_lock = threading.Lock()
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def acquire(index):
|
|
171
|
+
with _CaptureSource._reg_lock:
|
|
172
|
+
src = _CaptureSource._instances.get(index)
|
|
173
|
+
if src is None:
|
|
174
|
+
src = _CaptureSource(index)
|
|
175
|
+
_CaptureSource._instances[index] = src
|
|
176
|
+
src._refcount += 1
|
|
177
|
+
return src
|
|
178
|
+
|
|
179
|
+
def __init__(self, index):
|
|
180
|
+
self._index = index
|
|
181
|
+
self._refcount = 0
|
|
182
|
+
self._frame = None
|
|
183
|
+
self.__frame_width = _FRAME_WIDTH # 정규화 기준 폭 (생성 시 고정)
|
|
184
|
+
self._running = True
|
|
185
|
+
self._cap = cv2.VideoCapture(index)
|
|
186
|
+
thread = threading.Thread(target=self._run)
|
|
187
|
+
thread.daemon = True
|
|
188
|
+
thread.start()
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def _frame_width(self):
|
|
192
|
+
# 읽기 전용 — 런타임에 프레임 크기를 변경할 수 없다(setter 없음).
|
|
193
|
+
# 크기를 바꾸려면 모듈 상수 _FRAME_WIDTH 만 수정한다. _normalize 가 폭만 받고
|
|
194
|
+
# 높이를 비례 계산하므로 어떤 값으로 바꿔도 가로세로비는 항상 유지된다.
|
|
195
|
+
return self.__frame_width
|
|
196
|
+
|
|
197
|
+
def _run(self):
|
|
198
|
+
while self._running:
|
|
199
|
+
cap = self._cap
|
|
200
|
+
if cap is not None and cap.isOpened():
|
|
201
|
+
ok, frame = cap.read()
|
|
202
|
+
if ok:
|
|
203
|
+
self._frame = self._normalize(frame)
|
|
204
|
+
time.sleep(0.01)
|
|
205
|
+
|
|
206
|
+
def _normalize(self, frame):
|
|
207
|
+
# 기준 폭(self._frame_width)으로 리사이즈(가로세로비 유지). 폭이 같으면 그대로.
|
|
208
|
+
fw = self._frame_width
|
|
209
|
+
h, w = frame.shape[:2]
|
|
210
|
+
if w > 0 and w != fw:
|
|
211
|
+
nh = max(1, round(h * fw / w))
|
|
212
|
+
frame = cv2.resize(frame, (fw, nh))
|
|
213
|
+
return frame
|
|
214
|
+
|
|
215
|
+
def read(self):
|
|
216
|
+
return self._frame
|
|
217
|
+
|
|
218
|
+
def is_opened(self):
|
|
219
|
+
return self._cap is not None and self._cap.isOpened()
|
|
220
|
+
|
|
221
|
+
def wait_first_frame(self, timeout=0.5):
|
|
222
|
+
deadline = time.time() + timeout
|
|
223
|
+
while self._frame is None and time.time() < deadline:
|
|
224
|
+
time.sleep(0.01)
|
|
225
|
+
return self._frame
|
|
226
|
+
|
|
227
|
+
def release(self):
|
|
228
|
+
with _CaptureSource._reg_lock:
|
|
229
|
+
self._refcount -= 1
|
|
230
|
+
if self._refcount > 0:
|
|
231
|
+
return
|
|
232
|
+
self._running = False
|
|
233
|
+
_CaptureSource._instances.pop(self._index, None)
|
|
234
|
+
cap = self._cap
|
|
235
|
+
self._cap = None
|
|
236
|
+
if cap is not None:
|
|
237
|
+
cap.release()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Camera:
|
|
241
|
+
"""Camera ai extension"""
|
|
242
|
+
ID = "kr.robomation.virtual.ai.camera"
|
|
243
|
+
|
|
244
|
+
def __init__(self, device=None):
|
|
245
|
+
self._index = 0 if device is None else resolve(device)
|
|
246
|
+
self._source = _CaptureSource.acquire(self._index) if self._index >= 0 else None
|
|
247
|
+
self._name = f"Camera {self._index}"
|
|
248
|
+
self._showing = False
|
|
249
|
+
self._closed = False
|
|
250
|
+
Runner.register_component(self)
|
|
251
|
+
self._check_permission()
|
|
252
|
+
|
|
253
|
+
def device(self, x):
|
|
254
|
+
"""카메라 재지정. x = 이름(str) / 인덱스(int) / Camera 객체."""
|
|
255
|
+
if isinstance(x, Camera):
|
|
256
|
+
index = x._index
|
|
257
|
+
else:
|
|
258
|
+
index = resolve(x)
|
|
259
|
+
if index == self._index:
|
|
260
|
+
return
|
|
261
|
+
was_showing = self._showing
|
|
262
|
+
if was_showing:
|
|
263
|
+
self.display(False)
|
|
264
|
+
if self._source is not None:
|
|
265
|
+
self._source.release()
|
|
266
|
+
self._index = index
|
|
267
|
+
self._source = _CaptureSource.acquire(index) if index >= 0 else None
|
|
268
|
+
self._name = f"Camera {index}"
|
|
269
|
+
self._check_permission()
|
|
270
|
+
if was_showing:
|
|
271
|
+
self.display(True)
|
|
272
|
+
|
|
273
|
+
def read(self):
|
|
274
|
+
"""최신 프레임(numpy BGR) 또는 None."""
|
|
275
|
+
return self._source.read() if self._source is not None else None
|
|
276
|
+
|
|
277
|
+
def _view(self):
|
|
278
|
+
if self._source is None:
|
|
279
|
+
return None
|
|
280
|
+
frame = self._source.read()
|
|
281
|
+
if frame is None:
|
|
282
|
+
return None
|
|
283
|
+
# 정규화 프레임 그대로 반환. 창 크기에 맞춘 늘이기(왜곡 포함)는
|
|
284
|
+
# WINDOW_NORMAL 창의 imshow 가 처리한다(드래그하면 영상이 창 비율대로 늘어남).
|
|
285
|
+
return (self._name, frame)
|
|
286
|
+
|
|
287
|
+
def display(self, on=True):
|
|
288
|
+
"""카메라 화면 창 표시/숨김. 실제 표시는 메인 스레드 Runner.wait() 펌프에서."""
|
|
289
|
+
if on:
|
|
290
|
+
self._showing = True
|
|
291
|
+
_display.add_view(self._name, self._view)
|
|
292
|
+
else:
|
|
293
|
+
self._showing = False
|
|
294
|
+
_display.remove_view(self._name)
|
|
295
|
+
|
|
296
|
+
def _check_permission(self):
|
|
297
|
+
if self._source is None: # 연결된 카메라 없음(-1) → 권한 검사 불필요
|
|
298
|
+
return
|
|
299
|
+
# macOS: 권한 미허용 시 캡처가 안 열리거나 검은 프레임만 온다.
|
|
300
|
+
# 단 정상 허용돼도 카메라 워밍업에 시간이 걸려 첫 프레임이 잠시 None 일 수 있으므로,
|
|
301
|
+
# 구성을 막지 않고 백그라운드에서 충분히 기다린 뒤 판단한다.
|
|
302
|
+
if sys.platform != 'darwin':
|
|
303
|
+
return
|
|
304
|
+
thread = threading.Thread(target=self._permission_watch, args=(self._source,))
|
|
305
|
+
thread.daemon = True
|
|
306
|
+
thread.start()
|
|
307
|
+
|
|
308
|
+
def _permission_watch(self, source):
|
|
309
|
+
deadline = time.time() + 3.0
|
|
310
|
+
while time.time() < deadline:
|
|
311
|
+
if self._closed or source is not self._source:
|
|
312
|
+
return # dispose/재지정됨 → 무시
|
|
313
|
+
frame = source.read()
|
|
314
|
+
if frame is not None and frame.max() > 0:
|
|
315
|
+
return # 정상 (비검은 프레임 도착)
|
|
316
|
+
time.sleep(0.05)
|
|
317
|
+
if self._closed or source is not self._source:
|
|
318
|
+
return
|
|
319
|
+
from robomation.core import permission
|
|
320
|
+
print(
|
|
321
|
+
"[Camera] 카메라 입력이 감지되지 않습니다.\n"
|
|
322
|
+
" macOS 라면 Python 을 실행한 앱(VSCode/터미널 등)의 카메라 권한이 꺼져 있을 수 있습니다.\n"
|
|
323
|
+
" 시스템 설정 → 개인정보 보호 및 보안 → 카메라 에서 해당 앱을 허용한 뒤,\n"
|
|
324
|
+
" 그 앱을 완전히 종료했다가 다시 실행하세요.",
|
|
325
|
+
file=sys.stderr,
|
|
326
|
+
)
|
|
327
|
+
permission.open_camera_settings()
|
|
328
|
+
|
|
329
|
+
def dispose(self):
|
|
330
|
+
self._closed = True
|
|
331
|
+
self.display(False)
|
|
332
|
+
if self._source is not None:
|
|
333
|
+
self._source.release()
|
|
334
|
+
self._source = None
|
|
335
|
+
Runner.unregister_component(self)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Copyright (c) 2026 Robomation
|
|
2
|
+
#
|
|
3
|
+
# This library is free software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
5
|
+
# License as published by the Free Software Foundation; either
|
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
11
|
+
# Lesser General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU Lesser General
|
|
14
|
+
# Public License along with this library; if not, write to the
|
|
15
|
+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
|
16
|
+
# Boston, MA 02111-1307 USA
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
import cv2
|
|
22
|
+
|
|
23
|
+
from robomation.core.runner import Runner
|
|
24
|
+
|
|
25
|
+
_views = {} # key -> provider() → (title, frame) | None
|
|
26
|
+
_lock = threading.Lock()
|
|
27
|
+
_registered = False
|
|
28
|
+
_last_show = 0.0
|
|
29
|
+
_FPS_INTERVAL = 1.0 / 30
|
|
30
|
+
_created = set() # namedWindow 로 이미 생성한 창 제목 (중복 생성 방지)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def add_view(key, provider):
|
|
34
|
+
"""표시할 뷰 등록. provider() 는 (title, frame) 또는 None 을 반환."""
|
|
35
|
+
global _registered
|
|
36
|
+
with _lock:
|
|
37
|
+
_views[key] = provider
|
|
38
|
+
if not _registered:
|
|
39
|
+
Runner.register_wait_callback(_pump)
|
|
40
|
+
_registered = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def remove_view(key):
|
|
44
|
+
with _lock:
|
|
45
|
+
existed = _views.pop(key, None) is not None
|
|
46
|
+
_created.discard(str(key))
|
|
47
|
+
if existed and threading.current_thread() is threading.main_thread():
|
|
48
|
+
try:
|
|
49
|
+
cv2.destroyWindow(str(key))
|
|
50
|
+
cv2.waitKey(1)
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _pump():
|
|
56
|
+
# 메인 스레드에서만 GUI 호출 (다른 스레드에서 wait 가 불려도 표시는 건너뜀)
|
|
57
|
+
if threading.current_thread() is not threading.main_thread():
|
|
58
|
+
return
|
|
59
|
+
global _last_show
|
|
60
|
+
now = time.time()
|
|
61
|
+
if now - _last_show < _FPS_INTERVAL:
|
|
62
|
+
return
|
|
63
|
+
_last_show = now
|
|
64
|
+
with _lock:
|
|
65
|
+
items = list(_views.items())
|
|
66
|
+
if not items:
|
|
67
|
+
return
|
|
68
|
+
for key, provider in items:
|
|
69
|
+
try:
|
|
70
|
+
res = provider()
|
|
71
|
+
if res:
|
|
72
|
+
title, frame = res
|
|
73
|
+
if frame is not None and frame.shape[0] > 0 and frame.shape[1] > 0:
|
|
74
|
+
if title not in _created:
|
|
75
|
+
# WINDOW_AUTOSIZE: 창이 항상 영상 크기에 고정
|
|
76
|
+
cv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)
|
|
77
|
+
try:
|
|
78
|
+
cv2.setWindowProperty(title, cv2.WND_PROP_AUTOSIZE, 1)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
_created.add(title)
|
|
82
|
+
cv2.imshow(title, frame)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
cv2.waitKey(1) # GUI 이벤트 처리 (필수)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copyright (c) 2026 Robomation
|
|
2
|
+
#
|
|
3
|
+
# This library is free software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
5
|
+
# License as published by the Free Software Foundation; either
|
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
11
|
+
# Lesser General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU Lesser General
|
|
14
|
+
# Public License along with this library; if not, write to the
|
|
15
|
+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
|
16
|
+
# Boston, MA 02111-1307 USA
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import urllib.request
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# fd 2(stderr) 억제 상태 — 여러 스레드/엔진(연속 추론)이 겹쳐 들어와도 fd 가 깨지지
|
|
27
|
+
# 않도록 lock + 깊이(refcount)로 관리. 깊이>0 동안만 fd 2 가 devnull 로 향한다.
|
|
28
|
+
_supp_lock = threading.Lock()
|
|
29
|
+
_supp_depth = 0
|
|
30
|
+
_supp_saved = None
|
|
31
|
+
_supp_devnull = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@contextmanager
|
|
35
|
+
def suppress_native_stderr():
|
|
36
|
+
"""MediaPipe/TFLite 네이티브 로그(absl/glog) 억제용.
|
|
37
|
+
|
|
38
|
+
이 로그들은 C++ 에서 stderr(fd 2)에 직접 쓰므로 파이썬 logging/환경변수
|
|
39
|
+
(GLOG_minloglevel 등)로는 막히지 않는다. 잠깐 fd 2 를 devnull 로 돌려 숨긴다.
|
|
40
|
+
모델 생성/종료뿐 아니라 일부 모델은 detect 때도 경고를 내므로 그 호출도 감쌀 수 있다.
|
|
41
|
+
refcount 방식이라 동시/중첩 호출에도 안전하다(마지막 컨텍스트가 빠질 때 복구).
|
|
42
|
+
"""
|
|
43
|
+
global _supp_depth, _supp_saved, _supp_devnull
|
|
44
|
+
with _supp_lock:
|
|
45
|
+
if _supp_depth == 0:
|
|
46
|
+
sys.stderr.flush()
|
|
47
|
+
_supp_saved = os.dup(2)
|
|
48
|
+
_supp_devnull = os.open(os.devnull, os.O_WRONLY)
|
|
49
|
+
os.dup2(_supp_devnull, 2)
|
|
50
|
+
_supp_depth += 1
|
|
51
|
+
try:
|
|
52
|
+
yield
|
|
53
|
+
finally:
|
|
54
|
+
with _supp_lock:
|
|
55
|
+
_supp_depth -= 1
|
|
56
|
+
if _supp_depth == 0:
|
|
57
|
+
sys.stderr.flush()
|
|
58
|
+
os.dup2(_supp_saved, 2)
|
|
59
|
+
os.close(_supp_devnull)
|
|
60
|
+
os.close(_supp_saved)
|
|
61
|
+
_supp_saved = None
|
|
62
|
+
_supp_devnull = None
|
|
63
|
+
|
|
64
|
+
_BUNDLED_DIR = Path(__file__).resolve().parent.parent / '_model'
|
|
65
|
+
_CACHE_DIR = Path.home() / '.robomation' / 'models'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_model(filename, url, label):
|
|
69
|
+
"""모델 파일의 로컬 경로를 반환. 캐시→다운로드 순. 모두 실패 시 None."""
|
|
70
|
+
# 1) 사용자 캐시
|
|
71
|
+
cached = _CACHE_DIR / filename
|
|
72
|
+
if cached.exists():
|
|
73
|
+
return str(cached)
|
|
74
|
+
# 2) 다운로드 (온라인일 때만)
|
|
75
|
+
try:
|
|
76
|
+
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
print(f"[{label}] 모델 다운로드 중...",
|
|
78
|
+
file=sys.stderr)
|
|
79
|
+
urllib.request.urlretrieve(url, cached)
|
|
80
|
+
return str(cached)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"[{label}] 모델을 찾을 수 없습니다(번들/캐시 없음, 다운로드 실패): {e}",
|
|
83
|
+
file=sys.stderr)
|
|
84
|
+
return None
|