피지컬 AI를 글로만 읽으면 남는 게 없습니다. 카메라가 사람을 찾고, 그 결과로 모터가 실제로 도는 것을 한 번 보면 그 다음부터는 이야기가 달라집니다. 이 시리즈는 젯슨 보드 한 장으로 그 루프를 세 단계에 걸쳐 직접 만듭니다.
피지컬 AI는 네 개의 블록이 도는 루프입니다
챗봇은 답을 틀려도 화면 안에서 끝납니다. 피지컬 AI는 틀리면 물체가 넘어지고 로봇이 벽에 부딪힙니다. 이 차이 때문에 구조가 달라집니다. 클라우드에 사진을 올려 답을 기다리는 구조로는 못 만들고, 보드 위에서 루프가 닫혀야 합니다.

시리즈는 이 루프를 한 블록씩 붙여 나갑니다.
| 편 | 만드는 것 | 붙이는 블록 |
|---|---|---|
| 1부 (이 글) | 실시간 객체 검출 | ① 감지 + ② 추론 |
| 2부 | 사람을 따라 도는 팬틸트 카메라 | ③ 구동까지 연결 |
| 3부 | 주행 데이터로 학습시킨 자율주행 미니카 | ② 추론을 직접 학습 |
"젯슨 나노"는 이제 두 가지를 뜻합니다
중고 장터에서 "젯슨 나노"를 검색하면 두 종류가 섞여 나옵니다. 이름이 비슷해서 헷갈리는데, 실제로는 6년 차이가 나는 완전히 다른 물건입니다.
| 항목 | Jetson Nano 4GB (2019) | Jetson Orin Nano Super 8GB |
|---|---|---|
| GPU | 128코어 Maxwell, 472 GFLOPS(FP16) | 1024코어 Ampere + 텐서코어 32개 |
| AI 성능 | INT8 텐서코어 없음 | 67 TOPS (INT8, MAXN SUPER 모드) |
| 메모리 | 4GB LPDDR4, 25.6 GB/s | 8GB LPDDR5, 102 GB/s |
| 마지막 JetPack | 4.6.6 — 2024년 11월 EOL | 7.2.1 (현행) |
| OS / 파이썬 | Ubuntu 18.04 / Python 3.6 | Ubuntu 22.04 또는 24.04 / Python 3.10 이상 |
| 가격 | 단종, 중고만 | 249달러 |
원조 Jetson Nano는 2024년 11월 JetPack 4.6.6을 끝으로 EOL이 선언됐습니다. 계속 쓸 수는 있지만 Python 3.6에 묶여 있어서 요즘 나오는 파이토치·울트라리틱스 패키지가 아예 설치되지 않습니다. 학생 프로젝트라면 시간을 여기에 쓰지 마세요. 이미 갖고 있다면 1부의 검출까지는 버전을 낮춰 따라올 수 있지만, 3부의 학습은 무리입니다.
이 시리즈는 Jetson Orin Nano Super 개발자 키트 8GB를 기준으로 씁니다. 2024년 12월 소프트웨어 업데이트만으로 40 TOPS에서 67 TOPS로 올라간 그 보드입니다. 함께 살 것은 다음과 같습니다.
- NVMe SSD 256GB 이상 (M.2 2280). SD 카드로도 되지만 학습 데이터를 다루기 시작하면 체감이 완전히 다릅니다.
- 카메라: Raspberry Pi Camera Module v2 (IMX219) 또는 USB 웹캠. CSI를 쓴다면 오린 나노 개발자 키트의 카메라 커넥터가 22핀이라 라즈베리파이용 15핀 케이블이 그대로 들어가지 않습니다. 15핀에서 22핀으로 넘어가는 케이블인지 확인하고 사세요.
- 전원: 5V/5A 이상 어댑터. 번들 어댑터로 충분하지만, 남는 휴대폰 충전기로 때우면 부하가 걸릴 때 재부팅됩니다.
JetPack은 최신 대신 6.2를 고르세요
2026년 9월 현재 최신은 JetPack 7.2.1입니다. 그런데 학생 프로젝트에서는 6.2 계열을 권합니다. 이유는 성능이 아니라 설치와 호환성입니다.
| 항목 | JetPack 6.2.x | JetPack 7.2.1 |
|---|---|---|
| Jetson Linux | 36.4.3 (커널 5.15) | 39.2.1 (커널 6.8) |
| Ubuntu | 22.04 | 24.04 |
| CUDA | 12.6 | 13.2.1 |
| TensorRT | 10.3 | 10.16.2 |
| 설치 매체 | SD 카드 이미지 제공 | ISO를 USB에 구워 설치 |
| 필요한 호스트 PC | 없음, SD만 구우면 됨 | x86 Ubuntu 24.04 + SDK Manager |
| 예제와 컨테이너 | 거의 그대로 동작 | CUDA 13 기준으로 다시 빌드해야 하는 것들이 있음 |
결정적인 차이는 설치 경로입니다. NVIDIA 다운로드 페이지에 Jetson Orin Nano 개발자 키트용 SD 카드 이미지는 더 이상 제공하지 않는다고 명시돼 있습니다. JetPack 7을 쓰려면 우분투가 깔린 x86 PC와 USB-C 케이블, 리커버리 모드 점퍼가 필요합니다. 노트북 한 대로 수업을 따라가는 상황이면 여기서 반나절이 사라집니다.
JetPack 6.2.1의 SD 카드 이미지를 받아 balenaEtcher로 굽는 것이 가장 빠릅니다. 6.2.2가 나와 있으므로 부팅한 뒤 sudo apt update && sudo apt full-upgrade 로 올려두면 됩니다.
첫 부팅 뒤 30분에 할 일
우분투 초기 설정을 마치고 나면 성능 모드가 기본값으로 잡혀 있습니다. Super 모드를 켜지 않으면 자기 보드의 절반만 쓰는 셈입니다.
# 현재 전력 모드 확인 (Orin Nano Super 는 2번이 MAXN SUPER)
sudo nvpmodel -q
# MAXN SUPER 로 전환하고 클럭을 최대로 고정
sudo nvpmodel -m 2
sudo jetson_clocks
# 부팅할 때마다 클럭 고정을 자동 적용
sudo cp /usr/bin/jetson_clocks /usr/local/bin/jetson_clocks_boot
printf '[Unit]\nDescription=Pin Jetson clocks\nAfter=nvpmodel.service\n[Service]\nType=oneshot\nExecStart=/usr/bin/jetson_clocks\nRemainAfterExit=yes\n[Install]\nWantedBy=multi-user.target\n' | sudo tee /etc/systemd/system/jetson_clocks.service
sudo systemctl enable jetson_clocks.service
메모리는 8GB를 CPU와 GPU가 나눠 씁니다. 기본 스왑으로는 모델을 조금만 키워도 프로세스가 죽으므로 미리 늘려 둡니다.
sudo systemctl disable nvzramconfig
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
모니터링 도구도 하나 깝니다. jtop은 CPU와 GPU 사용률, 전력 모드, 온도, 메모리를 한 화면에 보여줍니다. 프로젝트 내내 두 번째 터미널에 띄워 두게 됩니다.
sudo pip3 install -U jetson-stats # JetPack 7(Ubuntu 24.04)이면 --break-system-packages 를 붙인다
sudo reboot
# 재부팅 후
jtop
데스크톱 환경을 끄면 GPU가 쓸 메모리가 수백 MB 늘어납니다. SSH로 붙어서 작업할 거라면 켜 두지 마세요.
sudo systemctl set-default multi-user.target # 되돌리려면 graphical.target
카메라를 붙일 때 CSI와 USB가 다른 이유
USB 웹캠이 훨씬 편합니다. 꽂으면 끝입니다. 그런데 프레임이 GPU까지 도달하는 경로가 다릅니다.

CSI 카메라는 하드웨어 ISP를 거쳐 NVMM이라는 GPU 공유 영역에 프레임을 올려 둡니다. 추론기가 그대로 읽으므로 복사가 없습니다. USB 카메라는 MJPEG을 CPU에서 풀고 GPU로 복사하는 단계가 붙습니다. 1부와 2부의 검출 정도면 USB로도 충분하지만, 3부의 주행처럼 지연이 곧 사고로 이어지는 작업에서는 CSI 쪽이 편합니다.
먼저 장치가 잡혔는지 확인합니다.
ls /dev/video*
v4l2-ctl --list-devices
# CSI 라면 이 명령이 센서 모드 목록을 뿌리고 30프레임을 받아야 정상입니다
gst-launch-1.0 nvarguscamerasrc sensor-id=0 num-buffers=30 ! fakesink -v
두 카메라를 같은 코드로 다루는 래퍼를 하나 만들어 두면 뒤에서 계속 재사용합니다.
# camera.py — CSI 카메라와 USB 카메라를 같은 인터페이스로 연다
import cv2
def csi_pipeline(sensor_id=0, cap_w=1280, cap_h=720,
out_w=640, out_h=360, fps=30, flip=0):
"""nvarguscamerasrc 로 CSI 프레임을 받아 BGR 로 내려주는 GStreamer 파이프라인."""
return (
f"nvarguscamerasrc sensor-id={sensor_id} ! "
f"video/x-raw(memory:NVMM), width={cap_w}, height={cap_h}, "
f"format=NV12, framerate={fps}/1 ! "
f"nvvidconv flip-method={flip} ! "
f"video/x-raw, width={out_w}, height={out_h}, format=BGRx ! "
"videoconvert ! video/x-raw, format=BGR ! "
# drop=true, max-buffers=1 이 없으면 처리가 느릴 때 프레임이 밀려 지연이 쌓인다
"appsink drop=true max-buffers=1 sync=false"
)
def open_camera(source="csi", index=0, out_w=640, out_h=360):
if source == "csi":
cap = cv2.VideoCapture(
csi_pipeline(sensor_id=index, out_w=out_w, out_h=out_h),
cv2.CAP_GSTREAMER,
)
else:
cap = cv2.VideoCapture(index, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, out_w)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, out_h)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
if not cap.isOpened():
raise RuntimeError("카메라를 열지 못했습니다. ls /dev/video* 로 장치부터 확인하세요.")
return cap
if __name__ == "__main__":
import sys
import time
cap = open_camera(sys.argv[1] if len(sys.argv) > 1 else "csi")
n, t0 = 0, time.time()
while n < 300:
ok, _ = cap.read()
if not ok:
break
n += 1
dt = time.time() - t0
print(f"{n} 프레임 / {dt:.1f}초 = {n / dt:.1f} fps")
cap.release()
여기서 자주 걸리는 함정이 하나 있습니다. pip install opencv-python 으로 깐 OpenCV는 GStreamer 지원 없이 빌드돼 있어서 cv2.CAP_GSTREAMER 가 조용히 실패합니다. JetPack이 기본 제공하는 python3-opencv 를 쓰세요.
python3 -c "import cv2; print(cv2.getBuildInformation())" | grep -i gstreamer
# GStreamer: YES 가 나와야 한다
첫 추론은 컨테이너 하나로 끝냅니다
파이토치와 TensorRT를 손으로 맞추는 데 하루를 쓰지 마세요. NVIDIA의 jetson-inference 가 사전 학습 모델과 실행 환경을 통째로 들고 있습니다.
git clone --recursive --depth=1 https://github.com/dusty-nv/jetson-inference
cd jetson-inference
docker/run.sh # JetPack 버전에 맞는 이미지를 알아서 받아 온다
컨테이너 안에서 한 줄이면 검출이 돕니다.
# CSI 카메라 → 모니터
detectnet --model=ssd-mobilenet-v2 csi://0 display://0
# USB 카메라를 쓸 때
detectnet /dev/video0 display://0
# 모니터 없이 SSH 로만 붙어 있다면 브라우저로 본다 (https://<젯슨IP>:8554)
detectnet csi://0 webrtc://@:8554/output
첫 실행은 몇 분간 멈춘 것처럼 보입니다. 고장이 아니라 ONNX 모델을 TensorRT 엔진으로 처음 변환하는 중입니다. 결과가 .engine 파일로 캐시되므로 두 번째부터는 수 초 만에 뜹니다.
검출 결과를 내 코드에서 받아 보기
2부에서 모터를 돌리려면 화면에 그려 주는 것이 아니라 좌표를 변수로 받는 것이 필요합니다. 그 최소 코드가 아래입니다. 이 파일이 2부의 출발점이 됩니다.
# detect_stream.py — 검출 결과를 좌표로 받아 쓰는 최소 코드
import argparse
import time
from jetson_inference import detectNet
from jetson_utils import videoSource, videoOutput, cudaFont
parser = argparse.ArgumentParser()
parser.add_argument("--input", default="csi://0", help="csi://0, /dev/video0 등")
parser.add_argument("--output", default="display://0",
help="display://0 또는 webrtc://@:8554/output")
parser.add_argument("--network", default="ssd-mobilenet-v2")
parser.add_argument("--threshold", type=float, default=0.5)
args = parser.parse_args()
net = detectNet(args.network, threshold=args.threshold)
camera = videoSource(args.input)
display = videoOutput(args.output)
font = cudaFont()
frames, t0 = 0, time.time()
while display.IsStreaming():
img = camera.Capture()
if img is None: # 타임아웃. 다음 프레임을 기다린다
continue
detections = net.Detect(img, overlay="box,labels,conf")
# 사람만 골라 가장 큰 것 하나를 표적으로 삼는다 — 2부에서 이 좌표를 서보로 보낸다
people = [d for d in detections if net.GetClassDesc(d.ClassID) == "person"]
target = max(people, key=lambda d: d.Area) if people else None
if target is not None:
# 화면 중심 대비 오차를 -1.0 ~ +1.0 으로 정규화
err_x = (target.Center[0] - img.width / 2) / (img.width / 2)
err_y = (target.Center[1] - img.height / 2) / (img.height / 2)
print(f"err_x={err_x:+.2f} err_y={err_y:+.2f} conf={target.Confidence:.2f}")
font.OverlayText(
img, img.width, img.height,
f"person {len(people)} | {net.GetNetworkFPS():.0f} FPS",
10, 10, font.White, font.Gray40,
)
display.Render(img)
frames += 1
if frames % 120 == 0:
print(f"[{frames} frames] 평균 {frames / (time.time() - t0):.1f} fps")
err_x 와 err_y 가 이 시리즈의 핵심 변수입니다. 표적이 화면 왼쪽에 있으면 음수, 오른쪽이면 양수가 됩니다. 2부에서는 이 값에 게인을 곱해 서보 각도로 바꾸고, 3부에서는 이 값 자체를 신경망이 직접 출력하게 만듭니다.
여기서 막히면 보는 표
| 증상 | 원인 | 조치 |
|---|---|---|
ls /dev/video* 에 아무것도 없음 | CSI 케이블 방향 또는 22핀 규격 불일치 | 전원 끄고 접점 면 방향 확인, 15핀에서 22핀으로 가는 케이블인지 확인 |
cap.isOpened() 가 False | pip 로 깐 OpenCV에 GStreamer가 없음 | pip uninstall opencv-python 후 JetPack 기본 python3-opencv 사용 |
| detectnet 첫 실행이 몇 분간 멈춤 | TensorRT 엔진 최초 빌드 | 정상. 두 번째 실행부터 수 초 |
| 추론 중 프로세스가 Killed | 8GB를 CPU와 GPU가 공유, 스왑 부족 | 스왑을 8GB로 늘리고 데스크톱 끄기 |
| FPS가 기대의 절반 | 전력 모드가 기본값 | sudo nvpmodel -m 2 후 sudo jetson_clocks |
nvpmodel -m 2 가 없다고 나옴 | JetPack 6.1 미만 | 6.2 이상으로 업데이트 |
| 부하가 걸릴 때 재부팅 | 전원 어댑터 용량 부족 | 5V/5A 이상 어댑터 사용 |
다음 편
2부에서는 지금 뽑은 err_x, err_y 를 서보 각도로 바꿔 카메라가 사람을 따라 돌게 만듭니다. 오린 나노의 40핀 하드웨어 PWM을 왜 쓰지 않는지, PCA9685를 어떻게 붙이는지, P 제어의 게인을 얼마부터 시작해야 떨지 않는지를 배선도와 코드로 다룹니다.
지금 할 일은 하나입니다. detect_stream.py 를 켜 놓고 카메라 앞에서 좌우로 걸어 보면서 err_x 값이 -1과 +1 사이를 얼마나 부드럽게 오가는지 눈으로 확인하세요. 이 값이 튀는 조명 조건과 거리를 미리 알아 두면, 2부에서 서보가 떠는 이유를 훨씬 빨리 찾습니다.
참고 자료
- NVIDIA Jetson Orin Nano Developer Kit Gets a "Super" Boost — NVIDIA Technical Blog
- JetPack SDK 6.2 — 구성 요소 버전과 SD 카드 이미지
- JetPack SDK Downloads and Notes — 7.2.1 및 SD 카드 이미지 중단 안내
- Announcing End of Life for NVIDIA JetPack 4 (JetPack 4.6.6)
- Jetson Orin Nano Developer Kit Quick Start Guide
- dusty-nv/jetson-inference — GitHub
'인공지능 AI' 카테고리의 다른 글
| 젯슨 나노 피지컬 AI 3부 - 주행 데이터 학습과 자율주행 (0) | 2026.09.08 |
|---|---|
| 젯슨 나노 피지컬 AI 2부 - 팬틸트 카메라 사람 추적 (0) | 2026.09.08 |
| AI 모방학습 준비와 절차 - LeRobot 기준 (0) | 2026.08.28 |
| 피지컬 AI, 개념과 2026년 현재 위치 (0) | 2026.08.25 |
| nvidia 그래픽 카드의 정보를 확인하는 방법 (0) | 2026.03.19 |