오늘 만들 프로그램
1단계. 라즈베리파이 환경 확인
cat /etc/os-release
which raspistill
vcgencmd get_camera
python3 -c "import cv2, numpy; print('OpenCV:', cv2.__version__); print('NumPy:', numpy.__version__)"/usr/bin/raspistill, supported=1 detected=1, OpenCV와 NumPy 버전이 출력되는 것입니다.2단계. 실습 폴더 만들기
mkdir -p ~/object_recognition_raspi_app/obj_data
mkdir -p ~/object_recognition_raspi_app/model
cd ~/object_recognition_raspi_app
pwd마지막 줄이 /home/pi/object_recognition_raspi_app이면 정상입니다.
3단계. robogram.net 이미지셋 준비
- 라즈베리파이 Chromium에서 robogram.net 데이터 저장소에 접속합니다.
- 수업에서 안내받은 이미지셋 ZIP 파일을 다운로드합니다.
- 파일 관리자에서 압축을 풉니다.
- 사물별 폴더를
~/object_recognition_raspi_app/obj_data안으로 이동합니다.
obj_data/keyboard, obj_data/mouse, obj_data/bottlecd ~/object_recognition_raspi_app
find obj_data -maxdepth 2 -type f | head -304단계. 공통 카메라 기능 파일 만들기
① nano로 Python 파일 만들기
cd ~/object_recognition_raspi_app
nano camera_utils.py② 전체 코드 복사·붙여넣기
아래 코드 복사하기를 누른 뒤 nano 화면에서 Ctrl + Shift + V로 붙여넣습니다. 환경에 따라 마우스 오른쪽 버튼의 붙여넣기를 사용합니다.
# -*- coding: utf-8 -*-
import os
import shutil
import subprocess
import tempfile
import cv2
class CameraSource:
def __init__(self, width=640, height=480):
self.width = int(width)
self.height = int(height)
self.cap = None
self.mode = None
self.temp_path = os.path.join(tempfile.gettempdir(), 'raspi_object_frame.jpg')
cap = cv2.VideoCapture(0)
if cap.isOpened():
cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
ok, frame = cap.read()
if ok and frame is not None:
self.cap = cap
self.mode = 'video'
return
cap.release()
if shutil.which('raspistill'):
self.mode = 'raspistill'
return
raise RuntimeError('No camera source was found. Check /dev/video0 or raspistill.')
def read(self):
if self.mode == 'video':
return self.cap.read()
command = [
'raspistill', '-n', '-t', '180',
'-w', str(self.width), '-h', str(self.height),
'-q', '75', '-o', self.temp_path,
]
try:
subprocess.run(
command, check=True, timeout=6,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
return False, None
frame = cv2.imread(self.temp_path)
return frame is not None, frame
def release(self):
if self.cap is not None:
self.cap.release()
if os.path.exists(self.temp_path):
try:
os.remove(self.temp_path)
except OSError:
pass
def mode_name(self):
if self.mode == 'video':
return 'OpenCV VideoCapture (/dev/video0)'
return 'Legacy CSI camera (raspistill)'③ 저장하고 nano 종료하기
④ 실행하기
cd ~/object_recognition_raspi_app
python3 -m py_compile camera_utils.py/dev/video0을 우선 사용하고, 없으면 raspistill로 자동 전환합니다. 문법 검사에서 아무 메시지가 없으면 정상입니다.5단계. 카메라 테스트 파일 만들기
① nano로 Python 파일 만들기
cd ~/object_recognition_raspi_app
nano 0_camera_test.py② 전체 코드 복사·붙여넣기
아래 코드 복사하기를 누른 뒤 nano 화면에서 Ctrl + Shift + V로 붙여넣습니다. 환경에 따라 마우스 오른쪽 버튼의 붙여넣기를 사용합니다.
# -*- coding: utf-8 -*-
import cv2
from camera_utils import CameraSource
def main():
camera = CameraSource(640, 480)
print('Camera test started.')
print('Camera mode:', camera.mode_name())
print('Click the camera window and press q to stop.')
try:
while True:
ok, frame = camera.read()
if not ok or frame is None:
print('A camera frame could not be captured.')
break
cv2.putText(frame, camera.mode_name(), (12, 28),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 2)
cv2.imshow('Raspberry Pi Camera Test', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
camera.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
main()③ 저장하고 nano 종료하기
④ 실행하기
cd ~/object_recognition_raspi_app
python3 0_camera_test.py라즈베리파이 속도 최적화
MobileNetV2 CNN은 인식률이 높은 대신 Raspberry Pi 4 CPU에서 매 프레임 실행하면 카메라 화면이 잠깐씩 멈출 수 있습니다. 이번 버전은 카메라 화면과 CNN 계산을 분리했습니다.
1_train_object_model.py를 다시 실행하면 기존 CNN v3 모델을 즉시 재사용합니다.INFERENCE_INTERVAL = 0.30을 0.40~0.50으로 높이면 화면이 더 부드러워지고,
0.20~0.25로 낮추면 물체명 반응은 빨라지지만 Pi에서 다시 끊김이 생길 수 있습니다.인식 알고리즘을 CNN으로 변경한 이유
이전 HSV·HOG·k-NN 방식에서는 키보드의 강한 선과 반복 무늬가 다른 사물보다 지나치게 강한 특징으로 작용해, 가방·스마트폰·모니터·마우스 등이 키보드로 오인식되는 현상이 나타날 수 있었습니다.
obj_data의 각 학습 사진을 CNN 특징 벡터로 변환해 사용합니다.1_train_object_model.py를 처음 실행하면 약 14MB의
mobilenetv2-7.onnx를 ONNX Model Zoo에서 자동 다운로드합니다.
한 번 저장되면 다음 실행부터 다시 받지 않습니다.object_model.npz와 구조가 다릅니다.
반드시 6단계 학습부터 다시 실행해 새 모델을 만드세요.6단계. 사물 인식 모델 학습 파일 만들기
python3 1_train_object_model.py를 다시 실행해
model/object_model.npz를 새로 만든 뒤 실시간 인식과 인식률 측정을 실행하세요.① nano로 Python 파일 만들기
cd ~/object_recognition_raspi_app
nano 1_train_object_model.py② 전체 코드 복사·붙여넣기
아래 코드 복사하기를 누른 뒤 nano 화면에서 Ctrl + Shift + V로 붙여넣습니다. 환경에 따라 마우스 오른쪽 버튼의 붙여넣기를 사용합니다.
# -*- coding: utf-8 -*-
import os
import cv2
import numpy as np
DATA_DIR = 'obj_data'
MODEL_DIR = 'model'
MODEL_PATH = os.path.join(MODEL_DIR, 'object_model.npz')
REPORT_PATH = os.path.join(MODEL_DIR, 'training_report.txt')
IMAGE_EXTS = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
def build_data_signature(class_names):
parts = []
for class_name in class_names:
class_dir = os.path.join(DATA_DIR, class_name)
for file_name in sorted(os.listdir(class_dir)):
if not file_name.lower().endswith(IMAGE_EXTS):
continue
path = os.path.join(class_dir, file_name)
try:
st = os.stat(path)
parts.append('{}|{}|{}|{}'.format(
class_name, file_name, int(st.st_size), int(st.st_mtime)
))
except OSError:
pass
return '\n'.join(parts)
def can_reuse_existing_model(signature):
if not os.path.exists(MODEL_PATH):
return False
try:
model = np.load(MODEL_PATH, allow_pickle=False)
if 'model_version' not in model.files or int(model['model_version'][0]) != 3:
return False
if 'data_signature' not in model.files:
return False
return str(model['data_signature'][0]) == signature
except Exception:
return False
MOBILENET_PATH = os.path.join('model', 'mobilenetv2-7.onnx')
MOBILENET_URL = (
'https://github.com/onnx/models/raw/refs/heads/main/'
'validated/vision/classification/mobilenet/model/mobilenetv2-7.onnx'
)
def ensure_mobilenet_model():
"""MobileNetV2 ONNX 모델이 없으면 한 번만 다운로드합니다."""
os.makedirs('model', exist_ok=True)
if os.path.exists(MOBILENET_PATH) and os.path.getsize(MOBILENET_PATH) > 1000000:
return
print('MobileNetV2 model is not installed.')
print('Downloading about 14 MB from the ONNX Model Zoo...')
try:
import urllib.request
urllib.request.urlretrieve(MOBILENET_URL, MOBILENET_PATH)
except Exception as e:
raise RuntimeError(
'MobileNetV2 download failed. Connect Raspberry Pi to the internet and try again. / '
+ str(e)
)
if (not os.path.exists(MOBILENET_PATH)
or os.path.getsize(MOBILENET_PATH) < 1000000):
raise RuntimeError(
'Downloaded MobileNetV2 file is invalid. Delete model/mobilenetv2-7.onnx and try again.'
)
def load_cnn():
ensure_mobilenet_model()
try:
cv2.setNumThreads(max(1, min(4, os.cpu_count() or 1)))
except Exception:
pass
try:
net = cv2.dnn.readNetFromONNX(MOBILENET_PATH)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
except Exception as e:
raise RuntimeError('OpenCV could not load MobileNetV2 ONNX model. / ' + str(e))
return net
def prepare_cnn_input(image):
"""
MobileNetV2 입력:
RGB 224x224, 0~1 스케일 후 ImageNet mean/std 정규화.
물체 비율은 유지하고 회색 여백을 넣습니다.
"""
h, w = image.shape[:2]
if h <= 0 or w <= 0:
raise ValueError('Invalid image size')
size = 224
scale = min(float(size) / w, float(size) / h)
nw = max(1, int(round(w * scale)))
nh = max(1, int(round(h * scale)))
resized = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_AREA)
canvas = np.full((size, size, 3), 127, dtype=np.uint8)
x = (size - nw) // 2
y = (size - nh) // 2
canvas[y:y+nh, x:x+nw] = resized
rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
rgb = (rgb - mean) / std
blob = np.transpose(rgb, (2, 0, 1))[np.newaxis, ...].astype(np.float32)
return blob
def extract_cnn_features(image, net):
"""
MobileNetV2의 1000차원 ImageNet 출력 벡터를
사물의 고수준 CNN 특징으로 사용합니다.
"""
blob = prepare_cnn_input(image)
net.setInput(blob)
output = net.forward().reshape(-1).astype(np.float32)
# 크기/이동에 덜 민감한 비교를 위해 L2 정규화
norm = float(np.linalg.norm(output))
if norm > 0:
output /= norm
return output
def main():
os.makedirs(MODEL_DIR, exist_ok=True)
if not os.path.isdir(DATA_DIR):
print('The obj_data folder does not exist.')
return
class_names = sorted([
name for name in os.listdir(DATA_DIR)
if os.path.isdir(os.path.join(DATA_DIR, name))
])
if not class_names:
print('No class folders were found in obj_data.')
return
data_signature = build_data_signature(class_names)
if can_reuse_existing_model(data_signature):
print('Training images have not changed.')
print('Existing MobileNetV2 CNN v3 model can be reused.')
print('Model file:', MODEL_PATH)
return
try:
net = load_cnn()
except Exception as e:
print(e)
return
print('CNN feature extractor: MobileNetV2 ONNX')
print('Training images are converted into CNN feature vectors.')
names = []
all_features = []
all_labels = []
counts = []
for class_name in class_names:
class_dir = os.path.join(DATA_DIR, class_name)
class_features = []
for file_name in sorted(os.listdir(class_dir)):
if not file_name.lower().endswith(IMAGE_EXTS):
continue
image = cv2.imread(os.path.join(class_dir, file_name))
if image is None:
continue
try:
class_features.append(extract_cnn_features(image, net))
count_now = len(class_features)
if count_now == 1 or count_now % 10 == 0:
print(' {}: {} images processed'.format(class_name, count_now))
except Exception as e:
print('Skipped:', file_name, '/', e)
if not class_features:
print('Skipped empty class:', class_name)
continue
model_class_index = len(names)
names.append(class_name)
counts.append(len(class_features))
for feature in class_features:
all_features.append(feature)
all_labels.append(model_class_index)
print('{}: {} images'.format(class_name, len(class_features)))
if not all_features:
print('No valid images were found.')
return
features = np.stack(all_features).astype(np.float32)
labels = np.array(all_labels, dtype=np.int32)
np.savez_compressed(
MODEL_PATH,
model_version=np.array([3], dtype=np.int32),
feature_type=np.array(['mobilenetv2_logits'], dtype='<U32'),
data_signature=np.array([data_signature], dtype='<U200000'),
names=np.array(names, dtype='<U64'),
features=features,
labels=labels,
counts=np.array(counts, dtype=np.int32),
)
with open(REPORT_PATH, 'w', encoding='utf-8') as report:
report.write('Object model training report - MobileNetV2 CNN v3\n')
report.write('==============================================\n')
for name, count in zip(names, counts):
report.write('{}: {} images\n'.format(name, count))
report.write('\nFeature extractor: MobileNetV2 ONNX / ImageNet pretrained\n')
report.write('Classifier: class-balanced CNN nearest-neighbor scoring\n')
print('Training completed.')
print('Model version: 3 / MobileNetV2 CNN')
print('Model file:', MODEL_PATH)
print('Report file:', REPORT_PATH)
if __name__ == '__main__':
main()
③ 저장하고 nano 종료하기
④ 실행하기
cd ~/object_recognition_raspi_app
python3 1_train_object_model.pymodel/object_model.npz와 model/training_report.txt가 생성됩니다.scikit-learn과 joblib 없이 OpenCV와 NumPy만 사용합니다.실시간 화면 표시와 인식 안정화
속도개선 버전에서 카메라 끊김은 해결되었지만, ROI 위쪽의 인식 결과와 상태 문구가 같은 위치에 겹칠 수 있었습니다. 이번 버전은 인식 결과를 화면 왼쪽 위의 반투명 정보 패널 하나에만 표시합니다.
Object, CNN 유사도, gap을 고정 패널에 두어 ROI 선과 겹치지 않습니다.2_live_object_recognition.py만 교체해 먼저 테스트할 수 있습니다.볼펜처럼 얇고 긴 물체 ROI 개선
기존 자동 ROI는 일정 면적과 가로·세로 크기를 가진 물체를 중심으로 찾았기 때문에, 볼펜처럼 길이는 길지만 폭이 좁은 물체는 경계 후보에서 빠지기 쉬웠습니다.
2_live_object_recognition.py와 3_measure_recognition_rate.py에 모두 적용됩니다.SEARCHING에서 멈추는 오류 수정
얇은 물체 ROI 수정 과정에서 K_PER_CLASS, MIN_SIMILARITY,
MIN_MARGIN 설정값이 7·8단계 코드에서 빠졌습니다. 이 때문에 CNN 백그라운드 스레드에서
NameError가 발생했지만, 기존 코드가 오류를 숨겨 화면에는 계속 SEARCHING만 표시되었습니다.
CNN inference error:로 출력하고
카메라 화면에도 ERROR와 간단한 오류 내용을 표시하도록 바꿨습니다.2_live_object_recognition.py와 3_measure_recognition_rate.py만 교체하면 됩니다.7단계 자동 ROI + 중앙 고정 ROI 절충 방식
7단계에서는 먼저 자동 ROI로 물체를 따라갑니다. 자동 ROI가 여러 프레임 연속으로 물체를 놓치면 8단계에서 안정적으로 사용한 중앙 고정 ROI로 자동 전환합니다.
ROI AUTO 또는 ROI FIXED로 현재 방식을 확인할 수 있습니다.
FIXED 모드에서는 노란색 중앙 사각형과 AUTO ROI lost - use center box 안내가 표시됩니다.2_live_object_recognition.py만 교체하면 됩니다.7단계. 실시간 사물 인식 파일 만들기
① nano로 Python 파일 만들기
cd ~/object_recognition_raspi_app
nano 2_live_object_recognition.py② 전체 코드 복사·붙여넣기
아래 코드 복사하기를 누른 뒤 nano 화면에서 Ctrl + Shift + V로 붙여넣습니다. 환경에 따라 마우스 오른쪽 버튼의 붙여넣기를 사용합니다.
# -*- coding: utf-8 -*-
import os
import time
import threading
from collections import deque
import cv2
import numpy as np
from camera_utils import CameraSource
MODEL_PATH = os.path.join('model', 'object_model.npz')
INFERENCE_INTERVAL = 0.22
ROI_EVERY_N_FRAMES = 2
RESULT_HISTORY = 3
AUTO_ROI_FAIL_LIMIT = 8 # 연속 실패 프레임 수
AUTO_ROI_RECOVER_LIMIT = 5 # 자동 ROI 복귀에 필요한 연속 성공 수
MOBILENET_PATH = os.path.join('model', 'mobilenetv2-7.onnx')
MOBILENET_URL = (
'https://github.com/onnx/models/raw/refs/heads/main/'
'validated/vision/classification/mobilenet/model/mobilenetv2-7.onnx'
)
def ensure_mobilenet_model():
"""MobileNetV2 ONNX 모델이 없으면 한 번만 다운로드합니다."""
os.makedirs('model', exist_ok=True)
if os.path.exists(MOBILENET_PATH) and os.path.getsize(MOBILENET_PATH) > 1000000:
return
print('MobileNetV2 model is not installed.')
print('Downloading about 14 MB from the ONNX Model Zoo...')
try:
import urllib.request
urllib.request.urlretrieve(MOBILENET_URL, MOBILENET_PATH)
except Exception as e:
raise RuntimeError(
'MobileNetV2 download failed. Connect Raspberry Pi to the internet and try again. / '
+ str(e)
)
if (not os.path.exists(MOBILENET_PATH)
or os.path.getsize(MOBILENET_PATH) < 1000000):
raise RuntimeError(
'Downloaded MobileNetV2 file is invalid. Delete model/mobilenetv2-7.onnx and try again.'
)
def load_cnn():
ensure_mobilenet_model()
try:
cv2.setNumThreads(max(1, min(4, os.cpu_count() or 1)))
except Exception:
pass
try:
net = cv2.dnn.readNetFromONNX(MOBILENET_PATH)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
except Exception as e:
raise RuntimeError('OpenCV could not load MobileNetV2 ONNX model. / ' + str(e))
return net
def prepare_cnn_input(image):
"""
MobileNetV2 입력:
RGB 224x224, 0~1 스케일 후 ImageNet mean/std 정규화.
물체 비율은 유지하고 회색 여백을 넣습니다.
"""
h, w = image.shape[:2]
if h <= 0 or w <= 0:
raise ValueError('Invalid image size')
size = 224
scale = min(float(size) / w, float(size) / h)
nw = max(1, int(round(w * scale)))
nh = max(1, int(round(h * scale)))
resized = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_AREA)
canvas = np.full((size, size, 3), 127, dtype=np.uint8)
x = (size - nw) // 2
y = (size - nh) // 2
canvas[y:y+nh, x:x+nw] = resized
rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
rgb = (rgb - mean) / std
blob = np.transpose(rgb, (2, 0, 1))[np.newaxis, ...].astype(np.float32)
return blob
def extract_cnn_features(image, net):
"""
MobileNetV2의 1000차원 ImageNet 출력 벡터를
사물의 고수준 CNN 특징으로 사용합니다.
"""
blob = prepare_cnn_input(image)
net.setInput(blob)
output = net.forward().reshape(-1).astype(np.float32)
# 크기/이동에 덜 민감한 비교를 위해 L2 정규화
norm = float(np.linalg.norm(output))
if norm > 0:
output /= norm
return output
def detect_object_roi(frame, previous_roi=None):
"""
일반 물체 + 얇고 긴 물체를 함께 찾는 자동 ROI.
볼펜처럼 면적은 작지만 길이가 긴 물체는 별도 조건으로 허용합니다.
"""
h, w = frame.shape[:2]
frame_area = float(w * h)
cx_frame = w / 2.0
cy_frame = h / 2.0
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 0)
# 얇은 물체의 양쪽 경계가 끊어지지 않도록 조금 더 강하게 연결
edges = cv2.Canny(gray, 28, 95)
close_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7))
edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, close_kernel, iterations=2)
edges = cv2.dilate(
edges,
cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)),
iterations=1
)
contours, _ = cv2.findContours(
edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
candidates = []
for contour in contours:
contour_area = float(cv2.contourArea(contour))
x, y, rw, rh = cv2.boundingRect(contour)
if rw <= 0 or rh <= 0:
continue
box_area = float(rw * rh)
area_ratio = box_area / frame_area
contour_ratio = contour_area / frame_area
long_side = float(max(rw, rh))
short_side = float(max(1, min(rw, rh)))
elongation = long_side / short_side
# 일반 물체 조건
general_ok = (
area_ratio >= 0.012
and area_ratio <= 0.78
and rw >= 35
and rh >= 35
)
# 볼펜·연필·리모컨처럼 얇고 긴 물체 조건
thin_ok = (
elongation >= 2.2
and long_side >= 95
and short_side >= 8
and area_ratio >= 0.0020
and area_ratio <= 0.28
and contour_ratio >= 0.00045
)
if not (general_ok or thin_ok):
continue
obj_cx = x + rw / 2.0
obj_cy = y + rh / 2.0
dx = (obj_cx - cx_frame) / max(1.0, cx_frame)
dy = (obj_cy - cy_frame) / max(1.0, cy_frame)
center_distance = min(1.0, (dx * dx + dy * dy) ** 0.5)
center_score = 1.0 - center_distance
# 큰 일반 물체와, 충분히 긴 얇은 물체를 모두 살림
area_score = min(1.0, area_ratio / 0.20)
thin_bonus = min(0.30, (elongation - 2.2) * 0.04) if thin_ok else 0.0
score = center_score * 0.62 + area_score * 0.28 + thin_bonus
candidates.append(
(score, x, y, rw, rh, thin_ok, elongation)
)
if candidates:
candidates.sort(key=lambda item: item[0], reverse=True)
_, x, y, rw, rh, is_thin, elongation = candidates[0]
# 얇은 물체는 CNN이 형태를 보면서도 너무 배경만 보지 않도록
# 긴 방향보다 짧은 방향에 여백을 더 줍니다.
if is_thin:
if rw >= rh:
pad_x = int(rw * 0.12)
pad_y = max(35, int(rh * 1.25))
else:
pad_x = max(35, int(rw * 1.25))
pad_y = int(rh * 0.12)
else:
pad_x = int(rw * 0.15)
pad_y = int(rh * 0.15)
x1 = max(0, x - pad_x)
y1 = max(0, y - pad_y)
x2 = min(w, x + rw + pad_x)
y2 = min(h, y + rh + pad_y)
# CNN 입력이 지나치게 좁아지는 것을 방지하는 최소 ROI 크기
min_roi_w = 120
min_roi_h = 120
roi_w = x2 - x1
roi_h = y2 - y1
if roi_w < min_roi_w:
extra = (min_roi_w - roi_w) // 2 + 1
x1 = max(0, x1 - extra)
x2 = min(w, x2 + extra)
if roi_h < min_roi_h:
extra = (min_roi_h - roi_h) // 2 + 1
y1 = max(0, y1 - extra)
y2 = min(h, y2 + extra)
detected_roi = (x1, y1, x2, y2)
# 사각형이 심하게 튀지 않도록 이전 ROI와 부드럽게 연결
if previous_roi is not None:
alpha = 0.48
px1, py1, px2, py2 = previous_roi
detected_roi = (
int(px1 * (1.0 - alpha) + x1 * alpha),
int(py1 * (1.0 - alpha) + y1 * alpha),
int(px2 * (1.0 - alpha) + x2 * alpha),
int(py2 * (1.0 - alpha) + y2 * alpha),
)
return detected_roi, True
# 잠깐 놓쳤으면 직전 ROI를 유지
if previous_roi is not None:
return previous_roi, False
# 최초 탐색용 중앙 가이드 영역
guide_w = int(w * 0.52)
guide_h = int(h * 0.62)
x1 = (w - guide_w) // 2
y1 = (h - guide_h) // 2
return (x1, y1, x1 + guide_w, y1 + guide_h), False
def get_fixed_fallback_roi(frame):
"""
자동 ROI가 불안정할 때 사용하는 중앙 고정 ROI.
8단계 측정 영역과 같은 비율을 사용합니다.
"""
h, w = frame.shape[:2]
roi_w = int(w * 0.62)
roi_h = int(h * 0.62)
x1 = (w - roi_w) // 2
y1 = (h - roi_h) // 2
return (x1, y1, x1 + roi_w, y1 + roi_h)
K_PER_CLASS = 3
MIN_SIMILARITY = 0.35
MIN_MARGIN = 0.008
def predict_object(image, names, train_features, train_labels, net):
feature = extract_cnn_features(image, net)
similarities = np.dot(train_features, feature)
class_scores = []
for class_index, class_name in enumerate(names):
class_sims = similarities[train_labels == class_index]
if class_sims.size == 0:
class_scores.append((-1.0, str(class_name)))
continue
k = min(K_PER_CLASS, class_sims.size)
nearest = np.partition(class_sims, class_sims.size - k)[-k:]
score = float(np.mean(nearest))
class_scores.append((score, str(class_name)))
class_scores.sort(key=lambda item: item[0], reverse=True)
best_score, best_name = class_scores[0]
second_score = class_scores[1][0] if len(class_scores) > 1 else -1.0
margin = best_score - second_score
if best_score < MIN_SIMILARITY or margin < MIN_MARGIN:
return 'UNKNOWN', best_score * 100.0, margin * 100.0
return best_name, best_score * 100.0, margin * 100.0
def resolve_target_name(target, names):
target_key = target.strip().lower()
lookup = {str(name).strip().lower(): str(name) for name in names}
if target_key in lookup:
return lookup[target_key]
# smartphone / smartphones 같은 단순 단수·복수 차이 자동 보정
candidates = []
if target_key.endswith('s'):
candidates.append(target_key[:-1])
else:
candidates.append(target_key + 's')
for candidate in candidates:
if candidate in lookup:
return lookup[candidate]
return None
def load_model():
if not os.path.exists(MODEL_PATH):
raise RuntimeError('The object model was not found. Run 1_train_object_model.py first.')
model = np.load(MODEL_PATH, allow_pickle=False)
if 'features' not in model.files or 'labels' not in model.files:
raise RuntimeError('Old object model detected. Run 1_train_object_model.py again.')
if 'model_version' not in model.files or int(model['model_version'][0]) != 3:
raise RuntimeError(
'This is not the MobileNetV2 CNN v3 model. Run 1_train_object_model.py again.'
)
return model['names'], model['features'], model['labels']
def main():
try:
names, train_features, train_labels = load_model()
net = load_cnn()
except Exception as e:
print(e)
return
camera = CameraSource(640, 480)
previous_roi = None
print('Live object recognition started.')
print('Camera mode:', camera.mode_name())
print('Model: MobileNetV2 CNN v3 - smooth preview mode')
print('Camera preview runs continuously; CNN runs in background.')
print('Put one object near the center. q: quit')
# 공유 상태
state_lock = threading.Lock()
shared_roi_image = [None]
shared_request_id = [0]
last_processed_id = [0]
stop_event = threading.Event()
result = {
'name': 'SEARCHING',
'similarity': 0.0,
'margin': 0.0,
'error': ''
}
recent_results = deque(maxlen=RESULT_HISTORY)
def inference_worker():
"""무거운 CNN 계산을 카메라 표시와 분리합니다."""
while not stop_event.is_set():
roi_image = None
request_id = 0
with state_lock:
if shared_request_id[0] != last_processed_id[0]:
if shared_roi_image[0] is not None:
roi_image = shared_roi_image[0].copy()
request_id = shared_request_id[0]
if roi_image is None:
time.sleep(0.01)
continue
try:
name, similarity, margin = predict_object(
roi_image, names, train_features, train_labels, net
)
with state_lock:
recent_results.append((name, similarity, margin))
vote_counts = {}
for item_name, item_similarity, item_margin in recent_results:
vote_counts[item_name] = vote_counts.get(item_name, 0) + 1
stable_name = max(vote_counts, key=vote_counts.get)
chosen = [
item for item in recent_results
if item[0] == stable_name
]
result['name'] = stable_name
result['similarity'] = sum(item[1] for item in chosen) / float(len(chosen))
result['margin'] = sum(item[2] for item in chosen) / float(len(chosen))
last_processed_id[0] = request_id
except Exception as e:
with state_lock:
result['name'] = 'ERROR'
result['error'] = str(e)
last_processed_id[0] = request_id
print('CNN inference error:', e)
worker = threading.Thread(target=inference_worker, daemon=True)
worker.start()
frame_count = 0
last_request_time = 0.0
auto_fail_count = 0
auto_success_count = 0
use_fixed_fallback = False
try:
while True:
ok, frame = camera.read()
if not ok or frame is None:
break
frame_count += 1
# 자동 ROI를 우선 사용하되, 연속 실패하면 중앙 고정 ROI로 전환합니다.
# 고정 ROI 사용 중에도 자동 ROI 탐색은 계속하여 안정적으로 다시 잡히면 자동 복귀합니다.
auto_box = previous_roi
object_found = False
if previous_roi is None or frame_count % ROI_EVERY_N_FRAMES == 0:
auto_box, object_found = detect_object_roi(frame, previous_roi)
if object_found:
auto_success_count += 1
auto_fail_count = 0
previous_roi = auto_box
else:
auto_fail_count += 1
auto_success_count = 0
else:
# 중간 프레임에서는 직전 상태를 유지
object_found = auto_fail_count == 0
if not use_fixed_fallback and auto_fail_count >= AUTO_ROI_FAIL_LIMIT:
use_fixed_fallback = True
auto_success_count = 0
if use_fixed_fallback and auto_success_count >= AUTO_ROI_RECOVER_LIMIT:
use_fixed_fallback = False
auto_fail_count = 0
if use_fixed_fallback or previous_roi is None:
x1, y1, x2, y2 = get_fixed_fallback_roi(frame)
roi_mode = 'FIXED'
else:
x1, y1, x2, y2 = previous_roi
roi_mode = 'AUTO'
now = time.time()
# CNN에는 최신 ROI만 전달. 이전 추론이 밀려도 큐가 쌓이지 않습니다.
if now - last_request_time >= INFERENCE_INTERVAL:
roi = frame[y1:y2, x1:x2]
if roi.size > 0:
with state_lock:
shared_roi_image[0] = roi.copy()
shared_request_id[0] += 1
last_request_time = now
with state_lock:
name = result['name']
similarity = result['similarity']
margin = result['margin']
error_text = result.get('error', '')
roi_status = roi_mode
if name == 'ERROR':
box_color = (0, 0, 255)
elif name in ('UNKNOWN', 'SEARCHING'):
box_color = (0, 200, 255)
else:
box_color = (0, 255, 0)
if roi_mode == 'FIXED':
# 고정 fallback은 조금 더 굵게 표시
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 255), 3)
cv2.putText(
frame,
'AUTO ROI lost - use center box',
(x1, min(frame.shape[0] - 12, y2 + 24)),
cv2.FONT_HERSHEY_SIMPLEX, 0.48, (0, 255, 255), 2
)
else:
cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 2)
# 글자가 ROI 선이나 다른 문구와 겹치지 않도록
# 화면 왼쪽 위의 고정 정보 패널 하나에만 표시합니다.
overlay = frame.copy()
cv2.rectangle(overlay, (8, 8), (500, 105), (0, 0, 0), -1)
cv2.addWeighted(overlay, 0.62, frame, 0.38, 0, frame)
cv2.putText(
frame,
'Object: {}'.format(name),
(18, 35),
cv2.FONT_HERSHEY_SIMPLEX, 0.72, box_color, 2
)
cv2.putText(
frame,
'CNN {:.1f}% gap {:.2f}% ROI {}'.format(
similarity, margin, roi_status
),
(18, 67),
cv2.FONT_HERSHEY_SIMPLEX, 0.54, (255, 255, 255), 2
)
if error_text:
cv2.putText(
frame,
('ERR: ' + error_text)[:58],
(18, 94),
cv2.FONT_HERSHEY_SIMPLEX, 0.42, (0, 0, 255), 1
)
cv2.imshow('Object Recognition - Raspberry Pi', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
stop_event.set()
worker.join(timeout=1.0)
camera.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
③ 저장하고 nano 종료하기
python3 -m py_compile 2_live_object_recognition.py를 실행하세요. 아무 메시지가 없으면 들여쓰기와 문법이 정상입니다.④ 실행하기
cd ~/object_recognition_raspi_app
python3 2_live_object_recognition.py8단계 인식률 측정은 중앙 고정 ROI 사용
실시간 체험인 7단계는 자동 ROI를 유지하지만, 8단계 인식률 측정은 모든 물체를 동일한 조건으로 비교하기 위해 중앙의 고정 사각형을 사용합니다.
3_measure_recognition_rate.py만 교체하면 됩니다.8단계. 정해진 횟수로 목표 사물 인식률 측정하기
q를 누르면 측정을 중단할 수 있습니다.인식률(%) = 정답 횟수 ÷ 실제 측정 횟수 × 100
① nano로 Python 파일 만들기
cd ~/object_recognition_raspi_app
nano 3_measure_recognition_rate.py
② 전체 코드 복사·붙여넣기
아래 코드 복사하기를 누른 뒤 nano 화면에서 Ctrl + Shift + V로 붙여넣습니다. 환경에 따라 마우스 오른쪽 버튼의 붙여넣기를 사용합니다.
# -*- coding: utf-8 -*-
import os
import time
import cv2
import numpy as np
from camera_utils import CameraSource
MODEL_PATH = os.path.join('model', 'object_model.npz')
ATTEMPTS = 10
PREPARE_SECONDS = 5.0
CAPTURE_SECONDS = 2.5
SAMPLE_INTERVAL = 0.50
MOBILENET_PATH = os.path.join('model', 'mobilenetv2-7.onnx')
MOBILENET_URL = (
'https://github.com/onnx/models/raw/refs/heads/main/'
'validated/vision/classification/mobilenet/model/mobilenetv2-7.onnx'
)
def ensure_mobilenet_model():
"""MobileNetV2 ONNX 모델이 없으면 한 번만 다운로드합니다."""
os.makedirs('model', exist_ok=True)
if os.path.exists(MOBILENET_PATH) and os.path.getsize(MOBILENET_PATH) > 1000000:
return
print('MobileNetV2 model is not installed.')
print('Downloading about 14 MB from the ONNX Model Zoo...')
try:
import urllib.request
urllib.request.urlretrieve(MOBILENET_URL, MOBILENET_PATH)
except Exception as e:
raise RuntimeError(
'MobileNetV2 download failed. Connect Raspberry Pi to the internet and try again. / '
+ str(e)
)
if (not os.path.exists(MOBILENET_PATH)
or os.path.getsize(MOBILENET_PATH) < 1000000):
raise RuntimeError(
'Downloaded MobileNetV2 file is invalid. Delete model/mobilenetv2-7.onnx and try again.'
)
def load_cnn():
ensure_mobilenet_model()
try:
cv2.setNumThreads(max(1, min(4, os.cpu_count() or 1)))
except Exception:
pass
try:
net = cv2.dnn.readNetFromONNX(MOBILENET_PATH)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
except Exception as e:
raise RuntimeError('OpenCV could not load MobileNetV2 ONNX model. / ' + str(e))
return net
def prepare_cnn_input(image):
"""
MobileNetV2 입력:
RGB 224x224, 0~1 스케일 후 ImageNet mean/std 정규화.
물체 비율은 유지하고 회색 여백을 넣습니다.
"""
h, w = image.shape[:2]
if h <= 0 or w <= 0:
raise ValueError('Invalid image size')
size = 224
scale = min(float(size) / w, float(size) / h)
nw = max(1, int(round(w * scale)))
nh = max(1, int(round(h * scale)))
resized = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_AREA)
canvas = np.full((size, size, 3), 127, dtype=np.uint8)
x = (size - nw) // 2
y = (size - nh) // 2
canvas[y:y+nh, x:x+nw] = resized
rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
rgb = (rgb - mean) / std
blob = np.transpose(rgb, (2, 0, 1))[np.newaxis, ...].astype(np.float32)
return blob
def extract_cnn_features(image, net):
"""
MobileNetV2의 1000차원 ImageNet 출력 벡터를
사물의 고수준 CNN 특징으로 사용합니다.
"""
blob = prepare_cnn_input(image)
net.setInput(blob)
output = net.forward().reshape(-1).astype(np.float32)
# 크기/이동에 덜 민감한 비교를 위해 L2 정규화
norm = float(np.linalg.norm(output))
if norm > 0:
output /= norm
return output
def get_fixed_measurement_roi(frame):
"""
8단계 인식률 측정 전용 중앙 고정 ROI.
자동으로 물체를 찾아다니지 않고, 모든 물체를 동일한 조건에서 측정합니다.
"""
h, w = frame.shape[:2]
# 화면 중앙의 넉넉한 고정 측정영역
roi_w = int(w * 0.62)
roi_h = int(h * 0.62)
x1 = (w - roi_w) // 2
y1 = (h - roi_h) // 2
x2 = x1 + roi_w
y2 = y1 + roi_h
return x1, y1, x2, y2
K_PER_CLASS = 3
MIN_SIMILARITY = 0.35
MIN_MARGIN = 0.008
def predict_object(image, names, train_features, train_labels, net):
feature = extract_cnn_features(image, net)
similarities = np.dot(train_features, feature)
class_scores = []
for class_index, class_name in enumerate(names):
class_sims = similarities[train_labels == class_index]
if class_sims.size == 0:
class_scores.append((-1.0, str(class_name)))
continue
k = min(K_PER_CLASS, class_sims.size)
nearest = np.partition(class_sims, class_sims.size - k)[-k:]
score = float(np.mean(nearest))
class_scores.append((score, str(class_name)))
class_scores.sort(key=lambda item: item[0], reverse=True)
best_score, best_name = class_scores[0]
second_score = class_scores[1][0] if len(class_scores) > 1 else -1.0
margin = best_score - second_score
if best_score < MIN_SIMILARITY or margin < MIN_MARGIN:
return 'UNKNOWN', best_score * 100.0, margin * 100.0
return best_name, best_score * 100.0, margin * 100.0
def resolve_target_name(target, names):
target_key = target.strip().lower()
lookup = {str(name).strip().lower(): str(name) for name in names}
if target_key in lookup:
return lookup[target_key]
# smartphone / smartphones 같은 단순 단수·복수 차이 자동 보정
candidates = []
if target_key.endswith('s'):
candidates.append(target_key[:-1])
else:
candidates.append(target_key + 's')
for candidate in candidates:
if candidate in lookup:
return lookup[candidate]
return None
def load_model():
if not os.path.exists(MODEL_PATH):
raise RuntimeError('The object model was not found. Run 1_train_object_model.py first.')
model = np.load(MODEL_PATH, allow_pickle=False)
if 'features' not in model.files or 'labels' not in model.files:
raise RuntimeError('Old object model detected. Run 1_train_object_model.py again.')
if 'model_version' not in model.files or int(model['model_version'][0]) != 3:
raise RuntimeError(
'This is not the MobileNetV2 CNN v3 model. Run 1_train_object_model.py again.'
)
return model['names'], model['features'], model['labels']
def main():
try:
names, train_features, train_labels = load_model()
net = load_cnn()
except Exception as e:
print(e)
return
print('Trained object names:')
for name in names:
print('-', name)
target_input = input('Enter the target object name: ').strip()
target = resolve_target_name(target_input, names)
if target is None:
print('Target name does not match a trained class.')
return
if target.lower() != target_input.lower():
print("Target name auto-corrected: '{}' -> '{}'".format(target_input, target))
camera = CameraSource(640, 480)
correct = 0
print('')
print('Recognition rate test: {} attempts'.format(ATTEMPTS))
print('Model: MobileNetV2 CNN v3')
print('Measurement ROI: fixed center box')
print('Put the ENTIRE object inside the green box before each attempt.')
print('Target:', target)
try:
for attempt in range(1, ATTEMPTS + 1):
print('')
print('Prepare attempt {}/{}.'.format(attempt, ATTEMPTS))
print('You have {:.0f} seconds to place the object.'.format(PREPARE_SECONDS))
# 1) 준비시간: 물체 위치/거리/사각형 확인
prepare_start = time.time()
latest_frame = None
while time.time() - prepare_start < PREPARE_SECONDS:
ok, frame = camera.read()
if not ok or frame is None:
continue
latest_frame = frame
x1, y1, x2, y2 = get_fixed_measurement_roi(frame)
preview = frame.copy()
box_color = (0, 255, 0)
cv2.rectangle(preview, (x1, y1), (x2, y2), box_color, 3)
remaining = max(0.0, PREPARE_SECONDS - (time.time() - prepare_start))
cv2.putText(
preview,
'Prepare {}/{} Target: {}'.format(attempt, ATTEMPTS, target),
(15, 28),
cv2.FONT_HERSHEY_SIMPLEX, 0.58, (255, 255, 255), 2
)
cv2.putText(
preview,
'Place ENTIRE object inside box {:.1f}s'.format(remaining),
(15, 58),
cv2.FONT_HERSHEY_SIMPLEX, 0.58, (255, 255, 0), 2
)
cv2.imshow('Recognition Rate Test - Raspberry Pi', preview)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
print('Measurement stopped by user.')
return
# 2) 여러 프레임 CNN 판정
print('Capturing for {:.1f} seconds...'.format(CAPTURE_SECONDS))
capture_start = time.time()
predictions = []
last_sample_time = 0.0
while time.time() - capture_start < CAPTURE_SECONDS:
ok, frame = camera.read()
if not ok or frame is None:
continue
latest_frame = frame
x1, y1, x2, y2 = get_fixed_measurement_roi(frame)
now = time.time()
if now - last_sample_time >= SAMPLE_INTERVAL:
roi = frame[y1:y2, x1:x2]
if roi.size > 0:
pred_name, pred_similarity, pred_margin = predict_object(
roi, names, train_features, train_labels, net
)
predictions.append(
(pred_name, pred_similarity, pred_margin)
)
last_sample_time = now
preview = frame.copy()
cv2.rectangle(preview, (x1, y1), (x2, y2), (255, 200, 0), 2)
remaining = max(0.0, CAPTURE_SECONDS - (time.time() - capture_start))
cv2.putText(
preview,
'CNN measuring... {:.1f}s'.format(remaining),
(15, 28),
cv2.FONT_HERSHEY_SIMPLEX, 0.62, (255, 255, 0), 2
)
cv2.imshow('Recognition Rate Test - Raspberry Pi', preview)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
print('Measurement stopped by user.')
return
if not predictions:
print('{}: no valid camera samples / wrong'.format(attempt))
continue
vote_counts = {}
for pred_name, pred_similarity, pred_margin in predictions:
vote_counts[pred_name] = vote_counts.get(pred_name, 0) + 1
name = max(vote_counts, key=vote_counts.get)
chosen = [p for p in predictions if p[0] == name]
similarity = sum(p[1] for p in chosen) / float(len(chosen))
margin = sum(p[2] for p in chosen) / float(len(chosen))
votes = vote_counts[name]
total_votes = len(predictions)
is_correct = name.lower() == target.lower()
if is_correct:
correct += 1
print(
'{}: prediction={} / cnn={:.1f}% / gap={:.2f}% / votes={}/{} / {}'.format(
attempt, name, similarity, margin, votes, total_votes,
'correct' if is_correct else 'wrong'
)
)
rate = correct / float(attempt) * 100.0
print('Current rate: {:.2f}% ({}/{})'.format(rate, correct, attempt))
if latest_frame is not None:
x1, y1, x2, y2 = get_fixed_measurement_roi(latest_frame)
result_frame = latest_frame.copy()
result_color = (0, 255, 0) if is_correct else (0, 0, 255)
cv2.rectangle(result_frame, (x1, y1), (x2, y2), result_color, 2)
cv2.putText(
result_frame,
'Prediction: {} CNN {:.1f}% votes {}/{}'.format(
name, similarity, votes, total_votes
),
(15, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, result_color, 2
)
cv2.putText(
result_frame,
'Rate: {:.1f}% ({}/{})'.format(rate, correct, attempt),
(15, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.58, (255, 255, 0), 2
)
cv2.imshow('Recognition Rate Test - Raspberry Pi', result_frame)
cv2.waitKey(800)
finally:
camera.release()
cv2.destroyAllWindows()
final_rate = correct / float(ATTEMPTS) * 100.0
print('')
print('========== Final result ==========')
print('Target:', target)
print('Correct / total: {}/{}'.format(correct, ATTEMPTS))
print('Recognition rate: {:.2f}%'.format(final_rate))
with open('recognition_result.txt', 'w', encoding='utf-8') as f:
f.write('Target: {}\n'.format(target))
f.write('Correct / total: {}/{}\n'.format(correct, ATTEMPTS))
f.write('Recognition rate: {:.2f}%\n'.format(final_rate))
f.write('Model: MobileNetV2 CNN v3\n')
f.write('Measurement ROI: fixed center box\n')
print('Result file: recognition_result.txt')
if __name__ == '__main__':
main()
③ 저장하고 nano 종료하기
④ 문법 검사와 실행
cd ~/object_recognition_raspi_app
python3 -m py_compile 3_measure_recognition_rate.py
python3 3_measure_recognition_rate.py
keyboard, 측정 횟수에 10을 입력하면
키보드를 10번 제시하여 맞힌 횟수와 최종 인식률을 자동 계산합니다.
⑤ 결과 파일 확인
최종 결과와 회차별 예측값은 recognition_result.txt에 저장됩니다.
cd ~/object_recognition_raspi_app
cat recognition_result.txt
========== Final result ==========
Target: keyboard
Correct / total: 8/10
Recognition rate: 80.00%
Result file: recognition_result.txt
물체 크기에 따라 자동으로 변하는 인식 사각형
고정 중앙 사각형 대신 OpenCV 윤곽선 검출로 중앙 부근의 물체를 찾고 크기에 맞춰 ROI를 자동 조절합니다.
물체를 가까이 가져오면 사각형이 커지고 멀리하면 작아집니다.
잘 안 될 때
| 증상 | 확인 및 해결 |
|---|---|
No such file or directory | cd ~/object_recognition_raspi_app 후 ls -la로 파일이 저장되었는지 확인합니다. |
supported=1 detected=0 | 전원을 끈 뒤 CSI 카메라 리본 케이블 방향과 연결 상태를 확인합니다. |
| 카메라 창이 열리지 않음 | raspistill -n -t 1000 -o test.jpg로 사진 촬영을 먼저 시험합니다. |
| 화면이 느림 | raspistill 방식은 USB 웹캠보다 느릴 수 있습니다. |
| 이미지셋을 찾지 못함 | obj_data/keyboard/사진.jpg처럼 사물별 폴더 안에 이미지가 있어야 합니다. |
| 인식 결과가 부정확함 | 배경·거리·각도·밝기가 다른 이미지를 더 준비한 후 모델을 다시 학습합니다. |
mkdir 파일명.py를 실행함 | 파일이 아니라 폴더가 생성됩니다. rmdir 파일명.py로 삭제하고 nano 파일명.py를 실행합니다. |
수업 종료 후 전체 삭제
cd ~
rm -rf ~/object_recognition_raspi_app
ls