|
|
참고 깃허브
(1) https://github.com/Glutamat42/Carla-Lane-Detection-Dataset-Generation
(3) https://github.com/carla-simulator/carla/releases/tag/0.9.10/
Carla Simulator 설치 및 환경 설정.
해당 깃허브 링크에서 원하는 버전의 Carla version 설치.
https://github.com/carla-simulator/carla/blob/master/Docs/download.md
차선을 찾는 기본 github(1)에서는 0.9.10 version의 carla 사용.
1. 해당 깃허브를 다운받은 이후 가상환경을 python 3.7 버전으로 생성.
2. carla에 사용되는 언리얼엔진 version 4 다운로드
3. requirements.txt 설치
carla에 대한 라이브러리는 공식이 없어 egg 혹은 wsl파일 형태로 라이브러리를 저장함.
egg파일의 위치는 WINDOWNOEDITOR/PythonAPI/carla/dist에 존재하며
[carla-0.9.11-py3.8-win-amd64.egg, carla-0.9.15-cp37-cp37m-win_amd64.whl, carla-0.9.15-py3.7-win-amd64.egg]
위처럼 carla버전과 python 버전에 대한 egg파일이 존재.
python 3.8에 대한 파일을 찾았으나 carla version이 0.9.11에 대한 파일이라 3.7로 python 버전을 유지.
4.
fast_lane_detection.py ( CARLA에서 실행하여 데이터를 수집. )
dataset_generator.py ( 수집된 데이터로 데이터세트 생성)
두 코드를 순차적으로 실행.
해당 코드를 실행하게 되면 해당 형태로 파일이 생성.
dataset 디렉토리에는 gt에 대한 json파일과 이미지들이 저장
Both, Left, None, Right에 대해서 폴더가 나뉘어져 있으나 클래스는 차선 하나로 동일.
해당 깃허브의 경우 Segmentation에 대한 코드가 아닌 각 라인을 따라가는 선에 대한 데이터셋만이 존재.
해당 방식으로 각 리스트에 각 차선을 표시하는 방법으로 기록 Segment에 대해서 데이터셋을 사용하려면 추가적인 후처리가 필요.
해당 환경에서 신호등 감지에 대한 코드의 초안 작성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | import os import cv2 import numpy as np import math import glob import sys try: sys.path.append(glob.glob('../carla/dist/carla-*%d.%d-%s.egg' % ( sys.version_info.major, sys.version_info.minor, 'win-amd64' if os.name == 'nt' else 'linux-x86_64'))[0]) except IndexError: pass import carla # === 사용자 설정 === OUTPUT_DIR = "dataset" FRAME_INTERVAL = 60 # 프레임 간격 (60프레임마다 저장) IMG_WIDTH = 1280 IMG_HEIGHT = 720 # Unknown은 운전자 시점에서 뒷면을 바라보는 신호등에 대한 객체 클래스 CLASS_MAP = {"Red": 0, "Yellow": 1, "Green": 2, "Unknown": 3} VISIBILITY_DOT_THRESHOLD = 0.4 # 시야각 임계값 (내적 값 0.4 미만은 Unknown 처리) TRAFFIC_LIGHT_SIZE = carla.Vector3D(x=0.5, y=1.0, z=1.2) # 신호등 3D 객체에 대한 크기값. # === 벡터 회전 함수 === def rotate_vector(vector, rotation): # 벡터를 주어진 회전(rotation)으로 변환하는 함수 # pitch, yaw, roll을 라디안으로 변환 pitch = math.radians(rotation.pitch) yaw = math.radians(rotation.yaw) roll = math.radians(rotation.roll) # 초기 벡터 값 저장 cx = vector.x cy = vector.y * math.cos(roll) - vector.z * math.sin(roll) # roll 회전 적용 cz = vector.y * math.sin(roll) + vector.z * math.cos(roll) # pitch 회전 적용 cx2 = cx * math.cos(pitch) + cz * math.sin(pitch) cy2 = cy cz2 = -cx * math.sin(pitch) + cz * math.cos(pitch) # yaw 회전 적용 x = cx2 * math.cos(yaw) - cy2 * math.sin(yaw) y = cx2 * math.sin(yaw) + cy2 * math.cos(yaw) z = cz2 return carla.Location(x, y, z) # 회전된 결과를 carla.Location 객체로 반환 # === Transform 역변환 === def inverse_transform(transform: carla.Transform) -> carla.Transform: # 주어진 Transform 객체의 역변환을 계산하는 함수 rot = transform.rotation # 원본 회전 정보 loc = transform.location # 원본 위치 정보 # 역회전 계산 (pitch, yaw, roll의 부호 반전) inv_rot = carla.Rotation( pitch=-rot.pitch, yaw=-rot.yaw, roll=-rot.roll ) # 역위치 계산 (위치의 부호 반전 후 회전 적용) inv_loc = carla.Location(x=-loc.x, y=-loc.y, z=-loc.z) inv_loc = rotate_vector(inv_loc, inv_rot) return carla.Transform(inv_loc, inv_rot) # 역변환된 Transform 객체 반환 # === 거리 시각화 (원형 선으로 표시) === def draw_distance_circle(camera, radius=50.0, step_deg=5): # 카메라를 중심으로 원형 거리선을 그리는 함수 cam_tf = camera.get_transform() # 카메라의 변환 정보 가져오기 cam_loc = cam_tf.location # 카메라 위치 points = [] # 원형 점들을 저장할 리스트 for deg in range(0, 360 + step_deg, step_deg): # 0도부터 360도까지 step_deg 간격으로 반복 angle_rad = math.radians(deg) # 각도를 라디안으로 변환 dx = math.cos(angle_rad) * radius # x 좌표 계산 dy = math.sin(angle_rad) * radius # y 좌표 계산 point = carla.Location(x=cam_loc.x + dx, y=cam_loc.y + dy, z=cam_loc.z) # 점 위치 계산 points.append(point) for i in range(len(points) - 1): # 인접한 점들 사이에 선 그리기 world.debug.draw_line(points[i], points[i + 1], thickness=0.05, color=carla.Color(255, 0, 255), life_time=0.3) # === 시야각 시각화 === def draw_fov_3d_debug(camera, fov_deg=50.0, vertical_fov_deg=90.0, length=15.0): # 카메라의 시야각을 3D로 시각화하는 함수 (수정 금지) cam_tf = camera.get_transform() # 카메라의 변환 정보 cam_loc = cam_tf.location # 카메라 위치 cam_rot = cam_tf.rotation # 카메라 회전 yaw = math.radians(cam_rot.yaw) # yaw 각도를 라디안으로 변환 pitch = 0.0 # 수직 pitch는 고정 (0도) half_hfov = math.radians(fov_deg / 2) # 수평 시야각의 절반 half_vfov = math.radians(vertical_fov_deg / 2) # 수직 시야각의 절반 directions = { 'left': (yaw - half_hfov, pitch), 'right': (yaw + half_hfov, pitch), 'up': (yaw, pitch + half_vfov), 'forward': (yaw, pitch) } colors = { 'left': carla.Color(0, 0, 255), 'right': carla.Color(0, 0, 255), 'up': carla.Color(255, 0, 0), 'forward': carla.Color(0, 255, 255) } for name, (yaw_i, pitch_i) in directions.items(): # 각 방향에 대해 선 그리기 dx = math.cos(pitch_i) * math.cos(yaw_i) # x 방향 벡터 dy = math.cos(pitch_i) * math.sin(yaw_i) # y 방향 벡터 dz = math.sin(pitch_i) # z 방향 벡터 endpoint = cam_loc + carla.Location(x=dx * length, y=dy * length, z=dz * length) # 끝점 계산 world.debug.draw_line(cam_loc, endpoint, thickness=0.05, color=colors[name], life_time=0.3) # === 3D BBox 시각화 === def draw_3d_bbox(location, transform, extent): # 3D 바운딩 박스를 시각화하는 함수 # location: 신호등 위치, transform: 신호등 변환, extent: 신호등 크기 base_z = location.z # 기준 z 좌표 (신호등 바닥) lamp_z = base_z + 3.0 # 램프 높이 조정 (기본 3m 오프셋) bbox_location = carla.Location(x=location.x, y=location.y, z=lamp_z) # 조정된 바운딩 박스 위치 corners = [ carla.Location(x=-extent.x, y=-extent.y, z=-extent.z), # 좌하후 carla.Location(x=extent.x, y=-extent.y, z=-extent.z), # 우하후 carla.Location(x=extent.x, y=extent.y, z=-extent.z), # 우상후 carla.Location(x=-extent.x, y=extent.y, z=-extent.z), # 좌상후 carla.Location(x=-extent.x, y=-extent.y, z=extent.z), # 좌하전 carla.Location(x=extent.x, y=-extent.y, z=extent.z), # 우하전 carla.Location(x=extent.x, y=extent.y, z=extent.z), # 우상전 carla.Location(x=-extent.x, y=extent.y, z=extent.z) # 좌상전 ] # 월드 좌표로 변환 world_corners = [transform.transform(corner + carla.Location(z=lamp_z - location.z)) for corner in corners] edges = [(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)] # 12개의 모서리 쌍 for start, end in edges: # 각 모서리 쌍에 대해 선 그리기 world.debug.draw_line(world_corners[start], world_corners[end], thickness=0.05, color=carla.Color(0, 255, 255), life_time=0.3) # === 2D YOLO 데이터셋 저장 === def save_yolo_annotation(image, bboxes, index): # 2D YOLO 형식으로 이미지와 바운딩 박스 데이터를 저장하는 함수 # image: 입력 이미지, bboxes: 바운딩 박스 리스트, index: 파일 인덱스 os.makedirs(os.path.join(OUTPUT_DIR, "images"), exist_ok=True) # images 디렉토리 생성 os.makedirs(os.path.join(OUTPUT_DIR, "labels"), exist_ok=True) # labels 디렉토리 생성 img_path = os.path.join(OUTPUT_DIR, "images", f"{index:06d}.jpg") # 이미지 파일 경로 label_path = os.path.join(OUTPUT_DIR, "labels", f"{index:06d}.txt") # 라벨 파일 경로 image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) # RGB를 BGR로 변환 (OpenCV 형식) cv2.imwrite(img_path, image) # 이미지 저장 with open(label_path, 'w') as f: # 라벨 파일 열기 for cls_id, x, y, w, h in bboxes: # 각 바운딩 박스에 대해 # YOLO 형식으로 기록 (클래스 ID, 중심 x, 중심 y, 너비, 높이) f.write(f"{cls_id} {x:.6f} {y:.6f} {w:.6f} {h:.6f}\n") # === 개별 신호등 필터링, 후방 제외 및 YOLO용 BBox 생성 === def process_traffic_lights(camera, image, index): # 신호등을 필터링하고 후방을 제외하며 YOLO용 2D 바운딩 박스를 생성하는 함수 cam_transform = camera.get_transform() # 카메라 변환 정보 cam_location = cam_transform.location # 카메라 위치 cam_forward = cam_transform.get_forward_vector() # 카메라 전방 벡터 cam_inv = inverse_transform(cam_transform) # 카메라 역변환 focal = IMG_WIDTH / (2 * np.tan(np.radians(90) / 2)) # 카메라 초점 거리 계산 (90도 FOV 기준) cam_intrinsics = np.array([ [focal, 0, IMG_WIDTH / 2], [0, focal, IMG_HEIGHT / 2], [0, 0, 1] ]) traffic_lights = world.get_actors().filter("traffic.traffic_light") # 모든 신호등 액터 가져오기 bboxes = [] # 바운딩 박스 리스트 for light in traffic_lights: light_location = light.get_location() # 신호등 위치 distance = light_location.distance(cam_location) # 카메라와의 거리 # 50m 이상 거리는 무시 (시야각 내에 존재하나 건물 및 너무 먼 거리에 대해선 보이지 않으므로) if distance > 50.0: continue to_light = light_location - cam_location # 카메라에서 신호등까지의 벡터 to_light = to_light.make_unit_vector() # 단위 벡터로 정규화 light_forward = light.get_transform().get_forward_vector() # 신호등 전방 벡터 # 내적 계산 dot = light_forward.x * (-to_light.x) + light_forward.y * (-to_light.y) + light_forward.z * (-to_light.z) if dot < VISIBILITY_DOT_THRESHOLD: # 시야각이 임계값 미만이면 cls_id = CLASS_MAP["Unknown"] # Unknown 클래스 else: state = light.get_state() # 신호등 상태 if state == carla.TrafficLightState.Red: # 상태에 따라 클래스 지정 cls_id = CLASS_MAP["Red"] elif state == carla.TrafficLightState.Yellow: cls_id = CLASS_MAP["Yellow"] elif state == carla.TrafficLightState.Green: cls_id = CLASS_MAP["Green"] else: continue # 수동으로 바운딩 박스 정의 및 시각화 draw_3d_bbox(light_location, light.get_transform(), TRAFFIC_LIGHT_SIZE) # 램프 높이에 맞춘 정점 조정 base_z = light_location.z # 기준 z 좌표 lamp_z = base_z + 3.0 # 램프 높이 오프셋 적용(신호등 BBox가 땅에 위치) verts = [ carla.Location(x=-TRAFFIC_LIGHT_SIZE.x, y=0, z=lamp_z), # 왼쪽 정점 carla.Location(x=TRAFFIC_LIGHT_SIZE.x, y=0, z=lamp_z) # 오른쪽 정점 ] world_verts = [light.get_transform().transform(v) for v in verts] # 월드 좌표로 변환 image_points = [] # 이미지 좌표 저장 리스트 for v in world_verts: # 각 정점에 대해 loc = cam_inv.transform(v) # 카메라 좌표계로 변환 if loc.z <= 0: # z가 0 이하이면 무시 (카메라 뒤) continue point = np.array([loc.x / loc.z, loc.y / loc.z, 1]) # 정규화된 좌표 pixel = cam_intrinsics @ point # 내부 행렬로 픽셀 좌표 계산 x, y = int(pixel[0]), int(pixel[1]) # 정수로 변환 image_points.append((x, y)) if len(image_points) != 2: # 두 점이 계산되지 않으면 무시 continue x1, y1 = image_points[0] # 첫 번째 점 x2, y2 = image_points[1] # 두 번째 점 x_min = max(0, min(x1, x2)) # 최소 x 좌표 y_min = max(0, min(y1, y2)) # 최소 y 좌표 x_max = min(IMG_WIDTH, max(x1, x2)) # 최대 x 좌표 y_max = min(IMG_HEIGHT, max(y1, y2)) # 최대 y 좌표 # 너비나 높이가 3px 미만이면 무시 (분별이 안가는 액터) if x_max - x_min < 3 or y_max - y_min < 3: continue x_center = (x_min + x_max) / 2 / IMG_WIDTH # 중심 x (정규화) y_center = (y_min + y_max) / 2 / IMG_HEIGHT # 중심 y (정규화) w = (x_max - x_min) / IMG_WIDTH # 너비 (정규화) h = (y_max - y_min) / IMG_HEIGHT # 높이 (정규화) bboxes.append((cls_id, x_center, y_center, w, h)) # 바운딩 박스 리스트에 추가 if bboxes: # 바운딩 박스가 있으면 save_yolo_annotation(image, bboxes, index) # YOLO 데이터 저장 print(f"[{index:06d}] Saved {len(bboxes)} bboxes") # 저장된 개수 출력 # === 메인 실행부 === client = carla.Client('localhost', 2000) # CARLA 클라이언트 생성 (포트 2000) client.set_timeout(5.0) world = client.get_world() blueprint_library = world.get_blueprint_library() # manual_control.py 코드로 ego 차량을 미리 생성해 두어야 함. vehicle = None # ego 차량 변수 for actor in world.get_actors().filter('vehicle.*'): # 모든 차량 액터 탐색 # 'hero' 역할 이름 확인 (차량이 해당 객체가 아니라면 차량 변경) if actor.attributes.get('role_name') == 'hero': vehicle = actor # ego 차량 설정 break if vehicle is None: raise RuntimeError("Ego vehicle with role_name 'hero' not found.") # ego 차량 없으면 오류 camera_bp = blueprint_library.find('sensor.camera.rgb') # RGB 카메라 블루프린트 camera_bp.set_attribute('image_size_x', str(IMG_WIDTH)) # 이미지 너비 설정 camera_bp.set_attribute('image_size_y', str(IMG_HEIGHT)) # 이미지 높이 설정 camera_bp.set_attribute('fov', '90') # 시야각 90도 설정 # 카메라 위치 및 회전 설정 camera_transform = carla.Transform(carla.Location(x=0.5, z=1.6), carla.Rotation(pitch=-15)) # 카메라 생성 및 차량에 부착 camera = world.spawn_actor(camera_bp, camera_transform, attach_to=vehicle) frame_count = 0 # 프레임 카운터 save_index = 0 # 저장 인덱스 def camera_callback(image): # 카메라 콜백 함수 (프레임마다 호출) global frame_count, save_index frame_count += 1 # 프레임 카운터 증가 if frame_count % FRAME_INTERVAL != 0: # 지정된 간격에 도달하지 않으면 return # 이미지 데이터 변환 array = np.frombuffer(image.raw_data, dtype=np.uint8).reshape((image.height, image.width, 4))[:, :, :3] draw_fov_3d_debug(camera) # 시야각 시각화 draw_distance_circle(camera) # 거리 원 시각화 process_traffic_lights(camera, array, save_index) # 신호등 처리 save_index += 1 # 인덱스 증가 camera.listen(camera_callback) # 카메라 콜백 등록 try: while True: world.wait_for_tick() # 월드 틱 대기 except KeyboardInterrupt: camera.stop() # 카메라 중지 camera.destroy() # 카메라 제거 print("Stopped.") # 종료 메시지 | cs |
1. 서버 열기. (CarlaUE4.exe 파일 실행.)
2. PythonAPI/examples/manual_control.py 실행하여 ego차량 소환
3. traffic_light_gen_dataset.py파일 실행(위 코드)
운전자 시점에서 카메라 시야각 표시되어있는 이미지.(서버에서도 보이며 manual_control로 실행한 윈도우에서도 확인 가능.)
신호등 객체를 인식을 하나 하나의 그룹에 속해있는 램프들을 하나로 묶어서 BBox를 표현하고 좌표 변환에 대해서 정상적으로 이루어지지 않는 모습도 확인(연두색 3D BBox)
신호등 객체에 대한 BBox 구현 중의 이미지.
1번째 이미지는 신호등 기둥을 하나의 그룹으로 인식하는데 해당 그룹에 대해 BBox를 구현했을 때의 모습
2번째 이미지와 3번째 이미지 역시 동일. (원형 내에 들어올 때{50M로 제한} 신호등을 인식하는 모습)
주요 변수
pitch : 사람의 고개를 위 아래로 흔드는 각도.
yaw : 사람의 고개를 바닥에 수평으로 좌우로 흔드는 각도
roll : 사람이 고개를 바닥에 수직으로 좌우로 흔드는 각도.
fov : 시야각(Field of View)
코드 내에서 Horizontal FOV와 Vertical FOV로 수평 시야각, 수직 시야각이 따로 계산된다.
(yaw에 대해서 +half_hfov, -half_hfov로 좌우를 계산하고 pitch에 대해 +half_vfow, -half_vfow로 상하를 계산.)
이후 작업해야할 목록
1. 신호등 객체에 대해서 좌표변환에 대한 정확한 정보 입력.
2. 2D로 운전자 시점에서 BBox좌표를 변환하여 데이터셋 생성.
3. 차선에 대해서 Segmentation 작업이 가능하도록 코드 수정.
