st-ten-1/src/components/vision.py

361 lines
15 KiB
Python
Raw Normal View History

2022-06-21 12:10:52 +00:00
import os
import sys
import traceback
from configparser import ConfigParser
from pathlib import Path
2022-06-27 09:03:26 +00:00
import cv2
2022-06-21 12:10:52 +00:00
import numpy
import numpy as np
import tensorflow as tf
from lib.helpers.object_detection.utils import label_map_util
2022-06-22 15:18:29 +00:00
from PyQt5.QtCore import (QFileSystemWatcher, QMutex, QThread, QTimer,
pyqtSignal)
2022-06-21 12:10:52 +00:00
from PyQt5.QtGui import QColor
from .component import Component
2022-06-22 15:18:29 +00:00
from .consumer import Consumer
2022-06-21 12:10:52 +00:00
if "--no-edgetpu" not in sys.argv:
if "--no-tflite" not in sys.argv:
from pycoral.utils.edgetpu import make_interpreter
else:
def make_interpreter(*args, **kwargs):
raise ValueError("\"--no-edgetpu\" in sys.argv")
if "--no-tflite" not in sys.argv:
from pycoral.adapters import detect
from tflite_runtime.interpreter import Interpreter
else:
def Interpreter(*args, **kwargs):
raise ValueError("\"--no-tflite\" in sys.argv")
# os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
#
# # Patch the location of gfile
# tf.gfile = tf.io.gfile
class Vision(Component):
status_signal = pyqtSignal(dict)
loading_model_signal = pyqtSignal(dict)
2022-06-22 15:18:29 +00:00
def __init__(self, config=None, name=None, period=None, lazy=True, paused=False, threaded=True):
2022-06-21 12:10:52 +00:00
super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded)
self.lock = QMutex()
self.simulate = "--sim-vision" in sys.argv
2022-06-22 15:18:29 +00:00
def start(self):
2022-06-21 12:10:52 +00:00
self.model = None
2022-06-22 15:18:29 +00:00
self.consumer = Consumer(work=self.check_features, work_fifo=True, drop_fifo=True, work_maxlen=1, name="vision_consumer", paused=False)
self.consumer_thread = QThread()
self.consumer_thread.setTerminationEnabled(True)
self.consumer.moveToThread(self.consumer_thread)
self.consumer_thread.started.connect(self.consumer.start)
self.consumer_thread.start()
self.consumer.wait_ready()
self.consumer.out.connect(self.process_consumed)
super().start()
2022-06-21 12:10:52 +00:00
def config_changed(self):
# OBJECT DETECTION
self.detection_threshold = float(self.config[self.name]["detection_threshold"])
# recipe
self.zones = None
self.labels = None
# LOAD RECIPE
self.recipes_dir = Path(self.config[self.name].get("recipes_dir", "./config/vision/recipes"))
self.set_recipe(None)
self.recipe_watcher = QFileSystemWatcher([])
self.recipe_watcher.fileChanged.connect(self._set_recipe)
# LOAD MODEL
self.models_dir = Path(self.config[self.name].get("models_dir", "./data/neural_networks"))
self.allowed_modes = dict.fromkeys([
"edgetpu",
"tflite_cpu",
"normal",
])
if "--no-edgetpu" in sys.argv:
self.allowed_modes.pop("edgetpu", None)
if "--no-tflite" in sys.argv:
self.allowed_modes.pop("edgetpu", None)
self.allowed_modes.pop("tflite_cpu", None)
self.load_model(self.config[self.name].get("neural_network", None))
# LOAD LABELS
label_map = label_map_util.load_labelmap("./config/vision/labels/labels.pbtxt")
self.num_classes = len(label_map.item)
categories = label_map_util.convert_label_map_to_categories(label_map, max_num_classes=self.num_classes, use_display_name=True)
self.category_index = label_map_util.create_category_index(categories)
self.classes_map = {c["name"]: k for k, c in self.category_index.items()}
def clear_recipe(self):
self.recipe = None
self.points = {}
self.zones = {}
self.labels = {}
def _set_recipe(self, recipe_path):
recipe_path = str(recipe_path)
self.log.info(f"changing recipe to {recipe_path!r}")
watched = self.recipe_watcher.files()
if recipe_path == "" and len(watched) == 0: # skip bad watcher signals
return
if len(watched) > 0:
self.recipe_watcher.removePaths(watched)
self.recipe_path = recipe_path
try:
if not os.path.isfile(self.recipe_path):
raise AssertionError(f"Recipe file {self.recipe_path!r} could not be found.")
config = ConfigParser(inline_comment_prefixes="#")
read = config.read(self.recipe_path)
if len(read) != 1 or self.recipe_path not in read:
raise AssertionError("Recipe could not be read.")
os.path.splitext(os.path.basename(read[0]))[0]
self.points = self.parse_points(config.get["shapes"])
self.zones = self.parse_zones(config.get["zones"])
self.labels = self.parse_labels(config.get("labels", None))
self.recipe_watcher.addPath(str(self.recipe_path))
except Exception:
self.log.exception(traceback.format_exc())
self.log.exception(f"Error reading {self.recipe_path!r}:")
self.clear_recipe()
self.status_signal.emit({
"recipe": self.recipe,
"points": self.points,
"zones": self.zones,
"labels": self.labels,
})
def set_recipe(self, recipe=None):
if recipe is None:
self.clear_recipe()
else:
self._set_recipe(self.recipes_dir / str(recipe))
def parse_points(self, config=None):
if config is None:
raise AssertionError(f"Recipe file {self.recipe_path!r} does not contain the 'shapes' section.")
config = {}
points = {}
for point_name, point_spec in config.items():
try:
center, size, fill_color, border_color, border_thickness, shape = point_spec.split(" ")
center = list(map(float, center.split(",")))
center[1] = -center[1]
size = list(map(float, size.split(",")))
if len(size) == 1:
size = [size[0], size[0]]
fill_color = QColor(fill_color.replace("0x", "#"))
border_color = QColor(border_color.replace("0x", "#"))
border_thickness = float(border_thickness)
shape = shape.lower()
points[point_name] = {
"center": center,
"size": size,
"fill_color": fill_color,
"border_color": border_color,
"border_thickness": border_thickness,
"shape": shape,
}
except Exception:
self.log.exception(traceback.format_exc())
self.log.exception(f"point {point_name!r} in recipe file {self.recipe_path!r} could not be parsed. spec: {point_spec!r}")
return points
def parse_zones(self, config=None):
if config is None:
raise AssertionError(f"Recipe file {self.recipe_path!r} does not contain the 'zones' section.")
config = {}
zones = {}
for zone_name, zone_spec in config.items():
zone_name = zone_name.upper()
try:
center, margin, d_class = zone_spec.split(" ")
center = list(map(float, center.split(",")))
center[1] = -center[1]
if margin == "none":
margin = None
elif "," in margin:
margin = list(map(float, margin.split(",")))
else:
margin = float(margin)
zones[zone_name] = {
"center": center,
"margin": margin,
"class": self.category_index[self.classes_map[d_class]],
}
except Exception:
self.log.exception(traceback.format_exc())
self.log.exception(f"region {zone_name!r} in recipe file {self.recipe_path!r} could not be parsed. spec: {zone_spec!r}")
return zones
def parse_labels(self, config=None):
if config is None:
config = {}
labels = {}
for label_name, label_spec in config.items():
try:
location, font_size, fill_color, border_color, border_thickness, text = label_spec.split(" ", 5)
location = list(map(float, location.split(",")))
location[1] = -location[1]
font_size = float(font_size)
fill_color = QColor(fill_color.replace("0x", "#"))
border_color = QColor(border_color.replace("0x", "#"))
border_thickness = float(border_thickness)
text = text.replace("\\n", "\n").replace("\\t", "\t")
labels[label_name] = {
"location": location,
"font_size": font_size,
"fill_color": fill_color,
"border_color": border_color,
"border_thickness": border_thickness,
"text": text,
}
except Exception:
self.log.exception(traceback.format_exc())
self.log.exception(f"label {label_name!r} in recipe file {self.recipe_path!r} could not be parsed. spec: {label_spec!r}")
return labels
def zone_center(self, zone):
return (int((zone["xmax"] + zone["xmin"]) / 2), int((zone["ymax"] + zone["ymin"]) / 2))
def get_center(self, rect):
return [(rect[0] + rect[2]) / 2, (rect[1] + rect[3]) / 2]
def get_distance(self, p1, p2):
return pow((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2, 1 / 2)
def load_model(self, model=None):
2022-06-22 15:18:29 +00:00
# print("VISION CONSUMER", str(int(QThread.currentThreadId())), flush=True)
2022-06-21 12:10:52 +00:00
self.log.info(f"requested neural network: {model!r}")
if model is None or model.lower() in [
"",
"any",
"last",
"latest",
"newest",
"none",
]:
model_name = sorted([d for d in os.listdir(self.models_dir) if os.path.isdir(self.models_dir / d)], reverse=True)[0]
self.log.info(f"loading neural network: {model_name!r}")
self.loading_model_signal.emit({"status": "loading"})
self.lock.lock()
tf_mode = None
# reset tflite variables
interpreter = None
if tf_mode is None and "edgetpu" in self.allowed_modes:
try:
# create tflite edgetpu interpreter
2022-06-27 09:03:26 +00:00
interpreter = make_interpreter(str(self.models_dir / model_name / f"{model_name}_edgetpu.tflite"))
2022-06-21 12:10:52 +00:00
tf_mode = "edgetpu"
except Exception:
self.log.exception(traceback.format_exc())
if tf_mode is None and "tflite_cpu" in self.allowed_modes:
try:
# create tflite cpu interpreter
2022-06-27 09:03:26 +00:00
interpreter = Interpreter(str(self.models_dir / model_name / f"{model_name}.tflite"))
2022-06-21 12:10:52 +00:00
tf_mode = "tflite_cpu"
except Exception:
self.log.exception(traceback.format_exc())
# reset tensorflow variables
model = None
if tf_mode is None and "normal" in self.allowed_modes:
try:
# create tensorflow model
2022-06-27 09:03:26 +00:00
model = tf.saved_model.load(str(self.models_dir / model_name))
2022-06-21 12:10:52 +00:00
tf_mode = "normal"
except Exception:
self.log.exception(traceback.format_exc())
self.lock.unlock()
if tf_mode is None:
raise RuntimeError("failed initialize any neural network model")
self.tf_mode = tf_mode
self.model_name = model_name
if interpreter is not None:
# if there is a new tflite interpreter initialize it and the related values
self.tflite_input_details = interpreter.get_input_details()
self.tflite_output_details = interpreter.get_output_details()
self.inf_index = self.tflite_input_details[0]["index"]
2022-06-27 09:03:26 +00:00
self.inf_shape = self.tflite_input_details[0]["shape"]
# interpreter.resize_tensor_input(self.inf_index, self.inf_shape)
interpreter.allocate_tensors()
interpreter.invoke() # warmup
2022-06-21 12:10:52 +00:00
else:
self.tflite_input_details = None
self.tflite_output_details = None
self.inf_index = None
2022-06-27 09:03:26 +00:00
self.inf_shape = None
2022-06-21 12:10:52 +00:00
self.interpreter = interpreter
# if there is a new model to be used, remove previous model if present
if model is not None and self.model is not None:
tf.keras.backend.clear_session()
self.model = model
self.log.info(f"initialized model {self.model!r} with mode {self.tf_mode!r}")
self.loading_model_signal.emit({"status": "done"})
def check_features(self, image, lock=True):
2022-06-27 09:03:26 +00:00
if image.shape != self.inf_shape[1:3]:
image = cv2.resize(image, self.inf_shape[1:3], interpolation=cv2.INTER_LINEAR)
2022-06-21 12:10:52 +00:00
tensor = np.expand_dims(np.asarray(image), axis=0)
# Run inference
if lock:
self.lock.lock()
if self.tf_mode in {"edgetpu", "tflite_cpu"}:
self.interpreter.set_tensor(self.inf_index, tensor)
self.interpreter.invoke()
objs = detect.get_objects(self.interpreter, self.detection_threshold, (1, 1))
if lock:
self.lock.unlock()
2022-06-27 09:03:26 +00:00
boxes = [(obj.bbox.ymin / self.inf_shape[1], obj.bbox.xmin / self.inf_shape[2], obj.bbox.ymax / self.inf_shape[1], obj.bbox.xmax / self.inf_shape[2]) for obj in objs]
2022-06-21 12:10:52 +00:00
classes = [obj.id + 1 for obj in objs]
scores = [obj.score for obj in objs]
detections = {
2022-06-27 09:03:26 +00:00
"detection_boxes": [boxes],
"detection_classes": [classes],
"detection_scores": [scores],
2022-06-21 12:10:52 +00:00
}
else:
detections = self.model(tensor)
if lock:
self.lock.unlock()
2022-06-27 09:03:26 +00:00
# WARNING: results other than the ones related to tensor[-1] will be discarded
2022-06-21 12:10:52 +00:00
parsed_detections = []
2022-06-27 09:03:26 +00:00
for d_box, d_class, d_score in zip( # , d_mask in zip(
detections["detection_boxes"][-1],
detections["detection_classes"][-1],
detections["detection_scores"][-1],
# detections["detection_masks"][-1],
2022-06-21 12:10:52 +00:00
):
if d_score < self.detection_threshold:
continue
2022-06-27 09:03:26 +00:00
box = list(d_box)
# box = d_box.numpy().tolist()
center = self.get_center(box)
2022-06-21 12:10:52 +00:00
detection = {
2022-06-27 09:03:26 +00:00
"class": self.category_index[int(d_class)],
"score": float(d_score),
# "score": d_score.numpy().tolist(),
# "mask": d_mask.numpy().tolist(),
"box": box,
2022-06-21 12:10:52 +00:00
"center": center,
}
parsed_detections.append(detection)
2022-06-27 09:03:26 +00:00
return {"result": parsed_detections, "shape": tensor[-1].shape, "ok": False, "tensor": tensor[-1]}
2022-06-21 12:10:52 +00:00
def _get(self, data):
# print("VISION", str(int(QThread.currentThreadId())), flush=True)
2022-06-22 15:18:29 +00:00
self.consumer.add_consumable(data[-1][list(self.sources)[0]])
super()._get(emit=False)
def process_consumed(self, data=None):
# print("VISION", str(int(QThread.currentThreadId())), flush=True)
if data is None:
2022-06-21 12:10:52 +00:00
return
2022-06-22 15:18:29 +00:00
data = data[-1][self.consumer.name]
if data is not None:
super()._get([{
"frame": data["consumed"],
"vision": data["result"],
}])