BrainAI Car 프로젝트 코드로 배우는 파이썬
실제 utils/ 파일들의 코드를 읽으면서 파이썬 핵심 개념을 익혀요. 코드를 먼저 봐도 되고, 막히는 단원을 골라봐도 돼요.
다른 파일 가져오기
다른 파일에 있는 코드를 현재 파일에서 쓰고 싶을 때 import 해요. `from 파일 import 이름` 형태가 가장 많이 쓰여요.
# 노트북에서 car 모듈 불러오기from utils.brain_ai_car import BrainAICar# 이제 BrainAICar 클래스를 쓸 수 있어요car = BrainAICar(mock=True)# 여러 개를 한 번에 import 할 수도 있어요from utils.brain_ai_car import BrainAICar, SERVO_CENTER, DEFAULT_FORWARD_SPEED
값에 이름 붙이기
변수는 값을 담는 상자예요. 파이썬은 `=` 오른쪽 값을 보고 타입을 자동으로 결정해요 — 숫자면 int, 소수면 float, 글자면 str, 참/거짓이면 bool.
# brain_ai_car.py 에서 실제로 쓰는 변수들BAUD_RATE = 115200 # int — 시리얼 통신 속도DEFAULT_FORWARD_SPEED = 350 # int — 기본 전진 속도SERVO_CENTER = 90 # int — 서보 중립 각도STOP_HOLD_DURATION = 0.5 # float — 정지 유지 시간(초)# 객체 생성 후 인스턴스 변수self.forward_speed = 350 # int — 현재 설정된 속도self._speed_confirmed = True # bool — 속도 명령 확인 여부port_name = "COM3" # str — 시리얼 포트 이름
설계도와 실체
클래스는 설계도, 객체는 설계도로 만든 실제 부품이에요. `class` 로 설계도를 만들고, `클래스이름()` 으로 실체(인스턴스)를 생성해요. `__init__` 은 생성과 동시에 자동으로 실행되는 함수예요.
# brain_ai_car.py — 자동차 제어 클래스class BrainAICar:def __init__(self, mock: bool = False):self.mock = mockself.forward_speed = 350car = BrainAICar(mock=True) # 인스턴스 만들기print(car.forward_speed) # 350# modeling.py — 훈련 설정 클래스 (dataclass 방식)@dataclassclass TrainingConfig:dataset_path: str = "laneD1_Dataset"model_name: str = "laneD1"epochs: int = 30batch_size: int = 32config = TrainingConfig()print(config.model_name) # "laneD1"
반복 동작을 묶기
`def` 로 함수를 정의해요. 클래스 안에 있으면 '메서드'라고 불러요. 첫 번째 인자 `self` 는 '이 객체 자신'을 가리켜요. `-> None` 은 반환값이 없다는 의미예요.
# brain_ai_car.py — 제어 메서드 예시def drive_forward(self, speed: int = None) -> None:"""전진 명령을 전송합니다."""target = speed if speed is not None else self.forward_speedself._issue_speed_command(target)def steer(self, steering_value) -> None:"""조향 명령을 전송합니다. -1.0(우) ~ +1.0(좌)"""angle = SERVO_CENTER - int(steering_value * self.steering_range)angle = max(SERVO_MIN, min(SERVO_MAX, angle))self._send(angle, bypass_rate_limit=True)# 사용car.drive_forward() # 기본 속도로 전진car.drive_forward(400) # 속도 400으로 전진car.steer(0.3) # 살짝 우회전
경우에 따라 다르게
`if` 는 '만약 ~이면'이에요. `not` 은 '~이 아니면', `in` 은 '~안에 있으면'이에요. `elif` 로 추가 조건, `else` 로 나머지 경우를 처리해요.
# brain_ai_car.py — USB 포트 찾기def _find_microbit_port(self):for port, desc, _ in sorted(serial.tools.list_ports.comports()):if 'USB' in desc: # desc 에 'USB' 문자가 있으면return portreturn None # 없으면 None 반환# 연결 확인 후 처리port = self._find_microbit_port()if not port:print("[ERROR] micro:bit not found.")return# 속도 명령 타입 분기if speed == 0: # 정지self._in_forced_stop = Trueelif speed == -1: # 후진self._issue_speed_command(-1)else: # 전진self._issue_speed_command(speed)
같은 일 여러 번
`for` 는 리스트나 범위의 항목을 하나씩 꺼내 반복해요. `break` 는 반복을 즉시 멈추고, `continue` 는 이번 회차를 건너뛰어요.
# brain_ai_car.py — 직렬 포트 목록을 순회해 마이크로비트 찾기for port, desc, hwid in sorted(serial.tools.list_ports.comports()):print(f'포트: {port}, 설명: {desc}')if 'USB' in desc:print('마이크로비트 발견!')break # 찾으면 즉시 종료# 안전 감지 결과 순회 (check_safety_objects 내부)for i, (class_id, conf) in enumerate(zip(classes, confidences)):if conf < confidence_threshold:continue # 신뢰도 낮으면 건너뜀detected.append(class_names[class_id])
오류가 나도 멈추지 않게
`try` 블록을 실행하다 오류가 생기면 `except` 로 넘어와요. 프로그램이 갑자기 종료되는 걸 막아줘요. `Exception as e` 로 오류 메시지를 꺼낼 수 있어요.
# brain_ai_car.py — 시리얼 연결 예외 처리try:self._serial = serial.Serial(port, BAUD_RATE, timeout=0.1,parity=serial.PARITY_NONE, rtscts=False,)print(f'[OK] Connected on {port}')self._serial.reset_input_buffer()except Exception as e:print(f'[ERROR] {e}')self._serial = None# 데이터 전송 예외 처리try:data = f'{int(cmd)}\n'.encode('utf-8')written = self._serial.write(data)return Trueexcept Exception as e:print(f'[SEND ERROR] {e}')return False
변수를 문자열 안에
문자열 앞에 `f` 를 붙이면 `{변수명}` 위치에 실제 값이 들어가요. `{숫자:.4f}` 처럼 소수점 자릿수, `{'ON' if 조건 else 'OFF'}` 처럼 조건식도 바로 넣을 수 있어요.
# modeling.py — TrainingConfig.print_summary() 에서def print_summary(self) -> None:print("=" * 55)print("Training Configuration")print(f' Dataset: {self.dataset_path}')print(f' Model name: {self.model_name}')print(f' Input size: {self.img_width}×{self.img_height}')print(f' Epochs: {self.epochs}')print(f' Batch size: {self.batch_size}')print(f' Learning rate: {self.learning_rate}')# 조건식을 중괄호 안에 바로 넣기print(f" Augmentation: {'ON' if self.use_augmentation else 'OFF'}")# modeling.py — _print_results() 에서 소수점 포맷팅print(f" Final train loss: {h['loss'][-1]:.4f} MAE: {h['mae'][-1]:.4f}")print(f" Final val loss: {h['val_loss'][-1]:.4f} MAE: {h['val_mae'][-1]:.4f}")# → Final train loss: 0.0123 MAE: 0.0871
여러 값 묶어서 관리
리스트(`[]`)는 순서 있는 값의 묶음이에요. 딕셔너리(`{}`)는 키-값 쌍의 묶음이에요. 리스트는 인덱스(0부터)로, 딕셔너리는 키로 접근해요.
# brain_ai_car.py — 리스트: 감지 대상 클래스 목록self.safety_classes = ['person', 'car', 'dog', 'stop_sign', 'crosswalk']print(self.safety_classes[0]) # 'person'self.safety_classes.append('bicycle') # 항목 추가# modeling.py — 딕셔너리: 증강 옵션 ON/OFF 설정augmentation_options: Dict[str, bool] = {"COLOR_JITTER": False, # 밝기·채도 랜덤 변환"HORIZONTAL_FLIP": False, # 좌우 반전"GAUSSIAN_NOISE": False, # 노이즈 추가}# 딕셔너리 키로 접근해서 분기opts = self.config.augmentation_optionsif opts.get("COLOR_JITTER"):img_arr = ImageAugmentation.color_jitter(img_arr)if opts.get("GAUSSIAN_NOISE"):img_arr = ImageAugmentation.gaussian_noise(img_arr)
두 가지 일 동시에
스레드는 '여러 일을 동시에' 처리하는 방법이에요. 자동차가 주행하는 동안 마이크로비트 응답을 별도 스레드로 계속 기다려요. `daemon=True` 는 메인 프로그램이 끝나면 이 스레드도 같이 종료한다는 뜻이에요.
# brain_ai_car.py — 백그라운드 스레드로 시리얼 수신 처리import threadingdef _start_read_thread(self):self._read_thread = threading.Thread(target=self._read_loop, # 별도 스레드로 실행할 함수daemon=True, # 메인 종료 시 같이 종료)self._read_thread.start()def _read_loop(self):while not self._stop_reading:if self._serial.in_waiting > 0:line = self._serial.readline().decode('utf-8').strip()self._handle_echo(line) # 마이크로비트 응답 처리time.sleep(0.001)
코스 2 object_detection.py 와 라벨링 툴 코드에서 뽑은 심화 개념이에요. 기초 단원을 끝냈거나, 개념이 궁금할 때 골라 읽어요.
데코레이터 — 괄호 없는 속성
`@property` 를 메서드 위에 붙이면 `obj.latency()` 대신 `obj.latency` 처럼 괄호 없이 호출할 수 있어요. 읽기 전용 계산값을 속성처럼 노출할 때 씁니다.
# object_detection.py — 최근 추론 지연(ms) 계산class ObjectDetector:def __init__(self):self.latency_history = []@propertydef latency(self) -> float:"""최근 10회 평균 추론 시간(ms)."""n = min(10, len(self.latency_history))return np.mean(self.latency_history[:n]) if n else 0.0detector = ObjectDetector()# 괄호 없이 속성처럼 읽음print(detector.latency) # 예: 23.4
결과를 하나씩 내놓기
`yield` 가 있는 함수는 제너레이터예요. 결과를 한꺼번에 리스트로 만들지 않고 요청할 때마다 하나씩 계산해요. 이미지 수천 장을 처리할 때 메모리를 크게 아낄 수 있어요.
# object_detection.py — 감지 결과를 하나씩 yielddef _iter_boxes(self, results):boxes = results[0].boxesnames = results[0].namesconfs = boxes.conf.cpu().numpy()clids = boxes.cls.cpu().numpy().astype(int)xyxys = boxes.xyxy.cpu().numpy().astype(int)for i in range(len(boxes)):name = names.get(clids[i], '')yield name, float(confs[i]), xyxys[i]# ↑ 이름 ↑ 신뢰도 ↑ 좌표# 사용 — for 루프가 하나씩 꺼냄for name, conf, coords in detector._iter_boxes(results):print(name, conf)
한 줄에서 여러 값 꺼내기
튜플(또는 리스트)의 값들을 변수 여러 개에 한꺼번에 담는 걸 언패킹이라 해요. `(x1, y1, x2, y2)` 처럼 중첩 구조도 한 줄에 풀 수 있어요.
# object_detection.py — 감지 결과에서 좌표 언패킹for name, conf, (x1, y1, x2, y2) in self._iter_boxes(results):# ↑이름 ↑신뢰도 ↑ 좌표 4개를 중첩 언패킹width = x2 - x1height = y2 - y1print(f'{name}: 좌표({x1},{y1})~({x2},{y2})')# 기본 언패킹 예시point = (100, 200)x, y = pointprint(x, y) # 100 200# swap — 언패킹으로 두 값 교환a, b = 1, 2a, b = b, aprint(a, b) # 2 1
모든 객체가 공유하는 값
클래스 안에 `self.` 없이 선언한 변수는 클래스 변수예요. 모든 인스턴스가 공유하고 `클래스명.변수` 로 접근해요. 감지할 표지판 종류처럼 '변하지 않는 공통 목록'에 씁니다.
# object_detection.py — 인식할 표지판 클래스 목록class ObjectDetector:# self 없이 클래스 바로 아래 선언 → 클래스 변수SIGN_CLASSES = ["left_turn_sign","stop_sign","forward_sign","right_turn_sign","school_zone_sign",]def is_sign(self, name: str) -> bool:return name in ObjectDetector.SIGN_CLASSES# 인스턴스 없이도 접근 가능print(ObjectDetector.SIGN_CLASSES[0]) # 'left_turn_sign'
for 반복문을 한 줄로
`[표현식 for 항목 in 시퀀스]` 형태로 리스트를 한 줄에 만들어요. `if 조건` 을 붙이면 필터링도 돼요. 일반 for 루프보다 짧고 빠릅니다.
# object_detection.py — 감지할 클래스를 소문자로 정규화targets = [c.lower() for c in custom_classes]# 동일한 일반 for 루프:# targets = []# for c in custom_classes:# targets.append(c.lower())# 조건 필터링 — 신뢰도 0.5 이상만high_conf = [name for name, conf, _ in detections if conf >= 0.5]# 중첩 — 2D 좌표를 1D로 펼치기flat = [v for row in matrix for v in row]# 딕셔너리 컴프리헨션 (같은 원리)scores = {name: conf for name, conf, _ in detections}
키가 없어도 안전하게
`dict[key]` 는 키가 없으면 KeyError가 나요. `dict.get(key, 기본값)` 은 키가 없을 때 기본값을 돌려줘서 프로그램이 멈추지 않아요.
# object_detection.py — 클래스 ID → 이름 변환# results[0].names 는 {0: 'left_turn_sign', 1: 'stop_sign', ...} 형태names = results[0].names# .get() — 키가 없으면 빈 문자열 반환name = names.get(cid, '')# [] 방식 — 키가 없으면 KeyError 발생# name = names[cid] ← 위험# 기본값을 다양하게 활용count = detections.get('stop_sign', 0) # 없으면 0label = class_map.get(idx, "unknown") # 없으면 unknownconfig = settings.get('threshold', 0.5) # 없으면 0.5
if/else를 한 줄로
`값1 if 조건 else 값2` 형태로 if/else를 한 줄에 써요. 짧고 간단한 조건에서 코드를 깔끔하게 만들어줘요.
# object_detection.py — 임계값 인자가 없으면 기본값 사용def detect(self, frame, confidence_threshold=None):thr = (confidence_thresholdif confidence_threshold is not Noneelse self.confidence_threshold)# 한 줄로 쓸 수도 있음thr = confidence_threshold if confidence_threshold is not None else self.confidence_threshold# 다른 예시들label = 'stop' if speed == 0 else 'go'sign = '+' if value >= 0 else '-'status = 'found' if port else 'not found'
AI가 다루는 숫자 묶음
NumPy 배열은 파이썬 리스트보다 훨씬 빠른 수치 계산에 쓰여요. 이미지가 곧 숫자 배열이고, AI 모델의 입출력도 전부 NumPy 배열이에요. `[:n]` 슬라이싱과 `np.mean()` 이 자주 쓰입니다.
import numpy as np# object_detection.py — 최근 10회 추론 시간 평균n = min(10, len(self.latency_history))avg = np.mean(self.latency_history[:n])# ↑ 슬라이싱: 앞에서 n개# 이미지 → NumPy 배열import cv2frame = cv2.imread('image.jpg') # shape: (H, W, 3)print(frame.shape) # (480, 640, 3)print(frame.dtype) # uint8# 배열 연산 (모든 픽셀에 동시 적용)normalized = frame / 255.0 # 0~255 → 0.0~1.0print(np.mean(frame)) # 전체 평균 밝기
패턴으로 파일 한꺼번에 찾기
`glob.glob(패턴)` 은 와일드카드(`*`, `**`)로 폴더 안 파일을 한꺼번에 찾아요. 수백 장의 이미지를 일일이 나열할 필요 없이 경로 패턴 하나로 전부 불러와요.
import glob# Step 4 Dataset Building — 하위 폴더까지 모든 jpg 찾기images = glob.glob('data/**/*.jpg', recursive=True)print(f'총 {len(images)}장 발견') # 예: 총 1024장 발견# 특정 폴더만train_imgs = glob.glob('dataset/train/*.jpg')val_imgs = glob.glob('dataset/val/*.jpg')# pathlib 방식 (최신 파이썬 스타일)from pathlib import Pathimages = list(Path('data').rglob('*.jpg'))labels = list(Path('data').rglob('*.txt'))print(len(images), len(labels))
AI 개발은 파이썬만이 아니에요
우리가 만든 라벨링 툴은 파이썬이 아닌 웹(JavaScript + Canvas)으로 만들었어요. AI 프로젝트엔 데이터 처리(Python), UI(JS/HTML), 배포(서버) 등 다양한 언어가 함께 쓰여요.
// bbox_labeler.html — canvas 위에 바운딩 박스 그리기 (JavaScript)function drawBoxes() {ctx.clearRect(0, 0, canvas.width, canvas.height);ctx.drawImage(img, 0, 0, canvas.width, canvas.height);for (const box of current.boxes) {ctx.strokeStyle = CLASS_COLORS[box.classIdx];ctx.lineWidth = 2;ctx.strokeRect(box.x, box.y, box.w, box.h);// 클래스 이름 라벨ctx.fillStyle = CLASS_COLORS[box.classIdx];ctx.fillText(CLASSES[box.classIdx], box.x + 4, box.y + 14);}}// Python과 비교 — 변수 선언만 달라요// Python: boxes = [] JS: const boxes = []// Python: for box in boxes JS: for (const box of boxes)