Source code for models.moilutils.moilutils

"""
Project Name: MoilApps v4.1.0
Writer : Haryanto
PROJECT MADE WITH: Qt Designer and PyQt6
Build for: MOIL-LAB
Copyright: MOIL-2024

This project can be use a a template to create better user interface when you design a project.

There are limitations on Qt licenses if you want to use your products
commercially, I recommend reading them on the official website:
https://doc.qt.io/qtforpython/licenses.html

"""
import datetime
import os
import shutil
import sys
from typing import Tuple
from moildev import Moildev
from moil_camera import MoilCam
from components.select_media_source import CameraSource
from components.form_crud_parameters import CameraParametersForm
from components.select_parameters_name import select_parameter
from PyQt6 import QtWidgets, QtCore, QtGui
from PyQt6.QtGui import QImage, qRgb
from moil_fisheye_marker.fisheye_marker import MoilFisheyeMarker

import cv2
import numpy as np
import pyexiv2

sys.path.append(os.path.dirname(__file__))

path_file = os.path.dirname(os.path.realpath(__file__))
parameters_database_path = path_file + "/components/camera_parameters.json"

model_directory = os.path.abspath(".")
QtCore.QDir.addSearchPath("icons", model_directory + "models/moilutils/components/icons")


[docs] class MoilUtils(MoilFisheyeMarker): def __init__(self): self.controller = None
[docs] @staticmethod def moil_camera(cam_type=None, cam_id=None, resolution: Tuple[int, int] = None): """ create camera object from moil camera package Args: cam_type: cam_id: resolution: Returns: """ if cam_type is None or cam_id is None: QtWidgets.QMessageBox.warning(None, "Warning!", "Camera parameter of camera id not pass!!") camera = None else: camera = MoilCam(cam_type=cam_type, cam_id=cam_id, resolution=resolution) return camera
[docs] @staticmethod def select_parameter_name(): """ Generate a Q-dialog for select camera parameter name. Returns: camera name .. code-block: python type_camera = mutils.select_type_camera() .. image:: assets/select-parameter-name.png :scale: 120 % :alt: alternate text :align: center """ params_name = select_parameter(parameters_database_path) return params_name
[docs] def select_media_source(self, from_plugin=False): """ Displays a dialog for selecting USB/web camera sources and detecting them. Available camera sources can be obtained by clicking the detection button on the dialog. Returns: Port number for usb cameras or the source link for web cameras .. code-block: python cam_source = mutils.select_source_camera() .. image:: assets/select-media-source.png :scale: 70 % :alt: alternate text :align: center """ open_cam_source = QtWidgets.QDialog() source_cam = CameraSource(open_cam_source, from_plugin) source_cam.main_controller(self.controller) open_cam_source.exec() media_source = source_cam.camera_source params_name = source_cam.parameter_selected cam_type = source_cam.cam_type source_type = source_cam.comboBox_camera_sources.currentText() return source_type, cam_type, media_source, params_name
[docs] def main_controller(self, controller): self.controller = controller
[docs] def form_camera_parameter(self, from_plugin=False): """ Generate a dialog for camera parameters setting. .. image:: assets/form-parameters.png :scale: 100 % :alt: alternate text :align: center """ open_cam_params = QtWidgets.QDialog() a = CameraParametersForm(open_cam_params, from_plugin, parameters_database_path) a.main_controller(self.controller) open_cam_params.exec()
[docs] @staticmethod def show_image_to_label(label, image, width, options=None): """ Display an image to the label widget on the user interface. Args: label: destination label image: image to show width: width for resizing the image while keeping the original aspect ratio options (dict): optional parameters including angle, plus_icon, and scale_content Returns: None. Shows image on the label. Example: .. code-block:: python options = {'angle': 0, 'plus_icon': False, 'scale_content': False} MoilUtils.show_image_to_label(label, image, 400, options) """ options = options or {} angle = options.get('angle', 0) plus_icon = options.get('plus_icon', False) scale_content = options.get('scale_content', False) if scale_content: label.setScaledContents(True) else: label.setScaledContents(False) height = MoilUtils.calculate_height(image, width) image = MoilUtils.resize_image(image, width) label.setMinimumSize(QtCore.QSize(width, height)) label.setMaximumSize(QtCore.QSize(width, height)) image = MoilUtils.rotate_image(image, angle) if plus_icon: # draw plus icons on image and show to label h, w = image.shape[:2] plus_length = 10 center = round(w / 2), round(h / 2) plus_lines = [(center[0] - plus_length, center[1]), (center[0] + plus_length, center[1]), (center[0], center[1] - plus_length), (center[0], center[1] + plus_length)] for line in plus_lines: MoilUtils.draw_line(image, center, line) image = QtGui.QImage(image.data, image.shape[1], image.shape[0], QtGui.QImage.Format.Format_RGB888).rgbSwapped() label.setPixmap(QtGui.QPixmap.fromImage(image))
[docs] @classmethod def connect_to_moildev(cls, parameter_name, parameters_database=None, virtual_param_ratio=None): """ Return a Moildev instance for a specific type of camera. Args: parameter_name: name of camera (You can use 'select_type_camera()' to get the name.) parameters_database: parameter of the camera virtual_param_ratio Returns: Moildev instance .. code-block:: python c_type = mutils.select_type_camera() moildev_camera1 = mutils.connect_to_moildev(type_camera=c_type) """ try: if parameters_database is None and virtual_param_ratio is None: moildev = Moildev.Moildev(parameters_database_path, parameter_name) elif parameters_database is None and virtual_param_ratio is not None: moildev = Moildev.Moildev(parameters_database_path, parameter_name, virtual_param_ratio) else: moildev = Moildev.Moildev(parameters_database, parameter_name) except FileNotFoundError: QtWidgets.QMessageBox.warning(None, "Warning!!", "The parameters database file is not found." ) print("The parameters database file is not found.") moildev = None except TypeError: QtWidgets.QMessageBox.warning(None, "Warning!!", "There is a type error while creating the Moildev instance." ) print("There is a type error while creating the Moildev instance.") moildev = None return moildev
[docs] @staticmethod def remap_image(image, map_x, map_y): """ Take an image and a pair of X-Y maps generated by a Moildev instance as inputs, then return a remapped image using the maps. Args: image: input image map_x: mapping function in the x direction. map_y: mapping function in the y direction. Returns: image: remapped image, typically it would be an anypoint image or a panorama image. - Example: .. code-block:: python image_anypoint = remap_image(image, mapX_anypoint, mapY_anypoint) """ image = cv2.remap(image, map_x, map_y, cv2.INTER_CUBIC) return image
[docs] @staticmethod def select_file(parent=None, title="Open file", dir_path=".", file_filter=""): """ Generate a dialog for file selection and return the path of the file selected. If no file is selected, an empty string is returned. Args: parent: parent windows of the dialog title: dialog title file_filter: filters for specific file types dir_path: dialog's working directory return: path of the file selected. - Example: .. code-block:: python path_img = mutils.select_file(dir_path="./", file_filter='*.jpg') """ option = QtWidgets.QFileDialog.Option.DontUseNativeDialog file_path, _ = QtWidgets.QFileDialog.getOpenFileName(parent, title, dir_path, file_filter, options=option) return file_path
[docs] @staticmethod def select_directory(parent=None, parent_dir="", title='Select Folder'): """ Generate a dialog for directory selection and return the path of the directory selected. If no directory is selected, an empty string is returned. Returns: path of the directory selected - Example: .. code-block:: python path_dir = mutils.select_file() """ option = QtWidgets.QFileDialog.Option.DontUseNativeDialog directory = QtWidgets.QFileDialog.getExistingDirectory(parent, title, directory=parent_dir, options=option) return directory
[docs] @staticmethod def copy_directory(src_directory, dst_directory): """ Recursively copy a whole directory to the destination directory. Args: src_directory: path of the source folder dst_directory: path of the destination directory Returns: None - Example: .. code-block:: python path_source = mutils.select_directory() mutils.copy_directory(path_source, '/home') """ directory_name = os.path.basename(src_directory) destination_path = os.path.join(dst_directory, directory_name) shutil.copytree(src_directory, destination_path)
[docs] @staticmethod def resize_image(image, width): """ Resize an image to one with a given width while maintaining its original aspect ratio and return it. Args: image: input image width: desired image width Returns: resized image """ h, w = image.shape[:2] r = width / float(w) hi = round(h * r) result = cv2.resize(image, (width, hi), interpolation=cv2.INTER_AREA) return result
[docs] @staticmethod def rotate_image(src, angle, center=None, scale=1.0): """ Return an image after rotation and scaling(not resizing). Args: src: input image angle: rotation angle center: coordinate of the rotation center. By default, it's the image center. scale: scaling factor Returns: rotated image - Example: .. code-block:: python image = mutils.rotate_image(image, 90, center=(20,25)) """ h, w = src.shape[:2] if center is None: center = (w / 2, h / 2) m = cv2.getRotationMatrix2D(center, angle, scale) rotated = cv2.warpAffine(src, m, (w, h)) return rotated
[docs] @staticmethod def calculate_height(image, width): """ Return the aspect ratio keeping height for a given image width Args: image: input image width: desired image width Returns: image height - Example: .. code-block:: python height = calculate_height(image, 140) """ h, w = image.shape[:2] r = width / float(w) height = round(h * r) return height
[docs] @staticmethod def draw_polygon(image, mapX, mapY, is_fov=False): """ Return image with a drawn polygon indicating the remapped area given an anypoint map pair. Args: image: input image map_x: anypoint mapX map_y: anypoint mapY Returns: image with a polygon - Example: .. code-block:: python img = mutils.read_image('sample_image.jpg') c_type = mutils.select_type_camera() m_instance = mutils.connect_to_moildev(type_camera=c_type) mx, my = m_instance.maps_anypoint(0, -90, 4) img = mutils.draw_polygon(img, mx, my) """ hi, wi = image.shape[:2] x1, y1, x2, y2, x3, y3, x4, y4 = [], [], [], [], [], [], [], [] for x in range(wi): if x < len(mapX[0]) and mapX[0, x] != 0 and mapY[0, x] != 0: x1.append(mapX[0, x]) y1.append(mapY[0, x]) if x < len(mapX[0]) and mapY[-1, x] != 0 and mapX[-1, x] != 0: x3.append(mapX[-1, x]) y3.append(mapY[-1, x]) for y in range(hi): if y < len(mapX[:, 0]) and mapY[y, 0] != 0 and mapX[y, 0] != 0: x2.append(mapX[y, 0]) y2.append(mapY[y, 0]) if y < len(mapX[:, 0]) and mapX[y, -1] != 0 and mapY[y, -1] != 0: x4.append(mapX[y, -1]) y4.append(mapY[y, -1]) if is_fov: r = np.array([x3, y3]) points3 = r.T.reshape((-1, 1, 2)) # Draw polyline on original image cv2.polylines(image, np.int32([points3]), False, (0, 255, 0), 10) return image else: p = np.array([x1, y1]) q = np.array([x2, y2]) r = np.array([x3, y3]) s = np.array([x4, y4]) points = p.T.reshape((-1, 1, 2)) points2 = q.T.reshape((-1, 1, 2)) points3 = r.T.reshape((-1, 1, 2)) points4 = s.T.reshape((-1, 1, 2)) # Draw polyline on original image if wi > 1944: line = 10 elif 1300 <= wi <= 1944: line = 6 elif 800 <= wi < 1300: line = 3 else: line = 5 cv2.polylines(image, np.int32([points]), False, (0, 0, 255), line) cv2.polylines(image, np.int32([points2]), False, (255, 0, 0), line) cv2.polylines(image, np.int32([points3]), False, (0, 255, 0), line) cv2.polylines(image, np.int32([points4]), False, (0, 255, 0), line) return image
[docs] @staticmethod def write_camera_type(image_file, type_camera): """ Write the camera type into the image's metadata. Args: image_file: image file path type_camera: name of camera Returns: None .. code-block:: mutils.write_camera_type('sample_image.jpg', 'Camera_name') """ img = pyexiv2.Image(image_file) pyexiv2.registerNs('a namespace for image', 'Image') img.modify_xmp({'Xmp.Image.cameraName': type_camera}) img.close()
[docs] @staticmethod def read_camera_type(image_file): """ Read the camera type from image's metadata. Args: image_file: image file path Returns: camera type .. code-block:: c_type = mutils.read_camera_type('sample_image.jpg') """ img = pyexiv2.Image(image_file) try: camera_type = img.read_xmp()['Xmp.Image.cameraName'] except KeyError: # Handle the case where the 'Xmp.Image.cameraName' key is not found camera_type = None finally: img.close() return camera_type
[docs] @staticmethod def save_image(image, dst_directory, type_camera=None): """ Save an image to a directory and write the camera type into its metadata if the type is given. The file name would be the date and time when the image is saved. Args: image: input image dst_directory: destination directory path type_camera: camera type Returns: file name .. code-block:: save_image(img, '.', 'camera_1') """ ss = datetime.datetime.now().strftime("%m_%d_%H_%M_%S") name = dst_directory + "/" + str(ss) + ".png" cv2.imwrite(name, image) if type_camera is not None: MoilUtils.write_camera_type(name, type_camera) return ss
[docs] @staticmethod def draw_line(image, coordinate_point_1=None, coordinate_point_2=None): """ Draw a line on the image from the coordinate given. If no coordinate is given, it draws lines on image margins. Args: image: input image coordinate_point_1: point 1 coordinate (x, y) coordinate_point_2: point 2 coordinate (x, y) Returns: image with a line drawn .. code-block:: img = mutils.draw_line(img, (300, 300), (300, 400) ) """ # draw anypoint line if coordinate_point_1 is None: h, w = image.shape[:2] if h >= 1000: cv2.line(image, (0, 0), (0, h), (255, 0, 0), 10) cv2.line(image, (0, 0), (w, 0), (0, 0, 255), 10) cv2.line(image, (0, h), (w, h), (0, 255, 0), 10) cv2.line(image, (w, 0), (w, h), (0, 255, 0), 10) else: cv2.line(image, (0, 0), (0, h), (255, 0, 0), 2) cv2.line(image, (0, 0), (w, 0), (0, 0, 255), 2) cv2.line(image, (0, h), (w, h), (0, 255, 0), 2) cv2.line(image, (w, 0), (w, h), (0, 255, 0), 2) else: # this for draw line on image cv2.line(image, coordinate_point_1, coordinate_point_2, (0, 255, 0), 1) return image
[docs] @staticmethod def calculate_ratio_image2label(label, image): """ Calculate the width and height ratio of the image to a label. Args: label : UI label image : input image Returns: width ratio and height ratio .. code-block:: w_ratio, h_ratio = mutils.calculate_ratio_image2label(label, img) """ h = label.height() w = label.width() height, width = image.shape[:2] ratio_x = width / w ratio_y = height / h return ratio_x, ratio_y
[docs] @staticmethod def cropping_image(image, left, right, top, bottom): """ Crop an image by ratio from every side. Args: image: input image right: ratio of right side (1-0) bottom: ratio of bottom side (1-0) left: ratio of left side (0-1) top: ratio of top side (0-1) Returns: image has already cropping """ a_right = round(image.shape[1] * right) a_bottom = round(image.shape[0] * bottom) a_left = round(image.shape[1] * left) a_top = round(image.shape[0] * top) return image[a_top:a_top + a_bottom, a_left:a_left + a_right]
[docs] @staticmethod def draw_list_point_with_text(image, coordinate_point, radius=5): """ Draw points and their indices on the image. Args: image: input image coordinate_point: a list of points' coordinates radius: point radius Returns: image with the point and their sequences drawn. .. code-block:: points_to_draw = [(100, 250), (200, 200), (450, 0)] img = mutils.draw_list_point_with_text(img, points_to_draw, radius=3) """ if coordinate_point is not None: for i, point in enumerate(coordinate_point): image = cv2.putText(image, str(i + 1), tuple(point), cv2.FONT_HERSHEY_SIMPLEX, 2, (200, 5, 5), 3, cv2.LINE_AA) cv2.circle(image, tuple(point), radius, (200, 5, 200), 20, -1) return image
[docs] @classmethod def draw_rectangle(cls, image, point_1, point_2, thickness=5): """ Draw rectangle on the image. Args: image (): input image point_1 (): the first point point_2 (): the second point to create rectangle thickness (): the thickness of rectangle line Returns: image with rectangle object """ image = cv2.rectangle(image, point_1, point_2, (0, 0, 225), thickness) return image
[docs] @classmethod def calculate_resolution_option(cls, image, ratio_resize): resolution_option = [] if image is not None: for ratio in ratio_resize: resolution_option.append((int(image.shape[1] * ratio), int(image.shape[0] * ratio))) return resolution_option
[docs] @classmethod def convert_cv2_to_q_image(cls, image): """ Convert an image from OpenCV format to Qt format. The function takes an image in OpenCV format and returns the equivalent image in Qt format. The image can be grayscale, RGB or RGBA. The conversion is done by creating a `QImage` object and setting the image data and format accordingly. Args: image (ndarray): The image in OpenCV format (height x width x channels) Returns: QImage: The image in Qt format """ qim = QImage() if image is None: return qim if image.dtype == np.uint8: if len(image.shape) == 2: qim = QImage(image.data, image.shape[1], image.shape[0], image.strides[0], QImage.Format.Format_Indexed8) qim.setColorTable([qRgb(i, i, i) for i in range(256)]) elif image.shape[2] == 3: image = np.ascontiguousarray(image) qim = QImage(image.data, image.shape[1], image.shape[0], image.strides[0], QImage.Format.Format_RGB888) elif image.shape[2] == 4: qim = QImage(image.data, image.shape[1], image.shape[0], image.strides[0], QImage.Format.Format_ARGB32) return qim
[docs] @classmethod def convert_q_image_to_mat(cls, q_image): """Converts a QImage to a NumPy array. Args: q_image: A QImage instance to be converted. Returns: A NumPy array with the image data. Raises: TypeError: If `q_image` is not a QImage instance. """ incoming_image = q_image.convertToFormat(QImage.Format.Format_ARGB32) width = incoming_image.width() height = incoming_image.height() ptr = incoming_image.constBits() # https://blog.csdn.net/weixin_42670810/article/details/120683036 ptr.setsize(incoming_image.bytesPerLine() * incoming_image.height()) arr = np.array(ptr).reshape(height, width, 4) # Copies the data return cv2.cvtColor(arr, cv2.COLOR_BGR2RGB)