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.
Files changed (76) hide show
  1. robomation/__init__.py +77 -0
  2. robomation/ai/__init__.py +42 -0
  3. robomation/ai/_internal/__init__.py +16 -0
  4. robomation/ai/_internal/_camera.py +335 -0
  5. robomation/ai/_internal/_display.py +85 -0
  6. robomation/ai/_internal/_model.py +84 -0
  7. robomation/ai/aruco_marker.py +189 -0
  8. robomation/ai/aruco_marker_roboid.py +398 -0
  9. robomation/ai/asr.py +121 -0
  10. robomation/ai/asr_roboid.py +319 -0
  11. robomation/ai/body_detection.py +291 -0
  12. robomation/ai/body_detection_roboid.py +450 -0
  13. robomation/ai/color_detection.py +197 -0
  14. robomation/ai/color_detection_roboid.py +467 -0
  15. robomation/ai/detailed_face_detection.py +268 -0
  16. robomation/ai/detailed_face_detection_roboid.py +508 -0
  17. robomation/ai/face_detection.py +231 -0
  18. robomation/ai/face_detection_roboid.py +458 -0
  19. robomation/ai/face_expression.py +206 -0
  20. robomation/ai/face_expression_roboid.py +500 -0
  21. robomation/ai/hand_detection.py +269 -0
  22. robomation/ai/hand_detection_roboid.py +495 -0
  23. robomation/ai/object_detection.py +244 -0
  24. robomation/ai/object_detection_roboid.py +483 -0
  25. robomation/ai/self_driving.py +239 -0
  26. robomation/ai/self_driving_roboid.py +416 -0
  27. robomation/cheesestick_ext/__init__.py +18 -0
  28. robomation/cheesestick_ext/csd01.py +68 -0
  29. robomation/cheesestick_ext/csd02.py +96 -0
  30. robomation/cheesestick_ext/csd03.py +77 -0
  31. robomation/cheesestick_ext/csd07.py +63 -0
  32. robomation/cheesestick_ext/csd09.py +178 -0
  33. robomation/cheesestick_ext/csd10.py +64 -0
  34. robomation/cheesestick_ext/hat010.py +193 -0
  35. robomation/cheesestick_ext/hat022.py +91 -0
  36. robomation/cheesestick_ext/neopixel.py +219 -0
  37. robomation/cheesestick_ext/pid10.py +48 -0
  38. robomation/cheesestick_ext/pid13.py +67 -0
  39. robomation/cheesestick_ext/pid26.py +51 -0
  40. robomation/core/__init__.py +18 -0
  41. robomation/core/_internal/__init__.py +16 -0
  42. robomation/core/_internal/_log.py +71 -0
  43. robomation/core/_internal/_scope.py +271 -0
  44. robomation/core/_internal/_sound.py +142 -0
  45. robomation/core/_internal/_tts.py +118 -0
  46. robomation/core/error.py +28 -0
  47. robomation/core/keyboard.py +100 -0
  48. robomation/core/model.py +559 -0
  49. robomation/core/permission.py +48 -0
  50. robomation/core/runner.py +337 -0
  51. robomation/core/scanner.py +36 -0
  52. robomation/core/serial_connector.py +355 -0
  53. robomation/core/utils.py +135 -0
  54. robomation/py.typed +0 -0
  55. robomation/roboids/__init__.py +36 -0
  56. robomation/roboids/beagle.py +436 -0
  57. robomation/roboids/beagle_roboid.py +714 -0
  58. robomation/roboids/cheesestick.py +557 -0
  59. robomation/roboids/cheesestick_roboid.py +1392 -0
  60. robomation/roboids/hamster.py +518 -0
  61. robomation/roboids/hamster_roboid.py +370 -0
  62. robomation/roboids/hamster_s.py +809 -0
  63. robomation/roboids/hamster_s_roboid.py +497 -0
  64. robomation/roboids/pio.py +546 -0
  65. robomation/roboids/pio_roboid.py +536 -0
  66. robomation/roboids/raccoonbot.py +763 -0
  67. robomation/roboids/raccoonbot_roboid.py +609 -0
  68. robomation/roboids/turtle.py +624 -0
  69. robomation/roboids/turtle_roboid.py +467 -0
  70. robomation-0.1.0.dist-info/METADATA +179 -0
  71. robomation-0.1.0.dist-info/RECORD +76 -0
  72. robomation-0.1.0.dist-info/WHEEL +5 -0
  73. robomation-0.1.0.dist-info/licenses/LICENSE +501 -0
  74. robomation-0.1.0.dist-info/scm_file_list.json +79 -0
  75. robomation-0.1.0.dist-info/scm_version.json +8 -0
  76. 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