pyaidrone 2.1__tar.gz → 2.2__tar.gz
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.
- {pyaidrone-2.1 → pyaidrone-2.2}/PKG-INFO +1 -1
- pyaidrone-2.2/pyaidrone/aiDrone.py +352 -0
- pyaidrone-2.2/pyaidrone/edu.py +192 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone/vision_ai.py +6 -2
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone.egg-info/PKG-INFO +1 -1
- {pyaidrone-2.1 → pyaidrone-2.2}/setup.py +1 -1
- pyaidrone-2.1/pyaidrone/aiDrone.py +0 -221
- pyaidrone-2.1/pyaidrone/edu.py +0 -188
- {pyaidrone-2.1 → pyaidrone-2.2}/README.md +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone/__init__.py +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone/aidrone_mavlink_bridge.py +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone/deflib.py +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone/ikeyevent.py +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone/packet.py +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone/parse.py +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone/sensor.py +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone.egg-info/SOURCES.txt +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone.egg-info/dependency_links.txt +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone.egg-info/requires.txt +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/pyaidrone.egg-info/top_level.txt +0 -0
- {pyaidrone-2.1 → pyaidrone-2.2}/setup.cfg +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import serial
|
|
2
|
+
import binascii
|
|
3
|
+
import math
|
|
4
|
+
from time import sleep
|
|
5
|
+
import random
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
from operator import eq
|
|
9
|
+
from threading import Thread
|
|
10
|
+
from serial.tools.list_ports import comports
|
|
11
|
+
from pyaidrone.parse import *
|
|
12
|
+
from pyaidrone.packet import *
|
|
13
|
+
from pyaidrone.deflib import *
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AIDrone(Parse, Packet):
|
|
17
|
+
def __init__(self, receiveCallback=None, index=0):
|
|
18
|
+
self.serial = None
|
|
19
|
+
self.isThreadRun = False
|
|
20
|
+
self.parse = Parse(AIDRONE)
|
|
21
|
+
self.makepkt = Packet(AIDRONE)
|
|
22
|
+
self.receiveCallback = receiveCallback
|
|
23
|
+
self.makepkt.clearPacket()
|
|
24
|
+
self.posX = 0
|
|
25
|
+
self.posY = 0
|
|
26
|
+
self.rot = 0
|
|
27
|
+
self.portIndex = index # 멀티 연결 시 드론 번호 식별용
|
|
28
|
+
# --- 영상 스트리밍 관련 기본값 ---
|
|
29
|
+
self.stream_host = "192.168.4.1"
|
|
30
|
+
self.stream_port = 80
|
|
31
|
+
self.stream_path = "/?action=stream"
|
|
32
|
+
self._cap = None
|
|
33
|
+
|
|
34
|
+
def receiveHandler(self):
|
|
35
|
+
while self.isThreadRun:
|
|
36
|
+
readData = self.serial.read(self.serial.in_waiting or 1)
|
|
37
|
+
packet = self.parse.packetArrange(readData)
|
|
38
|
+
if not eq(packet, "None"):
|
|
39
|
+
if self.receiveCallback is not None:
|
|
40
|
+
self.receiveCallback(packet)
|
|
41
|
+
self.serial.write(self.makepkt.getPacket())
|
|
42
|
+
|
|
43
|
+
def Open(self, portName="None"):
|
|
44
|
+
if eq(portName, "None"):
|
|
45
|
+
nodes = comports()
|
|
46
|
+
for node in nodes:
|
|
47
|
+
if "CH340" in node.description:
|
|
48
|
+
portName = node.device
|
|
49
|
+
|
|
50
|
+
if eq(portName, "None"):
|
|
51
|
+
print("Can't find Serial Port")
|
|
52
|
+
exit()
|
|
53
|
+
return False
|
|
54
|
+
try:
|
|
55
|
+
self.serial = serial.Serial(port=portName, baudrate=19200, timeout=1)
|
|
56
|
+
if self.serial.isOpen():
|
|
57
|
+
self.isThreadRun = True
|
|
58
|
+
self.thread = Thread(target=self.receiveHandler, args=(), daemon=True)
|
|
59
|
+
self.thread.start()
|
|
60
|
+
print(f"[Drone #{self.portIndex}] Connected to {portName}")
|
|
61
|
+
return True
|
|
62
|
+
else:
|
|
63
|
+
print("Can't open " + portName)
|
|
64
|
+
exit()
|
|
65
|
+
return False
|
|
66
|
+
except Exception:
|
|
67
|
+
print("Can't open " + portName)
|
|
68
|
+
exit()
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def Close(self):
|
|
72
|
+
self.isThreadRun = False
|
|
73
|
+
sleep(0.2)
|
|
74
|
+
pkt = self.makepkt.getPacket()
|
|
75
|
+
if (pkt[15] & 0x80) == 0x80:
|
|
76
|
+
self.makepkt.clearPacket()
|
|
77
|
+
self.setOption(0x8000)
|
|
78
|
+
self.serial.write(self.makepkt.getPacket())
|
|
79
|
+
sleep(0.2)
|
|
80
|
+
self.serial.write(self.makepkt.clearPacket())
|
|
81
|
+
sleep(0.2)
|
|
82
|
+
if self.serial is not None:
|
|
83
|
+
if self.serial.isOpen():
|
|
84
|
+
self.serial.close()
|
|
85
|
+
|
|
86
|
+
def setOption(self, option):
|
|
87
|
+
data = option.to_bytes(2, byteorder="little", signed=False)
|
|
88
|
+
self.makepkt.makePacket(14, data)
|
|
89
|
+
|
|
90
|
+
def takeoff(self):
|
|
91
|
+
alt = 70
|
|
92
|
+
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
93
|
+
self.makepkt.makePacket(12, data)
|
|
94
|
+
self.setOption(0x2F)
|
|
95
|
+
|
|
96
|
+
def landing(self):
|
|
97
|
+
alt = 0
|
|
98
|
+
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
99
|
+
self.makepkt.makePacket(12, data)
|
|
100
|
+
|
|
101
|
+
def altitude(self, alt):
|
|
102
|
+
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
103
|
+
self.makepkt.makePacket(12, data)
|
|
104
|
+
|
|
105
|
+
def velocity(self, dir=0, vel=100):
|
|
106
|
+
if dir > 3:
|
|
107
|
+
return
|
|
108
|
+
if dir == 1 or dir == 3:
|
|
109
|
+
vel *= -1
|
|
110
|
+
data = vel.to_bytes(2, byteorder="little", signed=True)
|
|
111
|
+
if dir == 0 or dir == 1:
|
|
112
|
+
self.makepkt.makePacket(8, data)
|
|
113
|
+
else:
|
|
114
|
+
self.makepkt.makePacket(6, data)
|
|
115
|
+
self.setOption(0x0F)
|
|
116
|
+
|
|
117
|
+
def move(self, dir=0, dist=100):
|
|
118
|
+
if dir > 3:
|
|
119
|
+
return
|
|
120
|
+
if dir == 1 or dir == 3:
|
|
121
|
+
dist *= -1
|
|
122
|
+
if dir == 0 or dir == 1:
|
|
123
|
+
self.posX += dist
|
|
124
|
+
data = self.posX.to_bytes(2, byteorder="little", signed=True)
|
|
125
|
+
self.makepkt.makePacket(8, data)
|
|
126
|
+
else:
|
|
127
|
+
self.posY += dist
|
|
128
|
+
data = self.posY.to_bytes(2, byteorder="little", signed=True)
|
|
129
|
+
self.makepkt.makePacket(6, data)
|
|
130
|
+
self.setOption(0x2F)
|
|
131
|
+
|
|
132
|
+
def rotation(self, rot=90):
|
|
133
|
+
self.rot += rot
|
|
134
|
+
data = self.rot.to_bytes(2, byteorder="little", signed=True)
|
|
135
|
+
self.makepkt.makePacket(10, data)
|
|
136
|
+
|
|
137
|
+
def motor(self, what, speed):
|
|
138
|
+
speed = DefLib.constrain(speed, 100, 0)
|
|
139
|
+
data = speed.to_bytes(2, byteorder="little", signed=True)
|
|
140
|
+
self.makepkt.makePacket(what * 2 + 6, data)
|
|
141
|
+
self.setOption(0x8000)
|
|
142
|
+
|
|
143
|
+
def emergency(self):
|
|
144
|
+
self.setOption(0x00)
|
|
145
|
+
self.serial.write(self.makepkt.getPacket())
|
|
146
|
+
|
|
147
|
+
# ── 스트리밍 관련 ──────────────────────────────────────────────────────────
|
|
148
|
+
def setStreamAddress(self, host: str, port: int = 80, path: str = "/?action=stream"):
|
|
149
|
+
"""MJPG-Streamer 주소 구성 요소를 저장합니다."""
|
|
150
|
+
if not isinstance(host, str) or len(host.strip()) == 0:
|
|
151
|
+
raise ValueError("host가 올바르지 않습니다.")
|
|
152
|
+
self.stream_host = host.strip()
|
|
153
|
+
self.stream_port = int(port)
|
|
154
|
+
if not path.startswith("/"):
|
|
155
|
+
path = "/" + path
|
|
156
|
+
self.stream_path = path
|
|
157
|
+
return self._build_stream_url()
|
|
158
|
+
|
|
159
|
+
def _build_stream_url(self):
|
|
160
|
+
"""내부 사용: 저장된 host/port/path로 URL 생성."""
|
|
161
|
+
if self.stream_port in (80, None):
|
|
162
|
+
return f"http://{self.stream_host}{self.stream_path}"
|
|
163
|
+
return f"http://{self.stream_host}:{self.stream_port}{self.stream_path}"
|
|
164
|
+
|
|
165
|
+
def streamon(self, host: str = None, port: int = None, path: str = None, return_url: bool = False):
|
|
166
|
+
"""
|
|
167
|
+
MJPG-Streamer 스트림을 엽니다.
|
|
168
|
+
- 인자를 주면 해당 값으로 주소를 덮어쓰고, 안 주면 저장된 값 사용.
|
|
169
|
+
- return_url=True 이면 URL 문자열만 반환(캡처는 열지 않음).
|
|
170
|
+
"""
|
|
171
|
+
if host is not None or port is not None or path is not None:
|
|
172
|
+
self.setStreamAddress(
|
|
173
|
+
host or self.stream_host,
|
|
174
|
+
self.stream_port if port is None else port,
|
|
175
|
+
self.stream_path if path is None else path,
|
|
176
|
+
)
|
|
177
|
+
url = self._build_stream_url()
|
|
178
|
+
if return_url:
|
|
179
|
+
return url
|
|
180
|
+
try:
|
|
181
|
+
import cv2 as cv
|
|
182
|
+
except Exception as e:
|
|
183
|
+
raise RuntimeError(f"OpenCV(cv2) 임포트 실패: {e}")
|
|
184
|
+
self.streamoff()
|
|
185
|
+
self._cap = cv.VideoCapture(url)
|
|
186
|
+
if not self._cap.isOpened():
|
|
187
|
+
self._cap.release()
|
|
188
|
+
self._cap = None
|
|
189
|
+
raise RuntimeError(f"스트림 열기 실패: {url}")
|
|
190
|
+
return self._cap
|
|
191
|
+
|
|
192
|
+
def streamoff(self):
|
|
193
|
+
"""열려 있는 스트림을 닫습니다."""
|
|
194
|
+
if self._cap is not None:
|
|
195
|
+
try:
|
|
196
|
+
self._cap.release()
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
self._cap = None
|
|
200
|
+
|
|
201
|
+
# ── 센서 데이터 요청 ────────────────────────────────────────────────────────
|
|
202
|
+
def requestSensorData(self, type_num, mode=1):
|
|
203
|
+
"""
|
|
204
|
+
드론에게 센서 및 비행 정보 출력을 요청합니다.
|
|
205
|
+
type_num: 9 (비행 정보), 10 (센서 신호)
|
|
206
|
+
mode: 1 (출력 시작), 0 (출력 중지)
|
|
207
|
+
"""
|
|
208
|
+
if type_num == 9:
|
|
209
|
+
data = mode.to_bytes(1, byteorder="little")
|
|
210
|
+
self.makepkt.packet[3] = 0xB2
|
|
211
|
+
self.makepkt.makePacket(7, data)
|
|
212
|
+
elif type_num == 10:
|
|
213
|
+
data = mode.to_bytes(1, byteorder="little")
|
|
214
|
+
self.makepkt.packet[3] = 0xB1
|
|
215
|
+
self.makepkt.makePacket(7, data)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
219
|
+
# 멀티 드론 헬퍼 함수
|
|
220
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
221
|
+
|
|
222
|
+
def getWindowsPortList():
|
|
223
|
+
"""Windows에서 CH340 포트 목록 반환."""
|
|
224
|
+
nodes = comports()
|
|
225
|
+
portList = [node.device for node in nodes if "CH340" in node.description]
|
|
226
|
+
if not portList:
|
|
227
|
+
print("Can't find Serial Port (CH340)")
|
|
228
|
+
return None
|
|
229
|
+
print(f"발견된 포트: {portList}")
|
|
230
|
+
return portList
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def getLinuxPortList():
|
|
234
|
+
"""Linux에서 /dev/ttyUSB* 포트 목록 반환."""
|
|
235
|
+
try:
|
|
236
|
+
output = subprocess.check_output("ls /dev/ttyUSB*", shell=True)
|
|
237
|
+
return tuple(output.decode().strip().split())
|
|
238
|
+
except subprocess.CalledProcessError:
|
|
239
|
+
print("Can't find Serial Port (/dev/ttyUSB*)")
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def AIDroneMultiOpen(nameList=None, receiveCallback=None):
|
|
244
|
+
"""
|
|
245
|
+
여러 대의 AIDrone을 동시에 연결합니다.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
nameList : list[str] | None
|
|
250
|
+
포트 이름 목록. None이면 OS에 맞게 자동 탐색.
|
|
251
|
+
receiveCallback : callable | None
|
|
252
|
+
수신 콜백. 호출 시 첫 번째 인자로 AIDrone 인스턴스가 전달됩니다.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
list[AIDrone] | None
|
|
257
|
+
연결된 드론 객체 목록.
|
|
258
|
+
|
|
259
|
+
Example
|
|
260
|
+
-------
|
|
261
|
+
>>> drones = AIDroneMultiOpen()
|
|
262
|
+
>>> multi_takeoff(drones)
|
|
263
|
+
>>> sleep(3)
|
|
264
|
+
>>> multi_landing(drones)
|
|
265
|
+
>>> AIDroneMultiClose(drones)
|
|
266
|
+
"""
|
|
267
|
+
if nameList is None:
|
|
268
|
+
if platform.system() == "Linux":
|
|
269
|
+
nameList = getLinuxPortList()
|
|
270
|
+
else:
|
|
271
|
+
nameList = getWindowsPortList()
|
|
272
|
+
|
|
273
|
+
if not nameList:
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
howMany = min(len(nameList), 10) # 최대 10대 제한
|
|
277
|
+
drones = []
|
|
278
|
+
|
|
279
|
+
for n in range(howMany):
|
|
280
|
+
drone = AIDrone(receiveCallback, n)
|
|
281
|
+
if drone.Open(nameList[n]):
|
|
282
|
+
drones.append(drone)
|
|
283
|
+
else:
|
|
284
|
+
print(f"[Drone #{n}] 연결 실패 → 전체 연결 중단")
|
|
285
|
+
AIDroneMultiClose(drones)
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
print(f"총 {len(drones)}대 드론 연결 완료.")
|
|
289
|
+
return drones
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def AIDroneMultiClose(drones):
|
|
293
|
+
"""연결된 모든 드론을 안전하게 닫습니다."""
|
|
294
|
+
if not drones:
|
|
295
|
+
return
|
|
296
|
+
# 착륙 명령 먼저 전송
|
|
297
|
+
for drone in drones:
|
|
298
|
+
drone.landing()
|
|
299
|
+
sleep(0.5)
|
|
300
|
+
for drone in drones:
|
|
301
|
+
drone.Close()
|
|
302
|
+
print("모든 드론 연결 종료.")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ── 멀티 제어 함수 ─────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
def multi_takeoff(drones):
|
|
308
|
+
"""모든 드론 이륙."""
|
|
309
|
+
for drone in drones:
|
|
310
|
+
drone.takeoff()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def multi_landing(drones):
|
|
314
|
+
"""모든 드론 착륙."""
|
|
315
|
+
for drone in drones:
|
|
316
|
+
drone.landing()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def multi_altitude(drones, alt):
|
|
320
|
+
"""모든 드론 고도 설정."""
|
|
321
|
+
for drone in drones:
|
|
322
|
+
drone.altitude(alt)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def multi_move(drones, dir=0, dist=100):
|
|
326
|
+
"""모든 드론 이동."""
|
|
327
|
+
for drone in drones:
|
|
328
|
+
drone.move(dir, dist)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def multi_velocity(drones, dir=0, vel=100):
|
|
332
|
+
"""모든 드론 속도 제어."""
|
|
333
|
+
for drone in drones:
|
|
334
|
+
drone.velocity(dir, vel)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def multi_rotation(drones, rot=90):
|
|
338
|
+
"""모든 드론 회전."""
|
|
339
|
+
for drone in drones:
|
|
340
|
+
drone.rotation(rot)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def multi_motor(drones, what, speed):
|
|
344
|
+
"""모든 드론 모터 제어."""
|
|
345
|
+
for drone in drones:
|
|
346
|
+
drone.motor(what, speed)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def multi_emergency(drones):
|
|
350
|
+
"""모든 드론 긴급 정지."""
|
|
351
|
+
for drone in drones:
|
|
352
|
+
drone.emergency()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import time
|
|
3
|
+
import numpy as np
|
|
4
|
+
import os
|
|
5
|
+
from pyaidrone.aiDrone import AIDrone
|
|
6
|
+
from pyaidrone.vision_ai import TFLiteDetector, yolo_decode, draw_box_xywh, largest_contour, contour_centroid
|
|
7
|
+
from pyaidrone.deflib import *
|
|
8
|
+
|
|
9
|
+
class EduAIDrone:
|
|
10
|
+
"""
|
|
11
|
+
AI 교육을 위해 복잡한 기능을 단순화한 통합 API 클래스
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, port="COM3", use_simulation=False, model_path=None, labels_path=None):
|
|
14
|
+
self.use_simulation = use_simulation
|
|
15
|
+
self.last_frame = None
|
|
16
|
+
self.height = 100
|
|
17
|
+
self.cap = None
|
|
18
|
+
self.stream_url = None # 수정: self.stream.url 오타 수정
|
|
19
|
+
|
|
20
|
+
# 1. 시뮬레이션 모드 전용 설정 (지연 로딩 적용)
|
|
21
|
+
if self.use_simulation:
|
|
22
|
+
try:
|
|
23
|
+
import rclpy
|
|
24
|
+
from rclpy.node import Node
|
|
25
|
+
from geometry_msgs.msg import Twist
|
|
26
|
+
from sensor_msgs.msg import Image
|
|
27
|
+
from cv_bridge import CvBridge
|
|
28
|
+
|
|
29
|
+
if not rclpy.ok(): rclpy.init()
|
|
30
|
+
self.node = Node('edu_drone_node')
|
|
31
|
+
self.cmd_vel_pub = self.node.create_publisher(Twist, '/cmd_vel', 10)
|
|
32
|
+
self.bridge = CvBridge()
|
|
33
|
+
self.sim_frame = None
|
|
34
|
+
self.image_sub = self.node.create_subscription(Image, '/camera', self._gz_image_cb, 10)
|
|
35
|
+
print("🎮 ROS2 Gazebo 모드 활성화됨")
|
|
36
|
+
except ImportError:
|
|
37
|
+
print("❌ ROS2 환경을 찾을 수 없습니다. 실기체 모드로 전환합니다.")
|
|
38
|
+
self.use_simulation = False
|
|
39
|
+
|
|
40
|
+
# 2. 하드웨어 드론 인터페이스 초기화
|
|
41
|
+
self.aidrone = AIDrone()
|
|
42
|
+
self.port = port
|
|
43
|
+
|
|
44
|
+
# 3. AI 탐지기 및 라벨 설정
|
|
45
|
+
self.detector = TFLiteDetector(model_path) if model_path else None
|
|
46
|
+
self.labels = []
|
|
47
|
+
if labels_path and os.path.exists(labels_path):
|
|
48
|
+
with open(labels_path, 'r', encoding='utf-8') as f:
|
|
49
|
+
self.labels = [line.strip() for line in f.readlines()]
|
|
50
|
+
|
|
51
|
+
def _gz_image_cb(self, msg):
|
|
52
|
+
"""Gazebo 카메라 데이터 수신 콜백"""
|
|
53
|
+
self.sim_frame = self.bridge.imgmsg_to_cv2(msg, "bgr8")
|
|
54
|
+
|
|
55
|
+
def connect(self):
|
|
56
|
+
"""드론 연결 (실기체인 경우 시리얼 오픈)"""
|
|
57
|
+
if self.use_simulation:
|
|
58
|
+
return True
|
|
59
|
+
return self.aidrone.Open(self.port)
|
|
60
|
+
|
|
61
|
+
def update_screen(self, window_name="AI Drone Edu"):
|
|
62
|
+
"""영상 업데이트 (시뮬레이션 토픽 또는 실기체 MJPG 스트림)"""
|
|
63
|
+
if self.use_simulation:
|
|
64
|
+
import rclpy
|
|
65
|
+
rclpy.spin_once(self.node, timeout_sec=0)
|
|
66
|
+
if self.sim_frame is not None:
|
|
67
|
+
self.last_frame = cv2.resize(self.sim_frame, (640, 480))
|
|
68
|
+
return self.last_frame
|
|
69
|
+
else:
|
|
70
|
+
if self.cap is None: return None
|
|
71
|
+
ret, frame = self.cap.read()
|
|
72
|
+
if ret and frame is not None:
|
|
73
|
+
self.last_frame = cv2.resize(frame, (640, 480))
|
|
74
|
+
return self.last_frame
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def takeoff(self):
|
|
78
|
+
print("🚀 이륙합니다...")
|
|
79
|
+
if not self.use_simulation:
|
|
80
|
+
self.aidrone.takeoff()
|
|
81
|
+
time.sleep(2)
|
|
82
|
+
|
|
83
|
+
def land(self):
|
|
84
|
+
print("🛬 착륙합니다...")
|
|
85
|
+
if self.use_simulation:
|
|
86
|
+
from geometry_msgs.msg import Twist
|
|
87
|
+
self.cmd_vel_pub.publish(Twist())
|
|
88
|
+
else:
|
|
89
|
+
self.aidrone.landing()
|
|
90
|
+
|
|
91
|
+
def move(self, direction, speed=100):
|
|
92
|
+
if self.use_simulation:
|
|
93
|
+
from geometry_msgs.msg import Twist
|
|
94
|
+
msg = Twist()
|
|
95
|
+
val = speed / 100.0
|
|
96
|
+
if direction == 'front': msg.linear.x = val
|
|
97
|
+
elif direction == 'back': msg.linear.x = -val
|
|
98
|
+
elif direction == 'left': msg.linear.y = val
|
|
99
|
+
elif direction == 'right': msg.linear.y = -val
|
|
100
|
+
self.cmd_vel_pub.publish(msg)
|
|
101
|
+
else:
|
|
102
|
+
dir_map = {'front': FRONT, 'back': BACK, 'right': RIGHT, 'left': LEFT}
|
|
103
|
+
if direction in dir_map:
|
|
104
|
+
self.aidrone.velocity(dir_map[direction], speed)
|
|
105
|
+
self.aidrone.setOption(0x0F)
|
|
106
|
+
|
|
107
|
+
def turn(self, angle):
|
|
108
|
+
if self.use_simulation:
|
|
109
|
+
from geometry_msgs.msg import Twist
|
|
110
|
+
msg = Twist()
|
|
111
|
+
msg.angular.z = float(angle) * (np.pi / 180.0)
|
|
112
|
+
self.cmd_vel_pub.publish(msg)
|
|
113
|
+
else:
|
|
114
|
+
self.aidrone.rotation(angle)
|
|
115
|
+
|
|
116
|
+
def stop(self):
|
|
117
|
+
if self.use_simulation:
|
|
118
|
+
from geometry_msgs.msg import Twist
|
|
119
|
+
self.cmd_vel_pub.publish(Twist())
|
|
120
|
+
else:
|
|
121
|
+
self.aidrone.velocity(FRONT, 0)
|
|
122
|
+
self.aidrone.setOption(0x0F)
|
|
123
|
+
|
|
124
|
+
def find_color(self, color="red"):
|
|
125
|
+
if self.last_frame is None: return None
|
|
126
|
+
hsv = cv2.cvtColor(self.last_frame, cv2.COLOR_BGR2HSV)
|
|
127
|
+
ranges = {
|
|
128
|
+
"red": [(0, 150, 50), (10, 255, 255)],
|
|
129
|
+
"blue": [(100, 150, 50), (140, 255, 255)],
|
|
130
|
+
"green": [(40, 100, 50), (80, 255, 255)]
|
|
131
|
+
}
|
|
132
|
+
low, high = ranges.get(color, ranges["red"])
|
|
133
|
+
mask = cv2.inRange(hsv, np.array(low), np.array(high))
|
|
134
|
+
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
135
|
+
big_c = largest_contour(contours)
|
|
136
|
+
if big_c is not None:
|
|
137
|
+
return contour_centroid(big_c)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def find_object(self, target_name, threshold=0.5):
|
|
141
|
+
if not self.detector or self.last_frame is None: return None
|
|
142
|
+
results = self.detector.infer(self.last_frame, yolo_decode)
|
|
143
|
+
for res in results:
|
|
144
|
+
name = self.labels[res.class_id] if self.labels else f"ID:{res.class_id}"
|
|
145
|
+
if name == target_name and res.score > threshold:
|
|
146
|
+
return ((res.box[0] + res.box[2]) / 2, (res.box[1] + res.box[3]) / 2)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
def read_qr(self):
|
|
150
|
+
if self.last_frame is None: return None
|
|
151
|
+
data, _, _ = cv2.QRCodeDetector().detectAndDecode(self.last_frame)
|
|
152
|
+
return data if data else None
|
|
153
|
+
|
|
154
|
+
def follow_target(self, error_x, error_y):
|
|
155
|
+
"""오차값을 보고 드론을 자동으로 회전 및 고도 조절"""
|
|
156
|
+
yaw_step = int(error_x * 0.15)
|
|
157
|
+
|
|
158
|
+
if self.use_simulation:
|
|
159
|
+
self.turn(yaw_step)
|
|
160
|
+
else:
|
|
161
|
+
self.aidrone.rotation(yaw_step)
|
|
162
|
+
|
|
163
|
+
throttle_change = int(error_y * 0.2)
|
|
164
|
+
self.height = max(50, min(150, self.height + throttle_change))
|
|
165
|
+
|
|
166
|
+
if self.use_simulation:
|
|
167
|
+
from geometry_msgs.msg import Twist
|
|
168
|
+
msg = Twist()
|
|
169
|
+
# 고도 조절 시 기존 회전 성분 유지 필요 시 여기에 추가 로직 필요
|
|
170
|
+
msg.linear.z = float(throttle_change / 100.0)
|
|
171
|
+
self.cmd_vel_pub.publish(msg)
|
|
172
|
+
else:
|
|
173
|
+
self.aidrone.altitude(self.height)
|
|
174
|
+
|
|
175
|
+
def start_stream(self, url):
|
|
176
|
+
self.stream_url = url
|
|
177
|
+
self.cap = cv2.VideoCapture(url)
|
|
178
|
+
return self.cap.isOpened()
|
|
179
|
+
|
|
180
|
+
def reconnect_stream(self):
|
|
181
|
+
"""스트림 재연결 (중복 제거 및 통합)"""
|
|
182
|
+
if self.stream_url is None: return False
|
|
183
|
+
if self.cap: self.cap.release()
|
|
184
|
+
self.cap = None
|
|
185
|
+
time.sleep(1)
|
|
186
|
+
return self.start_stream(self.stream_url)
|
|
187
|
+
|
|
188
|
+
def save_image(self, frame, folder="captured_images"):
|
|
189
|
+
if not os.path.exists(folder): os.makedirs(folder)
|
|
190
|
+
timestamp = time.strftime("%H%M%S")
|
|
191
|
+
filename = f"{folder}/target_{timestamp}.jpg"
|
|
192
|
+
cv2.imwrite(filename, frame)
|
|
@@ -142,8 +142,12 @@ class VisionTracker:
|
|
|
142
142
|
|
|
143
143
|
roi = (x_roi, y_roi, w_roi, h_roi)
|
|
144
144
|
|
|
145
|
-
# 🔧 5. 추적기 생성 및 초기화
|
|
146
|
-
|
|
145
|
+
# 🔧 5. 추적기 생성 및 초기화
|
|
146
|
+
if hasattr(cv2, 'legacy'):
|
|
147
|
+
self.tracker = cv2.legacy.TrackerCSRT_create()
|
|
148
|
+
else:
|
|
149
|
+
self.tracker = cv2.TrackerCSRT_create()
|
|
150
|
+
|
|
147
151
|
success = self.tracker.init(frame, roi)
|
|
148
152
|
|
|
149
153
|
if success:
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="pyaidrone",
|
|
5
|
-
version="2.
|
|
5
|
+
version="2.2",
|
|
6
6
|
description="Library for AIDrone Products",
|
|
7
7
|
long_description=open("README.md").read(), # README.md 내용을 long_description으로 사용
|
|
8
8
|
long_description_content_type="text/markdown", # README 파일이 markdown 형식임을 지정
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import serial
|
|
2
|
-
import binascii
|
|
3
|
-
import math
|
|
4
|
-
from time import sleep
|
|
5
|
-
import random
|
|
6
|
-
from operator import eq
|
|
7
|
-
from threading import Thread
|
|
8
|
-
from serial.tools.list_ports import comports
|
|
9
|
-
from pyaidrone.parse import *
|
|
10
|
-
from pyaidrone.packet import *
|
|
11
|
-
from pyaidrone.deflib import *
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class AIDrone(Parse, Packet):
|
|
15
|
-
def __init__(self, receiveCallback = None):
|
|
16
|
-
self.serial = None
|
|
17
|
-
self.isThreadRun = False
|
|
18
|
-
self.parse = Parse(AIDRONE)
|
|
19
|
-
self.makepkt = Packet(AIDRONE)
|
|
20
|
-
self.receiveCallback = receiveCallback
|
|
21
|
-
self.makepkt.clearPacket()
|
|
22
|
-
self.posX = 0
|
|
23
|
-
self.posY = 0
|
|
24
|
-
self.rot = 0
|
|
25
|
-
# --- 영상 스트리밍 관련 기본값 ---
|
|
26
|
-
self.stream_host = "192.168.4.1"
|
|
27
|
-
self.stream_port = 80
|
|
28
|
-
self.stream_path = "/?action=stream"
|
|
29
|
-
self._cap = None
|
|
30
|
-
|
|
31
|
-
def receiveHandler(self):
|
|
32
|
-
while self.isThreadRun:
|
|
33
|
-
readData = self.serial.read(self.serial.in_waiting or 1)
|
|
34
|
-
packet = self.parse.packetArrange(readData)
|
|
35
|
-
if not eq(packet, "None"):
|
|
36
|
-
if self.receiveCallback != None:
|
|
37
|
-
self.receiveCallback(packet)
|
|
38
|
-
self.serial.write(self.makepkt.getPacket())
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def Open(self, portName = "None"):
|
|
42
|
-
if eq(portName, "None"):
|
|
43
|
-
nodes = comports()
|
|
44
|
-
for node in nodes:
|
|
45
|
-
if "CH340" in node.description:
|
|
46
|
-
portName = node.device
|
|
47
|
-
|
|
48
|
-
if eq(portName, "None"):
|
|
49
|
-
print("Can't find Serial Port")
|
|
50
|
-
exit()
|
|
51
|
-
return False
|
|
52
|
-
try:
|
|
53
|
-
self.serial = serial.Serial(port=portName, baudrate=19200, timeout=1)
|
|
54
|
-
if self.serial.isOpen():
|
|
55
|
-
self.isThreadRun = True
|
|
56
|
-
self.thread = Thread(target=self.receiveHandler, args=(), daemon=True)
|
|
57
|
-
self.thread.start()
|
|
58
|
-
print("Connected to", portName)
|
|
59
|
-
return True
|
|
60
|
-
else:
|
|
61
|
-
print("Can't open " + portName)
|
|
62
|
-
exit()
|
|
63
|
-
return False
|
|
64
|
-
except:
|
|
65
|
-
print("Can't open " + portName)
|
|
66
|
-
exit()
|
|
67
|
-
return False
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def Close(self):
|
|
71
|
-
self.isThreadRun = False
|
|
72
|
-
sleep(0.2)
|
|
73
|
-
pkt = self.makepkt.getPacket()
|
|
74
|
-
if (pkt[15]&0x80) == 0x80:
|
|
75
|
-
self.makepkt.clearPacket()
|
|
76
|
-
self.setOption(0x8000)
|
|
77
|
-
self.serial.write(self.makepkt.getPacket())
|
|
78
|
-
sleep(0.2)
|
|
79
|
-
self.serial.write(self.makepkt.clearPacket())
|
|
80
|
-
sleep(0.2)
|
|
81
|
-
if self.serial != None:
|
|
82
|
-
if self.serial.isOpen() == True:
|
|
83
|
-
self.serial.close()
|
|
84
|
-
|
|
85
|
-
def setOption(self, option):
|
|
86
|
-
data = option.to_bytes(2, byteorder="little", signed=False)
|
|
87
|
-
self.makepkt.makePacket(14, data)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def takeoff(self):
|
|
91
|
-
alt = 70
|
|
92
|
-
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
93
|
-
self.makepkt.makePacket(12, data)
|
|
94
|
-
alt = 0x2F
|
|
95
|
-
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
96
|
-
self.setOption(0x2F)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def landing(self):
|
|
100
|
-
alt = 0
|
|
101
|
-
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
102
|
-
self.makepkt.makePacket(12, data)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def altitude(self, alt):
|
|
106
|
-
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
107
|
-
self.makepkt.makePacket(12, data)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def velocity(self, dir=0, vel=100):
|
|
111
|
-
if dir > 3:
|
|
112
|
-
return
|
|
113
|
-
if dir==1 or dir==3:
|
|
114
|
-
vel *= -1;
|
|
115
|
-
data = vel.to_bytes(2, byteorder="little", signed=True)
|
|
116
|
-
if dir==0 or dir==1:
|
|
117
|
-
self.makepkt.makePacket(8, data)
|
|
118
|
-
else:
|
|
119
|
-
self.makepkt.makePacket(6, data)
|
|
120
|
-
self.setOption(0x0F)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def move(self, dir=0, dist=100):
|
|
124
|
-
if dir > 3:
|
|
125
|
-
return
|
|
126
|
-
if dir==1 or dir==3:
|
|
127
|
-
dist *= -1;
|
|
128
|
-
if dir==0 or dir==1:
|
|
129
|
-
self.posX += dist
|
|
130
|
-
data = self.posX.to_bytes(2, byteorder="little", signed=True)
|
|
131
|
-
self.makepkt.makePacket(8, data)
|
|
132
|
-
else:
|
|
133
|
-
self.posY += dist
|
|
134
|
-
data = self.posY.to_bytes(2, byteorder="little", signed=True)
|
|
135
|
-
self.makepkt.makePacket(6, data)
|
|
136
|
-
self.setOption(0x2F)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def rotation(self, rot=90):
|
|
140
|
-
self.rot += rot
|
|
141
|
-
data = self.rot.to_bytes(2, byteorder="little", signed=True)
|
|
142
|
-
self.makepkt.makePacket(10, data)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def motor(self, what, speed):
|
|
147
|
-
speed = DefLib.constrain(speed, 100, 0)
|
|
148
|
-
data = speed.to_bytes(2, byteorder="little", signed=True)
|
|
149
|
-
self.makepkt.makePacket(what*2+6, data)
|
|
150
|
-
self.setOption(0x8000)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def emergency(self):
|
|
154
|
-
self.setOption(0x00)
|
|
155
|
-
self.serial.write(self.makepkt.getPacket())
|
|
156
|
-
|
|
157
|
-
# aiDrone.py — AIDrone 클래스 안에 아래 3개 메서드 추가
|
|
158
|
-
def setStreamAddress(self, host: str, port: int = 80, path: str = "/?action=stream"):
|
|
159
|
-
"""MJPG-Streamer 주소 구성 요소를 저장합니다."""
|
|
160
|
-
if not isinstance(host, str) or len(host.strip()) == 0:
|
|
161
|
-
raise ValueError("host가 올바르지 않습니다.")
|
|
162
|
-
self.stream_host = host.strip()
|
|
163
|
-
self.stream_port = int(port)
|
|
164
|
-
if not path.startswith("/"):
|
|
165
|
-
path = "/" + path
|
|
166
|
-
self.stream_path = path
|
|
167
|
-
return self._build_stream_url()
|
|
168
|
-
|
|
169
|
-
def _build_stream_url(self):
|
|
170
|
-
"""내부 사용: 저장된 host/port/path로 URL 생성."""
|
|
171
|
-
if self.stream_port in (80, None):
|
|
172
|
-
return f"http://{self.stream_host}{self.stream_path}"
|
|
173
|
-
return f"http://{self.stream_host}:{self.stream_port}{self.stream_path}"
|
|
174
|
-
|
|
175
|
-
def streamon(self, host: str = None, port: int = None, path: str = None, return_url: bool = False):
|
|
176
|
-
"""
|
|
177
|
-
MJPG-Streamer 스트림을 엽니다.
|
|
178
|
-
- 인자를 주면 해당 값으로 주소를 덮어쓰고, 안 주면 저장된 값 사용.
|
|
179
|
-
- return_url=True 이면 URL 문자열만 반환(캡처는 열지 않음).
|
|
180
|
-
- 기본 URL 예: http://192.168.4.1/?action=stream
|
|
181
|
-
"""
|
|
182
|
-
if host is not None or port is not None or path is not None:
|
|
183
|
-
self.setStreamAddress(host or self.stream_host,
|
|
184
|
-
self.stream_port if port is None else port,
|
|
185
|
-
self.stream_path if path is None else path)
|
|
186
|
-
|
|
187
|
-
url = self._build_stream_url()
|
|
188
|
-
if return_url:
|
|
189
|
-
return url
|
|
190
|
-
|
|
191
|
-
# OpenCV는 여기서만 임포트(필요할 때만)
|
|
192
|
-
try:
|
|
193
|
-
import cv2 as cv
|
|
194
|
-
except Exception as e:
|
|
195
|
-
raise RuntimeError(f"OpenCV(cv2) 임포트 실패: {e}")
|
|
196
|
-
|
|
197
|
-
# 이전 cap이 열려 있으면 정리
|
|
198
|
-
self.streamoff()
|
|
199
|
-
|
|
200
|
-
self._cap = cv.VideoCapture(url)
|
|
201
|
-
if not self._cap.isOpened():
|
|
202
|
-
self._cap.release()
|
|
203
|
-
self._cap = None
|
|
204
|
-
raise RuntimeError(f"스트림 열기 실패: {url}")
|
|
205
|
-
return self._cap
|
|
206
|
-
|
|
207
|
-
def streamoff(self):
|
|
208
|
-
"""열려 있는 스트림을 닫습니다."""
|
|
209
|
-
if self._cap is not None:
|
|
210
|
-
try:
|
|
211
|
-
self._cap.release()
|
|
212
|
-
except Exception:
|
|
213
|
-
pass
|
|
214
|
-
self._cap = None
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
pyaidrone-2.1/pyaidrone/edu.py
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import cv2
|
|
2
|
-
import time
|
|
3
|
-
import numpy as np
|
|
4
|
-
from pyaidrone.aiDrone import AIDrone
|
|
5
|
-
from pyaidrone.vision_ai import TFLiteDetector, yolo_decode, draw_box_xywh, largest_contour, contour_centroid
|
|
6
|
-
from pyaidrone.deflib import *
|
|
7
|
-
|
|
8
|
-
class EduAIDrone:
|
|
9
|
-
"""
|
|
10
|
-
AI 교육을 위해 복잡한 기능을 단순화한 통합 API 클래스
|
|
11
|
-
"""
|
|
12
|
-
def __init__(self, port="COM3", model_path=None, labels_path=None):
|
|
13
|
-
# 드론 객체 및 기본 설정
|
|
14
|
-
self.aidrone = AIDrone()
|
|
15
|
-
self.port = port
|
|
16
|
-
self.detector = TFLiteDetector(model_path) if model_path else None
|
|
17
|
-
self.labels = []
|
|
18
|
-
if labels_path:
|
|
19
|
-
with open(labels_path, 'r', encoding='utf-8') as f:
|
|
20
|
-
self.labels = [line.strip() for line in f.readlines()]
|
|
21
|
-
|
|
22
|
-
self.cap = None
|
|
23
|
-
self.last_frame = None
|
|
24
|
-
self.height = 100 # 기본 유지 고도
|
|
25
|
-
self.stream_url = None
|
|
26
|
-
|
|
27
|
-
# --- 연결 및 영상 관리 ---
|
|
28
|
-
def connect(self):
|
|
29
|
-
"""드론 연결 및 초기 세팅"""
|
|
30
|
-
if self.aidrone.Open(self.port):
|
|
31
|
-
self.aidrone.setOption(0)
|
|
32
|
-
print(f"✅ 연결 성공: {self.port}")
|
|
33
|
-
return True
|
|
34
|
-
return False
|
|
35
|
-
|
|
36
|
-
def start_stream(self, url="http://192.168.4.1/?action=stream"):
|
|
37
|
-
self.stream_url = url # ← URL 저장
|
|
38
|
-
self.cap = cv2.VideoCapture(url)
|
|
39
|
-
|
|
40
|
-
if self.cap.isOpened():
|
|
41
|
-
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # ← 버퍼 설정
|
|
42
|
-
print(f"✅ 스트림 연결: {url}")
|
|
43
|
-
return True
|
|
44
|
-
else:
|
|
45
|
-
print(f"❌ 스트림 연결 실패: {url}")
|
|
46
|
-
return False
|
|
47
|
-
|
|
48
|
-
def update_screen(self, window_name="AI Drone Edu"):
|
|
49
|
-
"""안전한 프레임 업데이트"""
|
|
50
|
-
# 1. cap이 None인지 확인
|
|
51
|
-
if self.cap is None:
|
|
52
|
-
print("⚠️ 스트림이 연결되지 않았습니다")
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
# 2. AttributeError 처리
|
|
56
|
-
try:
|
|
57
|
-
ret, frame = self.cap.read()
|
|
58
|
-
except AttributeError:
|
|
59
|
-
print("❌ cap.read() 에러")
|
|
60
|
-
self.cap = None
|
|
61
|
-
return None
|
|
62
|
-
|
|
63
|
-
# 3. 프레임 검증
|
|
64
|
-
if not ret or frame is None:
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
# 4. 안전한 리사이즈
|
|
68
|
-
try:
|
|
69
|
-
if frame.size == 0: # ← 크기 체크 추가!
|
|
70
|
-
return None
|
|
71
|
-
|
|
72
|
-
self.last_frame = cv2.resize(frame, (640, 480))
|
|
73
|
-
return self.last_frame
|
|
74
|
-
except Exception as e:
|
|
75
|
-
print(f"⚠️ 리사이즈 에러: {e}")
|
|
76
|
-
return None
|
|
77
|
-
|
|
78
|
-
# --- 단순 제어 명령어 ---
|
|
79
|
-
def takeoff(self):
|
|
80
|
-
print("🚀 이륙합니다..."); self.aidrone.takeoff(); time.sleep(2)
|
|
81
|
-
|
|
82
|
-
def land(self):
|
|
83
|
-
print("🛬 착륙합니다..."); self.aidrone.landing()
|
|
84
|
-
|
|
85
|
-
def move(self, direction, speed=100):
|
|
86
|
-
"""방향: 'front', 'back', 'left', 'right'"""
|
|
87
|
-
dir_map = {'front': FRONT, 'back': BACK, 'right': RIGHT, 'left': LEFT}
|
|
88
|
-
if direction in dir_map:
|
|
89
|
-
self.aidrone.velocity(dir_map[direction], speed)
|
|
90
|
-
|
|
91
|
-
def set_height(self, cm):
|
|
92
|
-
"""고도 설정 (50~150cm 추천)"""
|
|
93
|
-
self.height = max(50, min(150, cm))
|
|
94
|
-
self.aidrone.altitude(self.height)
|
|
95
|
-
|
|
96
|
-
def turn(self, angle):
|
|
97
|
-
"""회전: 양수(우회전), 음수(좌회전)"""
|
|
98
|
-
self.aidrone.rotation(angle)
|
|
99
|
-
|
|
100
|
-
def stop(self):
|
|
101
|
-
"""모든 이동 정지 (호버링)"""
|
|
102
|
-
self.aidrone.velocity(FRONT, 0)
|
|
103
|
-
self.aidrone.velocity(RIGHT, 0)
|
|
104
|
-
|
|
105
|
-
# --- AI 인지 기능 ---
|
|
106
|
-
def find_color(self, color="red"):
|
|
107
|
-
"""색상을 찾아 화면에 표시하고 좌표 반환"""
|
|
108
|
-
if self.last_frame is None: return None
|
|
109
|
-
hsv = cv2.cvtColor(self.last_frame, cv2.COLOR_BGR2HSV)
|
|
110
|
-
|
|
111
|
-
# 교육용 프리셋
|
|
112
|
-
ranges = {
|
|
113
|
-
"red": [(0, 150, 50), (10, 255, 255)],
|
|
114
|
-
"blue": [(100, 150, 50), (140, 255, 255)],
|
|
115
|
-
"green": [(40, 100, 50), (80, 255, 255)]
|
|
116
|
-
}
|
|
117
|
-
low, high = ranges.get(color, ranges["red"])
|
|
118
|
-
mask = cv2.inRange(hsv, np.array(low), np.array(high))
|
|
119
|
-
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
120
|
-
big_c = largest_contour(contours)
|
|
121
|
-
|
|
122
|
-
if big_c is not None:
|
|
123
|
-
cv2.drawContours(self.last_frame, [big_c], -1, (0, 255, 0), 2)
|
|
124
|
-
return contour_centroid(big_c)
|
|
125
|
-
return None
|
|
126
|
-
|
|
127
|
-
def find_object(self, target_name, threshold=0.5):
|
|
128
|
-
"""YOLO 모델로 사물 찾기 (통합된 vision_ai 로직 활용)"""
|
|
129
|
-
if not self.detector or self.last_frame is None: return None
|
|
130
|
-
|
|
131
|
-
# 1. vision_ai에 내장된 정식 yolo_decode를 사용하여 추론
|
|
132
|
-
results = self.detector.infer(self.last_frame, yolo_decode)
|
|
133
|
-
|
|
134
|
-
# 2. 결과 분석
|
|
135
|
-
for res in results:
|
|
136
|
-
# 라벨 리스트가 있다면 이름으로 비교, 없으면 ID로 비교
|
|
137
|
-
name = self.labels[res.class_id] if self.labels else f"ID:{res.class_id}"
|
|
138
|
-
|
|
139
|
-
if name == target_name and res.score > threshold:
|
|
140
|
-
# 3. 화면에 인식 결과 그리기 (xyxy -> xywh 변환 후 그리기)
|
|
141
|
-
x1, y1, x2, y2 = res.box
|
|
142
|
-
w, h = x2 - x1, y2 - y1
|
|
143
|
-
draw_box_xywh(self.last_frame, (x1, y1, w, h), label=f"{name} {int(res.score*100)}%")
|
|
144
|
-
|
|
145
|
-
# 4. 물체의 중심 좌표 반환 (학생들이 제어에 사용하기 위함)
|
|
146
|
-
return ((x1 + x2) / 2, (y1 + y2) / 2)
|
|
147
|
-
|
|
148
|
-
return None
|
|
149
|
-
|
|
150
|
-
def read_qr(self):
|
|
151
|
-
"""QR 코드 텍스트 읽기"""
|
|
152
|
-
if self.last_frame is None: return None
|
|
153
|
-
data, _, _ = cv2.QRCodeDetector().detectAndDecode(self.last_frame)
|
|
154
|
-
return data if data else None
|
|
155
|
-
|
|
156
|
-
# 오차값을 바탕으로 드론이 알아서 회전하고 고도를 조절
|
|
157
|
-
def follow_target(self, error_x, error_y):
|
|
158
|
-
"""오차값을 보고 드론을 자동으로 회전 및 고도 조절"""
|
|
159
|
-
# 1. 좌우 회전 (Yaw) 제어
|
|
160
|
-
yaw = int(error_x * 0.15)
|
|
161
|
-
self.aidrone.rotation(yaw)
|
|
162
|
-
|
|
163
|
-
# 2. 상하 고도 (Throttle) 제어
|
|
164
|
-
throttle_change = int(error_y * 0.2)
|
|
165
|
-
self.height = max(50, min(150, self.height + throttle_change))
|
|
166
|
-
self.aidrone.altitude(self.height)
|
|
167
|
-
|
|
168
|
-
# 조건이 맞으면 사진을 저장.
|
|
169
|
-
def save_image(self, frame, folder="captured_images"):
|
|
170
|
-
"""사진을 저장하고 기존 폴더가 없으면 생성"""
|
|
171
|
-
import os
|
|
172
|
-
if not os.path.exists(folder): os.makedirs(folder)
|
|
173
|
-
|
|
174
|
-
timestamp = time.strftime("%H%M%S")
|
|
175
|
-
filename = f"{folder}/target_{timestamp}.jpg"
|
|
176
|
-
cv2.imwrite(filename, frame)
|
|
177
|
-
|
|
178
|
-
def reconnect_stream(self):
|
|
179
|
-
"""스트림 재연결"""
|
|
180
|
-
if self.stream_url is None:
|
|
181
|
-
return False
|
|
182
|
-
|
|
183
|
-
if self.cap:
|
|
184
|
-
self.cap.release()
|
|
185
|
-
self.cap = None
|
|
186
|
-
|
|
187
|
-
time.sleep(1)
|
|
188
|
-
return self.start_stream(self.stream_url)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|