"""
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 main_controller(self, controller):
self.controller = controller
[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)