동그라미 6주차

HanGu·2025년 11월 1일

물리적 장치 확인 (Host)

# 웹캠 장치 확인
ls -l /dev/video*

Workspace 설정 (Host)

mkdir -p ~/wsStudyROS   # 워크스페이스를 호스트에 보존

Docker Container (Host)

xhost +local:docker

docker run -it \
  --name ros2_humble_dev \
  --privileged \
  --gpus all \
  --network host \
  -e DISPLAY=$DISPLAY \
  -e QT_X11_NO_MITSHM=1 \
  -v /tmp/.X11-unix:/tmp/.X11-unix:ro \
  -v $HOME/wsStudyROS:/wsStudyROS \
  --device=/dev/video0:/dev/video0 \
  --ipc=host \
  --shm-size=2g \
  -w /wsStudyROS \
  ros:humble \
  bash

편집기 설치(Docker Container)

install nano

각자 Computer 환경에 맞게 설정

(Docker Container)

(RTX3070, Nvidia 535, Cu11.8 기준)

# 1. 패키지 리스트 업데이트
apt-get update

# 2. ROS 2 유틸리티, OpenCV, pip 설치
apt-get install -y python3-pip wget \
                   ros-humble-cv-bridge \
                   ros-humble-rqt-image-view \
                   ros-humble-rqt-graph \
                   python3-opencv

# 3. (중요) CUDA 11.8 툴킷 설치 (드라이버가 아닌 라이브러리)
# (이 작업은 PyTorch가 GPU를 사용하기 위해 컨테이너 내부에 꼭 필요합니다)
wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda-repo-ubuntu2204-11-8-local_11.8.0-520.61.05-1_amd64.deb
dpkg -i cuda-repo-ubuntu2204-11-8-local_11.8.0-520.61.05-1_amd64.deb
cp /var/cuda-repo-ubuntu2204-11-8-local/cuda-*-keyring.gpg /usr/share/keyrings/
apt-get update
apt-get install -y cuda-toolkit-11-8

# 4. PyTorch (CUDA 11.8용) 및 YOLOv8 설치
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip3 install ultralytics

# 5. CUDA 환경 변수 설정
echo 'export PATH=/usr/local/cuda-11.8/bin${PATH:+:${PATH}}' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda-11.8/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}' >> ~/.bashrc
source ~/.bashrc

# 6. (선택) 설치 확인
# python3 verify_pytorch_gpu.py
# (실행하여 PyTorch와 nvcc가 모두 잡히는지 확인)

verify_pytorch_gpu.py

(Docker Container)

(버전 확인 용)

import sys
import subprocess
import platform

# --- 1. 시스템 환경 정보 확인 (NVIDIA 드라이버 및 CUDA 툴킷) ---
print("="*50)
print("1. 시스템 환경 정보 확인")
print("="*50)

# Ubuntu 버전 확인
try:
    # lsb_release -d : 배포판 이름과 버전 (예: Description: Ubuntu 22.04.3 LTS)
    result = subprocess.run(
        ['lsb_release', '-d'], 
        capture_output=True, text=True, check=True, encoding='utf-8'
    )
    # "Description:" 부분 제거하고 깔끔하게 출력
    print(f"💻 Ubuntu 버전: {result.stdout.strip().split(':')[-1].strip()}")
except Exception as e:
    print(f"💻 Ubuntu 버전 확인 실패: {e}")
    print("   (참고: 'lsb_release' 명령어가 없거나 Linux가 아닐 수 있습니다.)")

# nvidia-smi (NVIDIA 드라이버) 확인
try:
    # nvidia-smi 실행
    result = subprocess.run(
        ['nvidia-smi'], 
        capture_output=True, text=True, check=True, encoding='utf-8'
    )
    print("\n✅ nvidia-smi (NVIDIA 드라이버)가 성공적으로 실행되었습니다.")
    print("-" * 50)
    # nvidia-smi 출력 결과 전체를 보여줍니다.
    print(result.stdout)
    print("-" * 50)
except FileNotFoundError:
    print("\n❌ [오류] 'nvidia-smi' 명령어를 찾을 수 없습니다.")
    print("   NVIDIA 드라이버가 설치되지 않았거나 PATH에 잡혀있지 않습니다.")
except subprocess.CalledProcessError as e:
    print(f"\n❌ [오류] 'nvidia-smi' 실행 실패:")
    print(e.stderr)

# nvcc -V (CUDA Toolkit) 확인
try:
    # nvcc -V 실행
    result = subprocess.run(
        ['nvcc', '-V'], 
        capture_output=True, text=True, check=True, encoding='utf-8'
    )
    print("\n✅ nvcc -V (CUDA Toolkit)가 성공적으로 실행되었습니다.")
    # nvcc -V 출력 결과 중 "release"가 포함된 라인 (예: Cuda compilation tools, release 11.8, V11.8.89)
    for line in result.stdout.split('\n'):
        if 'release' in line:
            print(f"   {line.strip()}")
            break
except FileNotFoundError:
    print("\n❌ [오류] 'nvcc' 명령어를 찾을 수 없습니다.")
    print("   CUDA Toolkit이 설치되지 않았거나 PATH에 잡혀있지 않습니다.")
    print("   (참고: 3단계 환경 변수 설정을 완료했는지, 터미널을 껐다 켰는지 확인하세요.)")
except subprocess.CalledProcessError as e:
    print(f"\n❌ [오류] 'nvcc -V' 실행 실패:")
    print(e.stderr)


# --- 2. PyTorch GPU 검증 ---
print("\n" + "="*50)
print("2. PyTorch GPU 검증")
print("="*50)

try:
    import torch
    
    print(f"✅ PyTorch가 성공적으로 임포트되었습니다.")
    print(f"   - PyTorch 버전: {torch.__version__}")

    # PyTorch가 인식하는 CUDA 버전 (컴파일 시 사용된 버전)
    print(f"   - PyTorch가 컴파일된 CUDA 버전: {torch.version.cuda}")

    # GPU 사용 가능 여부 확인
    is_available = torch.cuda.is_available()
    
    if is_available:
        print("\n🎉 \033[92m[성공] PyTorch가 GPU를 성공적으로 인식했습니다.\033[0m")
        
        # GPU 장치 수
        print(f"   - 사용 가능한 GPU 개수: {torch.cuda.device_count()}")
        
        # 현재 GPU 장치 이름 (RTX 3070이 나와야 함)
        current_device = torch.cuda.current_device()
        print(f"   - 현재 GPU 장치 인덱스: {current_device}")
        print(f"   - 현재 GPU 장치 이름: {torch.cuda.get_device_name(current_device)}")
        
        # 최종 검증: GPU에서 텐서 연산 수행
        print("\n   [최종 검증] GPU에서 텐서 연산을 시도합니다...")
        try:
            # GPU 장치 정의
            device = torch.device('cuda')
            
            # CPU에서 텐서 생성
            x = torch.tensor([1.0, 2.0, 3.0])
            print(f"     1. CPU 텐서 생성: {x.device}")
            
            # 텐서를 GPU로 이동
            x_gpu = x.to(device)
            print(f"     2. GPU로 텐서 이동: {x_gpu.device}")
            
            # GPU에서 텐서 연산 수행
            y_gpu = x_gpu + 10.0
            print(f"     3. GPU에서 연산 수행 (결과): {y_gpu}")
            
            print("\n   \033[92m[최종 성공] GPU에서의 텐서 생성, 이동, 연산이 모두 확인되었습니다.\033[0m")
            
        except Exception as e:
            print(f"\n   \033[91m[오류] GPU 연산 중 문제가 발생했습니다: {e}\033[0m")
            
    else:
        print("\n❌ \033[91m[실패] PyTorch가 GPU를 인식하지 못합니다. (torch.cuda.is_available() == False)\033[0m")
        print("   [확인 사항]")
        print("   1. 'nvidia-smi'가 정상 작동하는지 확인하세요.")
        print(f"   2. 설치된 PyTorch가 GPU 버(cu118)가 맞는지 확인하세요. (현재: {torch.version.cuda})")
        print("      (만약 'cpu'로 나온다면, PyTorch를 GPU 버전으로 다시 설치해야 합니다.)")

except ImportError:
    print("\n❌ \Code [오류] PyTorch가 설치되어 있지 않습니다. ('import torch' 실패)")
    print("   CUDA 11.8 버전에 맞는 PyTorch를 설치해주세요.")
    print("   설치 명령어 예시:")
    print("   pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118")

except Exception as e:
    print(f"\n❌ [알 수 없는 오류] 스크립트 실행 중 오류가 발생했습니다: {e}")

print("\n" + "="*50)
print("검증이 완료되었습니다.")
print("="*50)
python3 verify_pytorch_gpu.py

# 1. 워크스페이스 및 src 디렉터리 생성
mkdir -p /wsStudyROS/ros2_ws/src
cd /wsStudyROS/ros2_ws/src

# 2. 커스텀 메시지 패키지 생성
ros2 pkg create --build-type ament_cmake yolo_msgs

# 3. YOLO 노드 패키지 생성
ros2 pkg create --build-type ament_python yolo_detector
# YOLO 검출 결과(클래스, 확률, 좌표)를 담을 메시지를 정의합니다.
# 1. msg 디렉터리 생성
mkdir /wsStudyROS/ros2_ws/src/yolo_msgs/msg
nano /wsStudyROS/ros2_ws/src/yolo_msgs/msg/BoundingBox.msg
# YOLOv8 검출 결과 (개별 박스)
string class_id
float32 probability
int64 x_min
int64 y_min
int64 x_max
int64 y_max

Ctrl + O > Enter > Ctrl + x

nano /wsStudyROS/ros2_ws/src/yolo_msgs/msg/BoundingBoxes.msg
# BoundingBox 메시지의 배열
std_msgs/Header header
yolo_msgs/BoundingBox[] detections

Ctrl + O > Enter > Ctrl + x

nano /wsStudyROS/ros2_ws/src/yolo_msgs/package.xml

기존의 것을 아래로 교체

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>yolo_msgs</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="hglab99@gmail.com">root</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  **<build_depend>rosidl_default_generators</build_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  <member_of_group>rosidl_interface_packages</member_of_group>
  <depend>std_msgs</depend>**
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Ctrl + O > Enter > Ctrl + x

nano /wsStudyROS/ros2_ws/src/yolo_msgs/CMakeLists.txt

기존의 것을 아래로 교체

cmake_minimum_required(VERSION 3.8)
project(yolo_msgs)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/BoundingBox.msg"
  "msg/BoundingBoxes.msg"
  DEPENDENCIES std_msgs
)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

install(
  DIRECTORY "msg"
  DESTINATION "share/${PROJECT_NAME}/"
)
ament_package()
mkdir /wsStudyROS/ros2_ws/src/yolo_detector/yolo_detector
nano /wsStudyROS/ros2_ws/src/yolo_detector/yolo_detector/webcam_yolo_pub.py
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
from ultralytics import YOLO

# 1. 커스텀 메시지 임포트
from yolo_msgs.msg import BoundingBox, BoundingBoxes

class YoloPublisher(Node):
    def __init__(self):
        super().__init__('yolo_publisher_node')
        
        # 2. YOLO 모델 로드 (v8n이 가장 가볍고 빠름)
        # GPU가 있으면 자동으로 CUDA를 사용합니다.
        self.model = YOLO('yolov8n.pt')
        self.get_logger().info('YOLOv8 model loaded successfully.')

        # 3. OpenCV 웹캠 설정 (컨테이너에 /dev/video0가 연결되어 있어야 함)
        self.cap = cv2.VideoCapture(0)
        if not self.cap.isOpened():
            self.get_logger().error('Failed to open webcam (device 0).')
            rclpy.shutdown()
        
        # 4. ROS 2 퍼블리셔 및 CvBridge 생성
        self.annotated_image_pub = self.create_publisher(Image, '/video/image_annotated', 10)
        self.detection_pub = self.create_publisher(BoundingBoxes, '/yolo/detections', 10)
        self.bridge = CvBridge()

        # 5. 메인 루프 (초당 30회)
        self.timer = self.create_timer(1.0 / 30.0, self.timer_callback)

    def timer_callback(self):
        ret, frame = self.cap.read()
        if not ret:
            self.get_logger().warn('Failed to read frame from webcam.')
            return

        # 6. YOLO 추론
        results = self.model(frame) 

        # 7. 검출 결과(BoundingBoxes) 메시지 생성
        detection_msg = BoundingBoxes()
        detection_msg.header.stamp = self.get_clock().now().to_msg()
        annotated_frame = frame.copy() # 원본 프레임 복사

        for result in results:
            boxes = result.boxes
            for box in boxes:
                b = BoundingBox()
                class_id_int = int(box.cls)
                b.class_id = self.model.names[class_id_int]
                b.probability = float(box.conf)
                coords = box.xyxy[0].cpu().numpy().astype(int)
				b.x_min, b.y_min, b.x_max, b.y_max = map(int, coords)
                
                detection_msg.detections.append(b)

                # [목표 1] 실시간 영상에 바운딩 박스 그리기
                cv2.rectangle(annotated_frame, (b.x_min, b.y_min), (b.x_max, b.y_max), (0, 255, 0), 2)
                cv2.putText(annotated_frame, f'{b.class_id}: {b.probability:.2f}',
                            (b.x_min, b.y_min - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

        # 8. 메시지 퍼블리시
        # [목표 1] 주석이 달린 이미지 퍼블리시
        self.annotated_image_pub.publish(self.bridge.cv2_to_imgmsg(annotated_frame, "bgr8"))
        
        # [목표 3] 검출 결과 퍼블리시
        self.detection_pub.publish(detection_msg)
        self.get_logger().info(f'Published {len(detection_msg.detections)} detections.')

def main(args=None):
    rclpy.init(args=args)
    yolo_publisher = YoloPublisher()
    rclpy.spin(yolo_publisher)
    yolo_publisher.cap.release()
    yolo_publisher.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()
nano /wsStudyROS/ros2_ws/src/yolo_detector/yolo_detector/detection_sub.py
import rclpy
from rclpy.node import Node
from yolo_msgs.msg import BoundingBoxes

class DetectionSubscriber(Node):
    def __init__(self):
        super().__init__('detection_subscriber_node')
        
        # 1. '/yolo/detections' 토픽을 구독
        self.subscription = self.create_subscription(
            BoundingBoxes,
            '/yolo/detections',
            self.listener_callback,
            10)
        self.get_logger().info('Detection subscriber node started. Waiting for data...')

    def listener_callback(self, msg):
        # 2. [목표 3] 콜백 함수: 수신한 데이터를 터미널에 출력
        self.get_logger().info(f'--- New Detections Received (Total: {len(msg.detections)}) ---')
        for detection in msg.detections:
            self.get_logger().info(
                f"  Class: {detection.class_id}, "
                f"Prob: {detection.probability:.2f}, "
                f"Box: [{detection.x_min}, {detection.y_min}, {detection.x_max}, {detection.y_max}]"
            )

def main(args=None):
    rclpy.init(args=args)
    detection_subscriber = DetectionSubscriber()
    rclpy.spin(detection_subscriber)
    detection_subscriber.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()
nano /wsStudyROS/ros2_ws/src/yolo_detector/package.xml

기존의 것을 아래로 교체

  GNU nano 6.2                                                                                              /wsStudyROS/ros2_ws/src/yolo_detector/package.xml                                                                                                        
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>yolo_detector</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="hglab99@gmail.com">root</maintainer>
  <license>TODO: License declaration</license>

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>
  <depend>rclpy</depend>
  <depend>sensor_msgs</depend>
  <depend>cv_bridge</depend>
  <depend>yolo_msgs</depend>


  <export>
    <build_type>ament_python</build_type>
  </export>
</package>
nano /wsStudyROS/ros2_ws/src/yolo_detector/setup.py

기존의 것을 아래로 교체

  GNU nano 6.2                                                                                                /wsStudyROS/ros2_ws/src/yolo_detector/setup.py *                                                                                                 M     
from setuptools import find_packages, setup

package_name = 'yolo_detector'

setup(
    name=package_name,
    version='0.0.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='root',
    maintainer_email='hglab99@gmail.com',
    description='TODO: Package description',
    license='TODO: License declaration',
    extras_require={
        'test': [
            'pytest',
        ],
    },
    entry_points={
        'console_scripts': [
            'yolo_publisher = yolo_detector.webcam_yolo_pub:main',
            'detection_subscriber = yolo_detector.detection_sub:main',
        ],
    },
)
cd /wsStudyROS/ros2_ws

# 1. (중요) 커스텀 메시지 패키지를 먼저 빌드
colcon build --packages-select yolo_msgs

# 2. (중요) 빌드된 메시지를 셸에 인식시킴
source install/setup.bash

# 3. 나머지 패키지(yolo_detector) 빌드
colcon build --packages-up-to yolo_detector

# 4. (중요) 셸에 최종 결과 인식
source install/setup.bash

터미널 4개 열기

터미널 1(기존)

# ROS 2 Humble 기본 환경 로드
source /opt/ros/humble/setup.bash

# 워크스페이스 인식 (필수)
source /wsStudyROS/ros2_ws/install/setup.bash

# Publisher 노드를 백그라운드(&)로 실행
ros2 run yolo_detector yolo_publisher

터미널 2(신규)

xhost +local:docker
docker exec -it ros2_humble_dev bash
# ROS 2 Humble 기본 환경 로드
source /opt/ros/humble/setup.bash

# 워크스페이스 인식 (필수)
source /wsStudyROS/ros2_ws/install/setup.bash

# Subscriber 노드를 포어그라운드로 실행
ros2 run yolo_detector detection_subscriber

넘파이 버전 오류 발생시
Docker Container 내부에서

pip3 install numpy==1.26.4

터미널 3(신규)

xhost +local:docker
docker exec -it ros2_humble_dev bash
# ROS 2 Humble 기본 환경 로드
source /opt/ros/humble/setup.bash

# 워크스페이스 인식
source /wsStudyROS/ros2_ws/install/setup.bash

# RQT 이미지 뷰 실행
rqt_image_view

만약 rqt_image 등과 같은 GUI 도구 미설치시

# 1. (선택) 패키지 리스트 업데이트
apt-get update

# 2. (필수) RQT 도구 설치 (image_view와 graph 둘 다 설치)
apt-get install -y ros-humble-rqt-image-view ros-humble-rqt-graph

그래도 안되면

apt-get install --reinstall ros-humble-rqt-image-view ros-humble-rqt-graph

hash -r
source /opt/ros/humble/setup.bash

그래도 안되면

터미널 4(신규)

xhost +local:docker
docker exec -it ros2_humble_dev bash
# ROS 2 Humble 기본 환경 로드
source /opt/ros/humble/setup.bash

# 워크스페이스 인식
source /wsStudyROS/ros2_ws/install/setup.bash

# RQT 그래프 실행
rqt_graph



0개의 댓글