"""
영상처리, 인공지능 비전 기술을 사용합니다.
Class:
:obj:`~openpibo.vision_classify.TeachableMachine`
:obj:`~openpibo.vision_classify.CustomClassifier`
"""
import cv2
import os
import json
import numpy as np
import tensorflow as tf
import logging
from PIL import Image # FIX: TeachableMachine.predict()에서 사용하므로 추가
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
os.environ['LIBCAMERA_LOG_LEVELS'] = '3'
tf.get_logger().setLevel(logging.ERROR)
tf.autograph.set_verbosity(0)
logging.getLogger("tensorflow").setLevel(logging.ERROR)
[문서]
class TeachableMachine:
"""
Functions:
:meth:`~openpibo.vision_classify.TeachableMachine.load`
:meth:`~openpibo.vision_classify.TeachableMachine.predict`
파이보의 카메라 Teachable Machine 기능을 사용합니다.
* ``이미지 프로젝트`` 의 ``표준 이미지 모델`` 을 사용합니다.
* ``Teachable Machine`` 에서 학습한 모델을 적용하여 추론할 수 있습니다.
* 학습한 모델은 ``Tensorflow Lite`` 형태로 다운로드 해주세요.
example::
from openpibo.vision_classify import TeachableMachine
tm = TeachableMachine()
# 아래의 모든 예제 이전에 위 코드를 먼저 사용합니다.
"""
[문서]
def load(self, model_path, label_path):
"""
(내부 함수) Tflite 모델로 불러옵니다. (부동소수점/양자화) 모두 가능
example::
tm.load_tflite('model_unquant.tflite', 'labels.txt')
:param str model_path: Teachable Machine의 모델파일
:param str label_path: Teachable Machine의 라벨파일
"""
with open(label_path, 'r') as f:
c = f.readlines()
class_names = [item.split(maxsplit=1)[1].strip('\n') for item in c]
self.interpreter = tf.lite.Interpreter(model_path=model_path)
self.interpreter.allocate_tensors()
self.input_details = self.interpreter.get_input_details()
self.output_details = self.interpreter.get_output_details()
self.floating_model = self.input_details[0]['dtype'] == np.float32
self.height = self.input_details[0]['shape'][1]
self.width = self.input_details[0]['shape'][2]
self.class_names = class_names
[문서]
def predict(self, img):
"""
Tflite 모델로 추론합니다.
example::
cm = Camera()
img = cm.read()
tm.predict(img)
:param numpy.ndarray img: 이미지 객체
:returns: 가장 높은 확률을 가진 클래스 명, 결과(raw 데이터)
"""
try:
img = cv2.cvtColor(cv2.resize(img, (self.width, self.height)), cv2.COLOR_BGR2RGB)
image = Image.fromarray(img) # FIX: PIL.Image import 추가로 정상 동작
input_data = np.expand_dims(image, axis=0)
if self.floating_model:
input_data = (np.float32(input_data) - 127.5) / 127.5
self.interpreter.set_tensor(self.input_details[0]['index'], input_data)
self.interpreter.invoke()
preds = self.interpreter.get_tensor(self.output_details[0]['index'])
preds = np.squeeze(preds)
return self.class_names[np.argmax(preds)], preds
except Exception as ex:
raise Exception('Teachable Machine Model did not load properly.')
[문서]
class CustomClassifier:
"""
Functions:
:meth:`~openpibo.vision_classify.CustomClassifier.load`
:meth:`~openpibo.vision_classify.CustomClassifier.predict`
파이보의 카메라 Classifier 기능을 사용합니다.
* ``이미지 프로젝트`` 의 ``표준 이미지 모델`` 을 사용합니다.
* ``Custom tools`` 에서 학습한 모델을 적용하여 추론할 수 있습니다.
example::
from openpibo.vision_classify import CustomClassifier
cf = CustomClassifier()
# 아래의 모든 예제 이전에 위 코드를 먼저 사용합니다.
"""
[문서]
def load(self, model_path, label_path):
"""
keras 모델로 불러옵니다.
example::
cf.load('model.keras', 'labels.txt')
:param str model_path: Classifier의 모델파일
:param str label_path: Classifier의 라벨파일
"""
self.model = tf.keras.models.load_model(model_path)
with open(label_path, "r", encoding="utf-8") as f:
self.class_names = [line.strip() for line in f.readlines()]
base_model = tf.keras.applications.MobileNetV2(input_shape=(224, 224, 3), include_top=False, pooling="avg")
self.feature_extractor = tf.keras.Model(inputs=base_model.input, outputs=base_model.output)
[문서]
def predict(self, img):
"""
keras 모델로 추론합니다.
example::
cm = Camera()
img = cm.read()
cf.predict(img)
:param numpy.ndarray img: 이미지 객체
:returns: 가장 높은 확률을 가진 클래스 명, 결과(raw 데이터)
"""
try:
img = cv2.resize(img, (224, 224))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype("float32") / 255.0
img = np.expand_dims(img, axis=0)
# FIX: .predict() → __call__(training=False)
# 단건 추론 시 .predict()의 배치 처리 오버헤드 제거 → 지연시간 감소
features = self.feature_extractor(img, training=False)
preds = self.model(features, training=False).numpy()
pred_index = np.argmax(preds)
name = self.class_names[pred_index]
return name, preds[0]
except Exception as ex:
raise Exception('Classifier Model did not load properly.')
[문서]
def convert_tfjs_to_keras(self, model_path, output_path):
"""
Load a TensorFlow.js model (model.json + weights.bin) and convert it to a Keras H5 model.
"""
model_json_path = os.path.join(model_path, "model.json")
weights_spec_path = os.path.join(model_path, "weightsSpecs.json")
weights_bin_path = os.path.join(model_path, "weights.bin")
if not os.path.exists(model_json_path):
raise FileNotFoundError(f"❌ Error: {model_json_path} 파일을 찾을 수 없습니다.")
if not os.path.exists(weights_spec_path):
raise FileNotFoundError(f"❌ Error: {weights_spec_path} 파일을 찾을 수 없습니다.")
if not os.path.exists(weights_bin_path):
raise FileNotFoundError(f"❌ Error: {weights_bin_path} 파일을 찾을 수 없습니다.")
with open(model_json_path, "r") as f:
model_config = json.load(f)
model = tf.keras.models.model_from_json(json.dumps(model_config))
with open(weights_spec_path, "r") as f:
weights_specs = json.load(f)
with open(weights_bin_path, "rb") as f:
binary_data = f.read()
weight_arrays = []
offset = 0
try:
for spec in weights_specs:
shape = tuple(spec["shape"])
dtype = np.dtype(spec["dtype"])
size = np.prod(shape)
byte_offset = spec.get("byte_offset", offset)
np_array = np.frombuffer(binary_data, dtype=dtype, count=size, offset=byte_offset).reshape(shape)
weight_arrays.append(np_array)
offset += size * dtype.itemsize
except Exception as e:
raise ValueError(f"❌ Error: 가중치 로드 중 오류 발생 - {str(e)}")
model_weights = model.get_weights()
if len(weight_arrays) != len(model_weights):
print(f"⚠️ Warning: 가중치 개수가 맞지 않습니다! (기대 값: {len(model_weights)}, 받은 값: {len(weight_arrays)})")
model.set_weights(weight_arrays)
model.save(output_path)
print(f"✅ TFJS → KERAS 변환 완료: {output_path}")
return model