import pathlib import sys from itertools import cycle import cv2 import imutils import numpy as np from PyQt5.QtCore import QMutex, Qt, QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import (QDialog, QFormLayout, QLabel, QPushButton, QSizePolicy, QSlider) try: from . import gxipy as gx except Exception: from dummies import gxpy as gx if "--sim-camera" in sys.argv: from components.dummies.gxpy import DummyCamera from datetime import datetime from .component import Component class GalaxyCamera(Component): _edits_new_frame = pyqtSignal(object) def __init__(self, config=None, name=None, period=1, lazy=True, paused=False, threaded=True, registers=None): super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded) self.lock = QMutex() self.simulate = "--sim-camera" in sys.argv self._last_frame = None def config_changed(self): if self.simulate: self.device_manager = None self.camera = DummyCamera() self.sim_imgs = cycle(sorted(pathlib.Path("data/simulation_images/").glob("*.png"))) else: self.device_manager = gx.DeviceManager() # create a device manager dev_num, dev_info_list = self.device_manager.update_device_list() if dev_num == 0: raise AssertionError(f"camera not detected: dev_num = {dev_num!r}") self.camera = self.device_manager.open_device_by_index(1) self.resolution = { "w": int(self.round_4(self.config[self.name]["horizontal_resolution"])), "h": int(self.round_4(self.config[self.name]["vertical_resolution"])), } self._period = int(self.config[self.name].get("frame_time_ms", 300)) / 1000 self.exposure_time = int(self.config[self.name]["exposure_time"]) self.roi = { "x": int(self.round_4(self.config[self.name].get("horizontal_crop_offset", 0))), "w": int(self.round_4(self.config[self.name].get("horizontal_crop_resolution", self.resolution["w"]))), "y": int(self.round_4(self.config[self.name].get("vertical_crop_offset", 0))), "h": int(self.round_4(self.config[self.name].get("vertical_crop_resolution", self.resolution["h"]))), "r": int(self.config[self.name].get("rotate_90_clockwise_times", 0)), } self.auto_white_balance = self.config[self.name].get("auto_white_balance", "").lower() in {"on", "1", "y", "yes", "true", "enable", "enabled", } self.balance = { "r": float(self.config[self.name].get("balance_red", 1)), "g": float(self.config[self.name].get("balance_green", 1)), "b": float(self.config[self.name].get("balance_blue", 1)), } self.lock.lock() self.camera.stream_off() self.camera.TriggerMode.set(gx.GxSwitchEntry.OFF) self.camera.ExposureTime.set(self.exposure_time) self.camera.Gain.set(1.0) self.camera.OffsetX.set(self.roi["x"]) self.camera.Width.set(self.roi["w"]) self.camera.OffsetY.set(self.roi["y"]) self.camera.Height.set(self.roi["h"]) if self.auto_white_balance: self.camera.BalanceWhiteAuto.set(gx.GxAutoEntry.ONCE) QThread.msleep(3000) self.camera.BalanceRatioSelector.set(gx.GxBalanceRatioSelectorEntry.RED) self.balance["r"] = self.camera.BalanceRatio.get() self.camera.BalanceRatioSelector.set(gx.GxBalanceRatioSelectorEntry.GREEN) self.balance["g"] = self.camera.BalanceRatio.get() self.camera.BalanceRatioSelector.set(gx.GxBalanceRatioSelectorEntry.BLUE) self.balance["b"] = self.camera.BalanceRatio.get() else: self.camera.BalanceRatioSelector.set(gx.GxBalanceRatioSelectorEntry.RED) self.camera.BalanceRatio.set(self.balance["r"]) self.camera.BalanceRatioSelector.set(gx.GxBalanceRatioSelectorEntry.GREEN) self.camera.BalanceRatio.set(self.balance["g"]) self.camera.BalanceRatioSelector.set(gx.GxBalanceRatioSelectorEntry.BLUE) self.camera.BalanceRatio.set(self.balance["b"]) if self.camera.GammaParam.is_readable(): self.gamma_lut = gx.Utility.get_gamma_lut(self.camera.GammaParam.get()) else: self.gamma_lut = None if self.camera.ContrastParam.is_readable(): self.contrast_lut = gx.Utility.get_contrast_lut(self.camera.ContrastParam.get()) else: self.contrast_lut = None if self.camera.ColorCorrectionParam.is_readable(): self.color_correction = self.camera.ColorCorrectionParam.get() else: self.color_correction = 0 self.camera.TriggerMode.set(gx.GxSwitchEntry.ON) self.camera.TriggerSource.set(gx.GxTriggerSourceEntry.SOFTWARE) self.camera.stream_on() self.edits = None self.lock.unlock() self.edits_enabled = "--camera-edits" in sys.argv if self.edits_enabled: self.init_edits() @staticmethod def round_4(x): return 4 * round(int(x) / 4) @Component.reconfig_on_error def _get(self): # print("GALAXY CAMERA", str(int(QThread.currentThreadId())), flush=True) frame = None self.lock.lock() if self.simulate: img_path = next(self.sim_imgs) self.log.debug(f"loading image {img_path}") frame = cv2.cvtColor(cv2.imread(str(img_path)), cv2.COLOR_BGR2RGB) QThread.msleep(1000) else: self.camera.TriggerSoftware.send_command() frame = self.camera.data_stream[0].get_image() if frame is not None: if frame.get_status() == gx.GxFrameStatusList.INCOMPLETE: self.log.error("incomplete frame") frame = None else: frame = frame.convert("RGB") frame.image_improvement(self.color_correction, self.contrast_lut, self.gamma_lut) frame = frame.get_numpy_array() frame = np.rot90(frame, self.roi["r"]) self.lock.unlock() if frame is None: self.log.error("failed to get frame") elif self.edits_enabled: frame = self.edit(frame, self.edits) self._edits_new_frame.emit([frame]) super()._get([frame]) def __del__(self, event=None): if self.camera is not None: self.camera.stream_off() self.camera.close_device() def init_edits(self): self.edits_enabled = True self.edits_dialog = EditsDialog(self.roi) self.edits_dialog.edits_changed.connect(self.set_edits) self._edits_new_frame.connect(self.edits_dialog.save_and_show_edits_new_frame) self.edits_dialog.edits_pause.connect(self.toggle_paused) def toggle_paused(self): if self.running: self.pause() else: self.resume() def set_edits(self, edits=None): self.edits = edits def edit(self, img, edits=None): if edits is None: return img # BRIGHTNESS AND CONTRAST contrast = float(edits.get("contrast", 0)) brightness = float(edits.get("brightness", 0)) if not (contrast == 0 and brightness == 0): img = imutils.adjust_brightness_contrast(img, brightness=brightness, contrast=contrast / 255 * 500) # ROTATE AND SCALE rotation = -float(edits.get("rotation", 0)) scale = float(edits.get("scale", 1)) if not (rotation == 0 and scale == 1): img = imutils.rotate(img, rotation, scale=scale) # TRANSLATE translation_x = float(edits.get("translation_x", 0)) translation_y = float(edits.get("translation_y", 0)) if not (translation_x == 0 and translation_y == 0): img = imutils.translate(img, translation_x, translation_y) return img class EditsDialog(QDialog): edits_changed = pyqtSignal(object) edits_pause = pyqtSignal() def __init__(self, roi): super().__init__() self.frame = None self.edits = { "brightness": 0, "contrast": 0, "rotation": 0, "scale": 1, "translation_x": 0, "translation_y": 0, } self.edits_specs = { "brightness": [[-255, 255], 1, QSlider(Qt.Horizontal), QLabel()], "contrast": [[-255, 255], 1, QSlider(Qt.Horizontal), QLabel()], "rotation": [[-180, 180], 1, QSlider(Qt.Horizontal), QLabel()], "scale": [[0, 5], 100, QSlider(Qt.Horizontal), QLabel()], "translation_x": [[-roi["w"], roi["w"]], 1, QSlider(Qt.Horizontal), QLabel()], "translation_y": [[-roi["h"], roi["h"]], 1, QSlider(Qt.Horizontal), QLabel()], } layout = QFormLayout() self.edits_frame_l = QLabel() self.edits_frame_l.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) layout.addRow(self.edits_frame_l) self.edits_pause_b = QPushButton("toggle paused") layout.addRow(self.edits_pause_b) self.edits_pause_b.clicked.connect(self.edits_pause) for edit, limits in self.edits_specs.items(): limit, multiplier, slider, label = limits slider.setRange(limit[0] * multiplier, limit[1] * multiplier) slider.setSingleStep(1) slider.setValue(self.edits[edit] * multiplier) label.setText(f"{edit}: {self.edits[edit]}") layout.addRow(label, slider) slider.valueChanged.connect(self.update_edits) self.edits_save_frame_b = QPushButton("save frame") layout.addRow(self.edits_save_frame_b) self.edits_save_frame_b.clicked.connect(self.edits_save_frame) self.setLayout(layout) self.update_edits() self.show() def update_edits(self): for edit, limits in self.edits_specs.items(): limit, multiplier, slider, label = limits self.edits[edit] = slider.value() / multiplier label.setText(f"{edit}: {self.edits[edit]}") self.edits_changed.emit(self.edits) def save_and_show_edits_new_frame(self, frame): self.frame = frame[0] if self.frame is not None: self.edits_frame_l.setPixmap( QPixmap.fromImage( QImage( self.frame.data, self.frame.shape[1], # width self.frame.shape[0], # height self.frame.shape[2] * self.frame.shape[1], # width * channels QImage.Format_RGB888 ) ).scaled( max(self.edits_frame_l.width(), 640), max(self.edits_frame_l.height(), 480), Qt.KeepAspectRatio, Qt.SmoothTransformation ) ) def edits_save_frame(self): if self.frame is None: return out_path = f"./{datetime.now().isoformat()}.png" self.log.info(f"saving frame: {out_path!r}") img = cv2.cvtColor(self.frame, cv2.COLOR_RGB2BGR) cv2.imwrite(out_path, img) return out_path