diff --git a/config/machine_settings/defaults.ini b/config/machine_settings/defaults.ini index 6e689c4..8be6fe5 100644 --- a/config/machine_settings/defaults.ini +++ b/config/machine_settings/defaults.ini @@ -44,7 +44,4 @@ port: COM1 baudrate: 9600 [vision] -detection_threshold: 0.5 neural_network: hs5-20000 - -; recipes_path: ./config/vision_test_recipes diff --git a/config/vision/recipes/termorestringente_923578.ini b/config/vision/recipes/termorestringente_923578.ini index 9f9a83d..861193a 100644 --- a/config/vision/recipes/termorestringente_923578.ini +++ b/config/vision/recipes/termorestringente_923578.ini @@ -22,7 +22,7 @@ instruction: CONTROLLARE PRESENZA TERMORESTRINGENTE [markers] [zones] -p1: 1000,500 800,800 hs-ok # HEATSINK PRESENT +p1: 1200,600 1800,1200 hs-ok # HEATSINK PRESENT [labels] p1: 660,1200 120 0xffffffff 0xff000000 4 TERMORESTRINGENTE diff --git a/simulate.sh b/simulate.sh index 3406f97..cad15a1 100755 --- a/simulate.sh +++ b/simulate.sh @@ -31,6 +31,7 @@ $* 2> >(sed $'s/.*/\e[31m&\e[m/' >&2) # & # --auto-login-user \ # --autotests-archive \ # --camera-edits \ +# --fail-vision \ # --full-screen \ # --interact \ # --maximized \ diff --git a/src/components/tecna_marposs_provaset_t3p_registers.py b/src/components/tecna_marposs_provaset_t3p_registers.py index 9ab50ce..9c9195c 100644 --- a/src/components/tecna_marposs_provaset_t3p_registers.py +++ b/src/components/tecna_marposs_provaset_t3p_registers.py @@ -49,7 +49,7 @@ registers = { 180: "END SEQUENCE PROGRAM", 190: "END PLUG", 200: "END CAGE", - 210: "FINE TEST", # WAITING THE START OF A NEW TEST", + 210: "FINE TEST", # "WAITING THE START OF A NEW TEST", }, }], "Running test: phase backwards time": [33 - 1, {"dt": "16bit_uint", "f": 26, }], "Running test: filling pressure": [34 - 1, {"dt": "32bit_int", "f": 21, }], diff --git a/src/components/vision.py b/src/components/vision.py index 378c0a7..5bdff57 100644 --- a/src/components/vision.py +++ b/src/components/vision.py @@ -47,6 +47,11 @@ else: def Interpreter(*args, **kwargs): raise ValueError("\"--no-tflite\" in sys.argv") +if "--fail-vision" not in sys.argv: + vision_override = None +else: + vision_override = False + # # Patch the location of gfile # tf.gfile = tf.io.gfile @@ -95,7 +100,7 @@ class Vision(Component): def config_changed(self): # OBJECT DETECTION - self.detection_threshold = float(self.config[self.name]["detection_threshold"]) + self.detection_threshold = float(self.config[self.name].get("detection_threshold", 0.5)) # recipe self.zones = None self.labels = None @@ -122,20 +127,26 @@ class Vision(Component): 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) + for k in self.category_index: + self.category_index[k]["color"] = self.category_index[k]["color"].replace("0x", "#") self.classes_map = {c["name"]: k for k, c in self.category_index.items()} - self.zone_detection_filter_mode = self.config[self.name].get("zone_detection_filter_mode", "box_touches") - self.zone_detection_preference_mode = self.config[self.name].get("zone_detection_preference_mode", "distance") + self.zone_detection_filter_mode = self.config[self.name].get("zone_detection_filter_mode", "box_inside") + self.zone_detection_preference_mode = self.config[self.name].get("zone_detection_preference_mode", "score") - def get_center(self, rect): + @staticmethod + def get_center(rect): return [(rect[0] + rect[2]) / 2, (rect[1] + rect[3]) / 2] - def get_size(self, rect): + @staticmethod + def get_size(rect): return [rect[2] - rect[0], rect[3] - rect[1]] - def get_box(self, center, size): + @staticmethod + def get_box(center, size): return [center[0] - size[0] / 2, center[1] - size[1] / 2, center[0] + size[0] / 2, center[1] + size[1] / 2] - def get_distance(self, p1, p2): + @staticmethod + def get_distance(p1, p2): return pow((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2, 1 / 2) def clear_recipe(self): @@ -180,6 +191,7 @@ class Vision(Component): if recipe is None: self.clear_recipe() else: + self.recipe = recipe self._set_recipe(self.recipes_dir / str(recipe)) def parse_markers(self, config=None): @@ -229,9 +241,10 @@ class Vision(Component): shape = "ellipse" d_class = self.category_index[self.classes_map[d_class]] zones[zone_name] = { - "border_color": QColor(d_class["color"].replace("0x", "#")), + "border_color": QColor(d_class["color"]), "border_thickness": 25, "box": self.get_box(center, size), + "convert_negative_placement": False, "center": center, "class": d_class, "fill_color": QColor("#00000000"), @@ -426,22 +439,23 @@ class Vision(Component): style = { "border_thickness": 25, "fill_color": QColor("#00000000"), - "shape": "ellipse", + "shape": "rect", + "convert_negative_placement": False, } items = {} for item_name, item in enumerate(detections): items[str(item_name)] = { - "box": item["box"], + **item, **style, - "border_color": QColor(item["class"]["color"].replace("0x", "#")), + "border_color": QColor(item["class"]["color"]), } return items else: return {} def process_detections(self, detections): - if self.zones is None or not len(self.zones) or detections is None or not len(detections): - return {} + if self.zones is None or not len(self.zones): + return None # MATCH DETECTIONS WITH RECIPE results = dict.fromkeys(self.zones) for detection in detections: @@ -533,8 +547,13 @@ class Vision(Component): "expected": expected_class, "detection": detection, } + global vision_override + if vision_override is None: + ok = all(map(lambda detection: detection["ok"] is True, checked.values())) + else: + ok = vision_override return { - "ok": all(map(lambda detection: detection["ok"] is True, checked.values())), + "ok": ok, "results": checked, } @@ -542,6 +561,7 @@ class Vision(Component): # DRAW ZONES RESULTS if self.zones is not None and len(self.zones) and results is not None and len(results): style = { + "pen_line": "SolidLine", "border_thickness": 50, "fill_color": QColor("#00000000"), } @@ -561,9 +581,7 @@ class Vision(Component): for item_name, item in results["results"].items(): zone = self.zones[item_name] items[str(item_name)] = { - "center": zone["center"], - "size": zone["size"], - "shape": zone["shape"], + **zone, **style, "border_color": Qt.green if item["ok"] else Qt.red, } @@ -571,9 +589,30 @@ class Vision(Component): else: return {} + @staticmethod + def convert_negative_placement(p, painter): + for k in p: + if p[k] < 0: + if k.startswith("x") or k.startswith("w"): + p[k] += painter.device().width() + elif k.startswith("y") or k.startswith("h"): + p[k] += painter.device().height() + else: + raise AssertionError("could not detect variable direction") + + @staticmethod + def apply_placement_offset(p, offset): + for k in p: + if k.startswith("x"): + p[k] += offset["x"] + elif k.startswith("y"): + p[k] += offset["y"] + else: + raise AssertionError("could not detect variable direction") + def render_items(self, items, offset=None, qimage=None, painter=None): if offset is None: - offset = [0, 0] + offset = {"x": 0, "y": 0} if painter is None: if qimage is None: raise AssertionError("one of 'qimage' or 'painter' parameter must not be None") @@ -581,35 +620,36 @@ class Vision(Component): painter.begin(qimage) for item_name, item in items.items(): try: - v = {} + convert_negative_placement = item.get("convert_negative_placement", True) if "box" in item: - v["x1"], v["y1"], v["x2"], v["y2"] = item["box"][1] + offset[1], item["box"][0] + offset[0], item["box"][3] + offset[1], item["box"][2] + offset[0] - v["w"], v["h"] = v["x2"] - v["x1"], v["y2"] - v["y1"] - v["xc"], v["yc"] = self.get_center([v["x1"], v["y1"], v["x2"], v["y2"]]) + p = {"x1": item["box"][1], "y1": item["box"][0], "x2": item["box"][3], "y2": item["box"][2], } + if convert_negative_placement: + Vision.convert_negative_placement(p, painter) + Vision.apply_placement_offset(p, offset) + p["w"], p["h"] = p["x2"] - p["x1"], p["y2"] - p["y1"] + p["xc"], p["yc"] = Vision.get_center([p["x1"], p["y1"], p["x2"], p["y2"]]) elif "location" in item: - v["x1"], v["y1"] = item["location"][1] + offset[1], item["location"][0] + offset[0] + p = {"x1": item["location"][1], "y1": item["location"][0], } + if convert_negative_placement: + Vision.convert_negative_placement(p, painter) + Vision.apply_placement_offset(p, offset) if "size" in item: - v["w"], v["h"] = item["size"][1], item["size"][0] - v["x2"], v["y2"] = v["x1"] + v["w"], v["y1"] + v["h"] - v["xc"], v["yc"] = self.get_center([v["x1"], v["y1"], v["x2"], v["y2"]]) + p["w"], p["h"] = item["size"][1], item["size"][0] + p["x2"], p["y2"] = p["x1"] + p["w"], p["y1"] + p["h"] + p["xc"], p["yc"] = Vision.get_center([p["x1"], p["y1"], p["x2"], p["y2"]]) else: - v["w"], v["h"] = 0, 0 - v["x2"], v["y2"] = v["x1"], v["y1"] - v["xc"], v["yc"] = v["x1"], v["y1"] + p["w"], p["h"] = 0, 0 + p["x2"], p["y2"] = p["x1"], p["y1"] + p["xc"], p["yc"] = p["x1"], p["y1"] elif "center" in item and "size" in item: - v["xc"], v["yc"] = item["center"][1] + offset[1], item["center"][0] + offset[0] - v["w"], v["h"] = item["size"][1], item["size"][0] - v["x1"], v["y1"], v["x2"], v["y2"] = self.get_box([v["xc"], v["yc"]], [v["w"], v["h"]]) + p = {"xc": item["center"][1], "yc": item["center"][0], } + if convert_negative_placement: + Vision.convert_negative_placement(p, painter) + Vision.apply_placement_offset(p, offset) + p["w"], p["h"] = item["size"][1], item["size"][0] + p["x1"], p["y1"], p["x2"], p["y2"] = Vision.get_box([p["xc"], p["yc"]], [p["w"], p["h"]]) else: raise AssertionError("item has no valid positioning information") - for k in list(v): - if v[k] < 0: - if k.startswith("x") or k.startswith("w"): - v[k] = painter.device().width() + v[k] - elif k.startswith("y") or k.startswith("h"): - v[k] = painter.device().height() + v[k] - else: - raise AssertionError("could not detect variable direction") painter.setOpacity(item.get("opacity", 0.5)) painter.setBrush(QBrush(item.get("fill_color", QColor("#ffffff")), getattr(Qt, item.get("brush_pattern", "SolidPattern")))) painter.setPen(QPen( @@ -620,14 +660,14 @@ class Vision(Component): getattr(Qt, item.get("pen_join", "MiterJoin")), )) if item["shape"] == "ellipse": - painter.drawEllipse(QPointF(v["xc"], v["yc"]), v["w"] / 2, v["h"] / 2) + painter.drawEllipse(QPointF(p["xc"], p["yc"]), p["w"] / 2, p["h"] / 2) elif item["shape"] == "cross": - painter.drawLine(QLineF(v["xc"], v["y1"], v["xc"], v["y2"])) - painter.drawLine(QLineF(v["x1"], v["yc"], v["x2"], v["yc"])) + painter.drawLine(QLineF(p["xc"], p["y1"], p["xc"], p["y2"])) + painter.drawLine(QLineF(p["x1"], p["yc"], p["x2"], p["yc"])) elif item["shape"] == "line": - painter.drawLine(QLineF(v["xc"], v["yc"], v["x2"], v["y2"])) + painter.drawLine(QLineF(p["xc"], p["yc"], p["x2"], p["y2"])) elif item["shape"] == "rect": - painter.drawRect(QRectF(v["x1"], v["y1"], v["x2"], v["y2"])) + painter.drawRect(QRectF(QPointF(p["x1"], p["y1"]), QPointF(p["x2"], p["y2"]))) elif item["shape"] == "text": old_render_hints = painter.renderHints() painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing, True) @@ -640,7 +680,7 @@ class Vision(Component): font.setWordSpacing(item.get("font_word_spacing", 0)) font.setPixelSize(round(item.get("font_size", 25))) path = QPainterPath() - path.addText(v["x1"], v["y1"], font, item["text"]) + path.addText(p["x1"], p["y1"], font, item["text"]) painter.drawPath(path) painter.setRenderHints(old_render_hints, True) else: diff --git a/src/components/vision_saver.py b/src/components/vision_saver.py index e5e25ac..1428572 100755 --- a/src/components/vision_saver.py +++ b/src/components/vision_saver.py @@ -1,4 +1,5 @@ import glob +import json import os import shutil from datetime import datetime @@ -26,34 +27,60 @@ class VisionSaver(Component): self.minimum_disk_free_space_gb = float(self.minimum_disk_free_space_gb) self.time_format = self.config[self.name]["time_format"] - def save(self, save_time, img, mask=True, resize=True): - if type(save_time) is float: - save_time = int(save_time) - if type(save_time) is int: - save_time = datetime.fromtimestamp(save_time) + def save(self, save_time=None, suffix=None, frame=None, vision=None, resize=None, mask=None): + self.remove_older_images_if_needed() + if type(save_time) is None: + save_time = datetime.now() + else: + if type(save_time) is float: + save_time = int(save_time) + if type(save_time) is int: + save_time = datetime.fromtimestamp(save_time) if type(save_time) is not datetime: raise ValueError(f"save_time must be float int or datetime, not {type(save_time)}") timestamp = save_time.strftime(self.time_format) save_dir = self.location / save_time.strftime("%Y") / save_time.strftime("%m") os.makedirs(save_dir, exist_ok=True) - out_path = save_dir / f"{timestamp}.png" - self.log.info(f"saving {out_path}") - img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - if resize and self.resize_resolution is not None: - img = cv2.resize(img, self.resize_resolution, interpolation=cv2.INTER_LINEAR) - if mask and self.mask_zones is not None: - height, width, channels = img.shape - out = np.full( - [height, width, channels], - [0] * channels - ) - for zone_name in self.mask_zones: - zone = self.bench.zones[zone_name]["box"] - out[zone[1]:zone[3], zone[0]:zone[2]] = img[zone[1]:zone[3], zone[0]:zone[2]] - else: - out = img - cv2.imwrite(str(out_path), out) - return out_path + out_paths = [] + if frame is not None: + if suffix is not None: + out_paths.append(save_dir / f"{timestamp}.{suffix}.png") + else: + out_paths.append(save_dir / f"{timestamp}.png") + self.log.info(f"saving {out_paths[-1]}") + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + # resize + if resize is None or resize is True: + resize = self.resize_resolution + elif resize is False: + resize = None + if resize is not None: + frame = cv2.resize(frame, resize, interpolation=cv2.INTER_LINEAR) + # mask + if mask is None or mask is True: + mask = self.mask_zones + elif mask is False: + mask = None + if mask is not None: + height, width, channels = frame.shape + out = np.full([height, width, channels], [0] * channels) + for zone_name in mask: + zone = self.bench.zones[zone_name]["box"] + out[zone[1]:zone[3], zone[0]:zone[2]] = frame[zone[1]:zone[3], zone[0]:zone[2]] + else: + out = frame + # save frame + cv2.imwrite(str(out_paths[-1]), out) + if vision is not None: + if suffix is not None: + out_paths.append(save_dir / f"{timestamp}.{suffix}.json") + else: + out_paths.append(save_dir / f"{timestamp}.json") + self.log.info(f"saving {out_paths[-1]}") + # save vision + with open(out_paths[-1], "w") as f: + json.dump(vision, f) + return out_paths def remove_older_images_if_needed(self): if self.minimum_disk_free_space_gb is None: diff --git a/src/ui/archive/archive.py b/src/ui/archive/archive.py index 13ba81b..d706234 100755 --- a/src/ui/archive/archive.py +++ b/src/ui/archive/archive.py @@ -46,19 +46,19 @@ class Archive(Widget): replace_widget(self, "crud_w", self.crud) self.selected = None self.print_b.setEnabled(False) - self.crud().db_tw.setSelectionBehavior(QAbstractItemView.SelectRows) - self.crud().db_tw.setSelectionMode(QAbstractItemView.SingleSelection) - self.crud().db_tw.itemSelectionChanged.connect(self.check) + self.crud.db_tw.setSelectionBehavior(QAbstractItemView.SelectRows) + self.crud.db_tw.setSelectionMode(QAbstractItemView.SingleSelection) + self.crud.db_tw.itemSelectionChanged.connect(self.check) self.print_b.clicked.connect(self.print_label) def check(self): - if not self.crud().modified: - selected = self.crud().get_selected_rows() + if not self.crud.modified: + selected = self.crud.get_selected_rows() if len(selected) == 1: selected = selected[0] - 1 # - 1 because rn starts from 1 (filters line) - if selected >= 0 and selected < len(self.crud().data_index): - selected = self.crud().data_index[selected] - self.selected = self.crud().db.table_model.get_by_id(selected) + if selected >= 0 and selected < len(self.crud.data_index): + selected = self.crud.data_index[selected] + self.selected = self.crud.db.table_model.get_by_id(selected) self.print_b.setEnabled(True) return self.selected = None diff --git a/src/ui/autotests_archive/autotests_archive.py b/src/ui/autotests_archive/autotests_archive.py index 3d24536..8768564 100755 --- a/src/ui/autotests_archive/autotests_archive.py +++ b/src/ui/autotests_archive/autotests_archive.py @@ -46,19 +46,19 @@ class Autotests_Archive(Widget): replace_widget(self, "crud_w", self.crud) self.selected = None self.print_b.setEnabled(False) - self.crud().db_tw.setSelectionBehavior(QAbstractItemView.SelectRows) - self.crud().db_tw.setSelectionMode(QAbstractItemView.SingleSelection) - self.crud().db_tw.itemSelectionChanged.connect(self.check) + self.crud.db_tw.setSelectionBehavior(QAbstractItemView.SelectRows) + self.crud.db_tw.setSelectionMode(QAbstractItemView.SingleSelection) + self.crud.db_tw.itemSelectionChanged.connect(self.check) self.print_b.clicked.connect(self.print_label) def check(self): - if not self.crud().modified: - selected = self.crud().get_selected_rows() + if not self.crud.modified: + selected = self.crud.get_selected_rows() if len(selected) == 1: selected = selected[0] - 1 # - 1 because rn starts from 1 (filters line) - if selected >= 0 and selected < len(self.crud().data_index): - selected = self.crud().data_index[selected] - self.selected = self.crud().db.table_model.get_by_id(selected) + if selected >= 0 and selected < len(self.crud.data_index): + selected = self.crud.data_index[selected] + self.selected = self.crud.db.table_model.get_by_id(selected) self.print_b.setEnabled(True) return self.selected = None diff --git a/src/ui/crud/crud.ui b/src/ui/crud/crud.ui index a2b62b4..0c12501 100755 --- a/src/ui/crud/crud.ui +++ b/src/ui/crud/crud.ui @@ -26,6 +26,18 @@ Crud + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/ui/dialog/dialog.ui b/src/ui/dialog/dialog.ui index ccca71d..a854bcc 100644 --- a/src/ui/dialog/dialog.ui +++ b/src/ui/dialog/dialog.ui @@ -6,8 +6,8 @@ 0 0 - 28 - 28 + 94 + 16 @@ -20,6 +20,18 @@ Dialog + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/ui/helpers/calc_foreground_color.py b/src/ui/helpers/calc_foreground_color.py index f60ae30..3c7b6b4 100644 --- a/src/ui/helpers/calc_foreground_color.py +++ b/src/ui/helpers/calc_foreground_color.py @@ -1,4 +1,10 @@ +from PyQt5.QtGui import QColor + + def calc_foreground_color(background_color): + if type(background_color) is QColor: + background_color = [background_color.red(), background_color.green(), background_color.blue(), ] + to_qcolor = True colors = [] for c in background_color[:3]: c /= 255 # 8bit to sRGB @@ -8,6 +14,8 @@ def calc_foreground_color(background_color): c = ((c + 0.055) / 1.055) ** 2.4 colors.append(c) # Luminance - L = 0.2126 * colors[0] + 0.7152 * colors[1] + 0.0722 * colors[1] + L = 0.2126 * colors[0] + 0.7152 * colors[1] + 0.0722 * colors[2] foreground_color = [(1 - round(L)) * 255] * 3 + if to_qcolor: + foreground_color = QColor(*foreground_color) return foreground_color diff --git a/src/ui/leak_step_editor/leak_step_editor.ui b/src/ui/leak_step_editor/leak_step_editor.ui index c6bb528..ebceec0 100644 --- a/src/ui/leak_step_editor/leak_step_editor.ui +++ b/src/ui/leak_step_editor/leak_step_editor.ui @@ -6,14 +6,26 @@ 0 0 - 987 - 210 + 955 + 161 Leak Step Editor + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/ui/recipe_selection/recipe_selection.ui b/src/ui/recipe_selection/recipe_selection.ui index cb59aef..cfe82d6 100644 --- a/src/ui/recipe_selection/recipe_selection.ui +++ b/src/ui/recipe_selection/recipe_selection.ui @@ -6,14 +6,26 @@ 0 0 - 800 - 600 + 347 + 50 Recipe Selection + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/ui/test/test.py b/src/ui/test/test.py index dbf5031..de5a7d8 100755 --- a/src/ui/test/test.py +++ b/src/ui/test/test.py @@ -38,18 +38,18 @@ class Test(Widget): self.step = None # INIT CYCLE STATES self.cycle_available_steps = { - # "assembly_1": Test_Assembly(self.select_step_img("assembly_1"), u"INSERIRE SENSORE"), - "autotest": Test_Assembly(None, u"ESEGUIRE PROCEDURA DI AUTOTEST", Test_Autotest()), - "barcodes": Test_Assembly(self.select_step_img("scan"), u"LEGGERE IL BARCODE DEL PEZZO DA COLLAUDARE", Test_Barcodes()), - "done": Test_Assembly(self.select_step_img("success"), u"COLLAUDO COMPLETATO"), - "emergency": Test_Assembly(self.select_step_img("reset_emergency"), u"EMERGENZA INTERVENUTA - RIPRISTINARE PULSANTE E SELEZIONARE \"RESET EMERGENZA\" DAL MEN\u00d9 \"STRUMENTI\""), - "fail": Test_Assembly(self.select_step_img("fail"), u"CICLO INTERROTTO"), - "leak": Test_Assembly(None, u"PREMERE START PER INIZIARE LA PROVA TENUTA", Test_Leak(components=self.components, recipe=self.recipe, step=self.step)), - "print": Test_Assembly(self.select_step_img("print"), u"STAMPA ETICHETTA IN CORSO"), - "select_recipe": Test_Assembly(None, u"SELEZIONARE IL CODICE DA COLLAUDARE", Recipe_Selection()), - "vision": Test_Assembly(None, u"VERIFICARE CONTROLLO CON TELECAMERA", Test_Vision(components=self.components, recipe=self.recipe, step=self.step)), - "wait": Test_Assembly(self.select_step_img("wait"), u"ATTENDERE - PAUSA INTER CICLO"), - None: Test_Assembly(self.select_step_img("warning"), u"ATTENZIONE - LA RICETTA SELEZIONATA NON CONTIENE FASI DI TEST"), + # "assembly_1": Test_Assembly(img_path=self.select_step_img("assembly_1"), text=u"INSERIRE SENSORE", widget=None), + "autotest": Test_Assembly(img_path=None, text=u"ESEGUIRE PROCEDURA DI AUTOTEST", widget=Test_Autotest()), + "barcodes": Test_Assembly(img_path=self.select_step_img("scan"), text=u"LEGGERE IL BARCODE DEL PEZZO DA COLLAUDARE", widget=Test_Barcodes()), + "done": Test_Assembly(img_path=self.select_step_img("success"), text=u"COLLAUDO COMPLETATO", widget=None), + "emergency": Test_Assembly(img_path=self.select_step_img("reset_emergency"), text=u"EMERGENZA INTERVENUTA - RIPRISTINARE PULSANTE E SELEZIONARE \"RESET EMERGENZA\" DAL MEN\u00d9 \"STRUMENTI\"", widget=None), + "fail": Test_Assembly(img_path=self.select_step_img("fail"), text=u"CICLO INTERROTTO", widget=None), + "leak": Test_Assembly(img_path=None, text=None, widget=Test_Leak(components=self.components, recipe=self.recipe, step=self.step)), + "print": Test_Assembly(img_path=self.select_step_img("print"), text=u"STAMPA ETICHETTA IN CORSO", widget=None), + "select_recipe": Test_Assembly(img_path=None, text=u"SELEZIONARE IL CODICE DA COLLAUDARE", widget=Recipe_Selection()), + "vision": Test_Assembly(img_path=None, text=u"VERIFICARE CONTROLLO CON TELECAMERA", widget=Test_Vision(components=self.components, recipe=self.recipe, step=self.step)), + "wait": Test_Assembly(img_path=self.select_step_img("wait"), text=u"ATTENDERE - PAUSA INTER CICLO", widget=None), + None: Test_Assembly(img_path=self.select_step_img("warning"), text=u"ATTENZIONE - LA RICETTA SELEZIONATA NON CONTIENE FASI DI TEST", widget=None), } self.cycle_steps = None self.cycle_index = -1 @@ -155,16 +155,19 @@ class Test(Widget): # RESET TEST DATA self.data = {"ok": True, "overridden": False} self.archived = None + # COUNT RESET + self.pieces[0] = 0 + self.pieces[1] = 0 elif action == "fail": self.log.info(f"cycle next: action: {action!r}") - # COUNT FAIL - self.pieces[1] += 1 # FAIL AND RESTART TEST self.step = Steps(type="fail") self.cycle_index = -1 # RESET TEST DATA self.data = {"ok": True, "overridden": False} self.archived = None + # COUNT FAIL + self.pieces[1] += 1 elif action is not None: raise NotImplementedError(f"cycle next: action {action!r} is not a valid action") # if action did not set the next cycle step @@ -270,11 +273,13 @@ class Test(Widget): def done(self, ok=True): self.log.info("cycle done") - t = self.data.get("vision", {}).get(0, {}).get("time", None) - f = self.data.get("vision", {}).get(0, {}).get("frame", None) - if t is not None and f is not None: - file_path = self.components["vision_saver"].save(t, f) - self.log.info(f"cycle vision image saved locally: {file_path!r}") + vision_test_1 = self.data.get("vision", {}).get(0, {}) + out_paths = self.components["vision_saver"].save( + save_time=vision_test_1.get("time", None), + frame=vision_test_1.get("frame", None), + # vision=vision_test_1.get("detections", None), + ) + self.log.info(f"cycle vision saved locally: {out_paths!r}") for vision in self.data["vision"].values(): vision.pop("frame", None) vision.pop("render", None) @@ -287,10 +292,14 @@ class Test(Widget): def print(self, archived=None): self.log.info("cycle print") # LABEL PRINT + leak_test_1 = self.data.get("leak", {}).get(0, {}) + leak_test_1_step_spec = leak_test_1.get("step", {}).get("spec", {}) + leak_test_1_results = leak_test_1.get("results", {}) + leak_test_1_results_data = leak_test_1_results.get("data", {}) datamatrix = str(archived.recipe.part_number) + archived.time.strftime("%m%y") + f"{archived.id:0>5}" - pmax = self.data.get("leak", {}).get(0, {}).get("step", {}).get("spec", {}).get("test_pressure", "-") - pmin = self.data.get("leak", {}).get(0, {}).get("results", {}).get("data", {}).get("Running test: pressure at the end of settling", "-") - leak = self.data.get("leak", {}).get(0, {}).get("results", {}).get("data", {}).get("Running test: measured leak", "-") + pmax = leak_test_1_step_spec.get("test_pressure", "-") + pmin = leak_test_1_results_data.get("Running test: pressure at the end of settling", "-") + leak = leak_test_1_results_data.get("Running test: measured leak", "-") context = { "DATAMATRIX": datamatrix, "DATAMATRIX_TEXT": datamatrix, @@ -302,13 +311,13 @@ class Test(Widget): "PMAX": f"{pmax:.3f}" if type(pmax) is not str else pmax, "PMIN": f"{pmin:.3f}" if type(pmin) is not str else pmin, "LEAK": f"{leak:.3f}" if type(leak) is not str else leak, - "TFILL": str(self.data.get("leak", {}).get(0, {}).get("step", {}).get("spec", {}).get("filling_time", "-")), - "TSTAB": str(self.data.get("leak", {}).get(0, {}).get("step", {}).get("spec", {}).get("settling_time", "-")), - "TTEST": str(self.data.get("leak", {}).get(0, {}).get("step", {}).get("spec", {}).get("test_time", "-")), - "MAXLEAK": str(self.data.get("leak", {}).get(0, {}).get("step", {}).get("spec", {}).get("test_pressure_max_delta", "-")), - "PTESTMIN": str(self.data.get("leak", {}).get(0, {}).get("step", {}).get("spec", {}).get("test_pressure_min_delta", "-")), + "TFILL": str(leak_test_1_step_spec.get("filling_time", "-")), + "TSTAB": str(leak_test_1_step_spec.get("settling_time", "-")), + "TTEST": str(leak_test_1_step_spec.get("test_time", "-")), + "MAXLEAK": str(leak_test_1_step_spec.get("test_pressure_max_delta", "-")), + "PTESTMIN": str(leak_test_1_step_spec.get("test_pressure_min_delta", "-")), "RESULT_L1": "ESITO" + str(" FORZATO" if self.data.get("overridden", False) else ""), - "RESULT_L2": str("CONFORME" if self.data.get("leak", {}).get(0, {}).get("results", {}).get("ok", False) else "SCARTO"), + "RESULT_L2": str("CONFORME" if leak_test_1_results.get("ok", False) else "SCARTO"), } self.components["label_printer"].print_label("EtichettaR5", context=context) self.log.info(f"cycle printed: {context!r}") diff --git a/src/ui/test/test.ui b/src/ui/test/test.ui index 18f8721..c4d3b5d 100755 --- a/src/ui/test/test.ui +++ b/src/ui/test/test.ui @@ -6,8 +6,8 @@ 0 0 - 1003 - 255 + 804 + 86 diff --git a/src/ui/test_admin_permission/test_admin_permission.ui b/src/ui/test_admin_permission/test_admin_permission.ui index 5a75bec..d117bec 100644 --- a/src/ui/test_admin_permission/test_admin_permission.ui +++ b/src/ui/test_admin_permission/test_admin_permission.ui @@ -6,8 +6,8 @@ 0 0 - 422 - 139 + 184 + 103 diff --git a/src/ui/test_assembly/test_assembly.py b/src/ui/test_assembly/test_assembly.py index d003c79..b4b131b 100755 --- a/src/ui/test_assembly/test_assembly.py +++ b/src/ui/test_assembly/test_assembly.py @@ -1,3 +1,5 @@ +import weakref + from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap from ui.helpers import replace_widget @@ -39,6 +41,8 @@ class Test_Assembly(Widget): else: if hasattr(self, attr): delattr(self, attr) + if hasattr(widget, "set_parent_assembly_widget"): + widget.set_parent_assembly_widget(weakref.ref(self)) self.widget.setVisible(True) else: self.widget.setVisible(False) diff --git a/src/ui/test_assembly/test_assembly.ui b/src/ui/test_assembly/test_assembly.ui index 2bf66ac..90a6d07 100755 --- a/src/ui/test_assembly/test_assembly.ui +++ b/src/ui/test_assembly/test_assembly.ui @@ -7,13 +7,25 @@ 0 0 94 - 108 + 89 Test Assembly + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/ui/test_autotest/test_autotest.ui b/src/ui/test_autotest/test_autotest.ui index ab44c93..42bf393 100755 --- a/src/ui/test_autotest/test_autotest.ui +++ b/src/ui/test_autotest/test_autotest.ui @@ -6,14 +6,27 @@ 0 0 - 18 - 18 + 94 + 16 Test Autotest - + + + 0 + + + 0 + + + 0 + + + 0 + + diff --git a/src/ui/test_barcodes/test_barcodes.ui b/src/ui/test_barcodes/test_barcodes.ui index 994686b..b002d36 100644 --- a/src/ui/test_barcodes/test_barcodes.ui +++ b/src/ui/test_barcodes/test_barcodes.ui @@ -6,14 +6,26 @@ 0 0 - 187 - 162 + 169 + 144 Test Barcodes + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/ui/test_leak/test_leak.py b/src/ui/test_leak/test_leak.py index 649e9c4..98d1167 100644 --- a/src/ui/test_leak/test_leak.py +++ b/src/ui/test_leak/test_leak.py @@ -15,6 +15,10 @@ class Test_Leak(Test_Test): self.components["tecna_t3"].write_recipe(self.recipe, self.step) self.get_connection = self.components["tecna_t3"].out.connect(self.get) self.components["tecna_t3"].resume() + if self.parent_assembly_widget is not None: + self.parent_assembly_widget().set_text(text=None) + self.start_b.setEnabled(False) + self.stop_b.setEnabled(False) def stop(self): # disable tes loop @@ -22,6 +26,10 @@ class Test_Leak(Test_Test): self.components["tecna_t3"].pause() self.disconnect(self.get_connection) super().stop() + if self.parent_assembly_widget is not None: + self.parent_assembly_widget().set_text(text=None) + self.start_b.setEnabled(False) + self.stop_b.setEnabled(False) def get(self, data=None, override=False): if self.done: # avoid proccessing if completed @@ -62,6 +70,21 @@ class Test_Leak(Test_Test): if type(v) is float: v = round(v, 2) l.setText(str(v)) + if d.get("Running test: active phase", None) in { + "WAITING START", + "ATTESA START", + "WAITING THE START OF A NEW TEST" + "FINE TEST", + }: + if self.parent_assembly_widget is not None: + self.parent_assembly_widget().set_text(text=u"PREMERE START PER INIZIARE LA PROVA TENUTA") + self.start_b.setEnabled(True) + self.stop_b.setEnabled(False) + else: + if self.parent_assembly_widget is not None: + self.parent_assembly_widget().set_text(text=u"PROVA TENUTA IN CORSO") + self.start_b.setEnabled(False) + self.stop_b.setEnabled(True) super().visualize(data) def save_last(self): diff --git a/src/ui/test_leak/test_leak.ui b/src/ui/test_leak/test_leak.ui index af9e28f..90ef48e 100644 --- a/src/ui/test_leak/test_leak.ui +++ b/src/ui/test_leak/test_leak.ui @@ -6,14 +6,26 @@ 0 0 - 980 - 704 + 370 + 491 Test Leak + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/ui/test_test/test_test.py b/src/ui/test_test/test_test.py index 2acd5a2..77a61c0 100644 --- a/src/ui/test_test/test_test.py +++ b/src/ui/test_test/test_test.py @@ -1,4 +1,5 @@ import sys +import weakref from lib.helpers import timing from PyQt5.QtCore import Qt, QTimer, pyqtSignal @@ -40,6 +41,7 @@ class Test_Test(Widget): self.override_b.clicked.connect(self.override) self.override_b.setEnabled(True) # setup vision staus gui + self.parent_assembly_widget = None self.status_imgs_full = { True: QPixmap("src/ui/imgs/success.png"), "": QPixmap("src/ui/imgs/neo.ico"), @@ -61,6 +63,14 @@ class Test_Test(Widget): self.status_palettes[""].setColor(QPalette.Base, QColor(255, 255, 0)) self.visualize() + def set_parent_assembly_widget(self, parent_assembly_widget=None): + if parent_assembly_widget is None: + self.parent_assembly_widget = None + elif type(parent_assembly_widget) is weakref.ReferenceType: + self.parent_assembly_widget = parent_assembly_widget + else: + self.parent_assembly_widget = weakref.ref(parent_assembly_widget) + def start(self, recipe=None, step=None): self.admin_challenged = False self.recipe = recipe diff --git a/src/ui/test_test/test_test.ui b/src/ui/test_test/test_test.ui index 79f63c4..be761cb 100644 --- a/src/ui/test_test/test_test.ui +++ b/src/ui/test_test/test_test.ui @@ -32,7 +32,7 @@ Risultato - + @@ -99,7 +99,7 @@ - SALVA IMMAGINE + SALVA diff --git a/src/ui/test_vision/test_vision.py b/src/ui/test_vision/test_vision.py index 6d3c7db..e90dcb5 100644 --- a/src/ui/test_vision/test_vision.py +++ b/src/ui/test_vision/test_vision.py @@ -1,7 +1,9 @@ import sys from PyQt5.QtCore import pyqtSignal -from PyQt5.QtGui import QImage, QPixmap +from PyQt5.QtGui import QColor, QImage, QPalette, QPixmap +from PyQt5.QtWidgets import QHeaderView, QProgressBar, QTableWidgetItem +from ui.helpers import calc_foreground_color from ui.test_test import Test_Test @@ -13,7 +15,17 @@ class Test_Vision(Test_Test): self.ok_counter_limit = 2 else: self.ok_counter_limit = 1 + # DETECTIONS TABLE + self.results_table_headers = { + "class": "RILEVATO", + "expected": "ATTESO", + "score": "PUNTEGGIO", + } super().__init__(components=components, recipe=recipe, step=step) + self.results_tw.setColumnCount(len(self.results_table_headers)) + self.results_tw.setHorizontalHeaderLabels(self.results_table_headers.values()) + self.results_tw.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.results_tw.verticalHeader().setSectionResizeMode(QHeaderView.Stretch) def start(self, recipe=None, step=None): super().start(recipe=recipe, step=step) @@ -27,6 +39,9 @@ class Test_Vision(Test_Test): self.components["vision"].resume() self.components["galaxy_camera"].resume() self.request_frame.emit() # request first frame + # DETECTIONS TABLE + # hide previous results + self.results_tw.setRowCount(0) def stop(self): # disable camera-vision loop @@ -52,12 +67,36 @@ class Test_Vision(Test_Test): "render": data["vision"].get("render", None), }], override=override) + @staticmethod + def tr(text): + translations = { + "no_detection": "non_rilevato", + "_": " ", + "big": "grande", + "black": "nero", + "blue": "blu", + "brown": "marrone", + "empty": "vuoto", + "green": "verde", + "ko": "ko", + "ok": "ok", + "orange": "arancione", + "red": "rosso", + "small": "piccolo", + "white": "bianco", + "yellow": "giallo", + } + for s, t in translations.items(): + text = text.replace(s, t) + return text + def visualize(self, data=None): if data is None: data = {} overridden = data.get("overridden", False) frame = data.get("frame", None) render = data.get("render", None) + results = data.get("results", {}).get("results", None) if overridden: img = self.status_imgs_full["warning"] elif render is not None: @@ -81,12 +120,84 @@ class Test_Vision(Test_Test): self.components["neo_pixels"].set_all_pixel_color("#ff0000") else: self.components["neo_pixels"].set_all_pixel_color("#ffffff") + # DETECTIONS TABLE + if results is not None: + self.results_tw.setRowCount(len(results)) + self.results_tw.setVerticalHeaderLabels(list(results)) + for r, [zone_name, result] in enumerate(results.items()): + for c, header in enumerate(self.results_table_headers): + if header == "class": + i = self.results_tw.item(r, c) + if i is None: + i = QTableWidgetItem() + self.results_tw.setItem(r, c, i) + detection = result.get("detection", None) + if detection is not None: + text = self.tr(detection["class"]["name"]) + color = QColor(detection["class"]["color"]) + else: + text = "" + color = QColor("#ffffff") + i.setText(text) + i.setBackground(color) + i.setForeground(calc_foreground_color(color)) + elif header == "expected": + i = self.results_tw.item(r, c) + if i is None: + i = QTableWidgetItem() + self.results_tw.setItem(r, c, i) + expected = result.get("expected", None) + if expected is not None: + text = self.tr(expected["name"]) + color = QColor(expected["color"]) + else: + text = "" + color = QColor("#ffffff") + i.setText(text) + i.setBackground(color) + i.setForeground(calc_foreground_color(color)) + elif header == "score": + w = self.results_tw.cellWidget(r, c) + if w is None: + w = QProgressBar() + w.setRange(0, 100) + self.results_tw.setCellWidget(r, c, w) + score = result.get("detection", {}).get("score", None) + ok = result.get("ok", None) + if score is not None: + value = int(score * 100) + else: + value = 0 + w.setValue(value) + if ok is not None: + if ok: + if score is not None: + color = QColor(int(255 - max(score - 0.5, 0) / 0.5 * 255), 255, 0) + else: + color = QColor("#00ff00") + else: + color = QColor("#ff0000") + else: + color = QColor("#0000ff") + p = QPalette() + p.setColor(QPalette.Highlight, color) + w.setPalette(p) + else: + raise NotImplementedError() + else: + self.results_tw.setRowCount(0) super().visualize(data, img=img) def save_last(self): if self.last is None: return - self.components["vision_saver"].save(self.last["time"], self.last["frame"]) + self.components["vision_saver"].save( + save_time=self.last.get("time", None), + frame=self.last.get("frame", None), + vision=self.last.get("detections", None), + suffix="manual_save", + resize=[256, 256], + ) def emit_ok(self): super().emit_ok() diff --git a/src/ui/test_vision/test_vision.ui b/src/ui/test_vision/test_vision.ui index 2d09b26..bb9707b 100644 --- a/src/ui/test_vision/test_vision.ui +++ b/src/ui/test_vision/test_vision.ui @@ -6,15 +6,27 @@ 0 0 - 618 - 835 + 880 + 629 Test Vision - + + 0 + + + 0 + + + 0 + + + 0 + + @@ -27,13 +39,32 @@ - + + + + + 0 + 0 + + + + SALVA IMMAGINE + + + + + + + 0 + 0 + + Risultato - + @@ -49,7 +80,7 @@ - + @@ -72,35 +103,77 @@ - - - - 0 - 0 - - - - - 600 - 700 - - - - - - - - - - - - - 0 - 0 - - - - SALVA IMMAGINE - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Visione + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + 612 + 512 + + + + - + + + + diff --git a/src/ui/vision_step_editor/vision_step_editor.ui b/src/ui/vision_step_editor/vision_step_editor.ui index 5842a40..a146aec 100644 --- a/src/ui/vision_step_editor/vision_step_editor.ui +++ b/src/ui/vision_step_editor/vision_step_editor.ui @@ -6,14 +6,26 @@ 0 0 - 174 - 91 + 143 + 67 Vision Step Edito + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/src/ui/window/window.ui b/src/ui/window/window.ui index 7a20ddc..8c7fc91 100644 --- a/src/ui/window/window.ui +++ b/src/ui/window/window.ui @@ -13,7 +13,6 @@ Window -