"""
This module defines utility functions for file selection, image processing, and feature detection
used in the MoilApp application.
Dependencies:
- PyQt5.QtWidgets: Provides the QFileDialog and QMessageBox for file selection and notifications.
- cv2: OpenCV library for image processing.
- numpy: Library for numerical operations.
- datetime: Library to handle date and time operations.
- os: Library to interact with the operating system.
- math: Library for mathematical functions.
Functions:
- select_file: Opens a file dialog to select a file.
- read_image: Reads an image from the specified path.
- saveImage: Saves an image with a timestamped filename.
- drawPoint: Draws a point on the image.
- image_resize: Resizes an image while maintaining its aspect ratio.
- corner_detect: Detects corners in an image using Harris corner detection.
- draw_corners: Draws detected corners on an image.
- get_corner_list: Extracts corner coordinates from the detected corners.
- distance: Calculates the distance between two points.
- draw_matches: Draws lines between matched points in two images.
"""
from PyQt5 import QtWidgets
import datetime
import os
import cv2
import numpy as np
import math
[docs]
def select_file(title, dir_path, file_filter):
"""
Opens a file dialog to select a file.
Args:
title (str): The title of the file dialog window.
dir_path (str): The directory path to start the file dialog.
file_filter (str): The filter for the file types to select.
Returns:
str: The selected file path.
"""
options = QtWidgets.QFileDialog.DontUseNativeDialog
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(None, title, dir_path,
file_filter,
options=options)
return file_path
[docs]
def read_image(image_path):
"""
Reads an image from the specified path.
Args:
image_path (str): The path of the image file to read.
Returns:
ndarray: The image read from the file.
Raises:
FileNotFoundError: If the image cannot be loaded.
"""
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError("`{}` not cannot be loaded".format(image_path))
return img
[docs]
def saveImage(filename, image):
"""
Saves an image with a timestamped filename.
Args:
filename (str): The base filename to use for saving the image.
image (ndarray): The image to save.
"""
ss = datetime.datetime.now().strftime("%H_%M_%S")
name = "../result/Images/" + filename + "_" + str(ss) + ".png"
os.makedirs(os.path.dirname(name), exist_ok=True)
cv2.imwrite(name, image)
QtWidgets.QMessageBox.information(None, "Information", "Image saved !!")
[docs]
def drawPoint(image, heightImage, coordinatePoint):
"""
Draws a point on the image.
Args:
image (ndarray): The image on which to draw the point.
heightImage (int): The height of the image to determine the point size.
coordinatePoint (tuple): The coordinates of the point to draw.
Returns:
ndarray: The image with the point drawn on it.
"""
if heightImage >= 1000:
cv2.circle(image, coordinatePoint, 10, (0, 255, 0), 20, -1)
else:
cv2.circle(image, coordinatePoint, 6, (0, 255, 0), 12, -1)
return image
[docs]
def image_resize(image, width=None, height=None, inter=cv2.INTER_AREA):
"""
Resizes an image while maintaining its aspect ratio.
Args:
image (ndarray): The image to resize.
width (int, optional): The desired width of the resized image.
height (int, optional): The desired height of the resized image.
inter: The interpolation method for resizing.
Returns:
ndarray: The resized image.
"""
dim = None
(h, w) = image.shape[:2]
if width is None and height is None:
return image
if width is None:
r = height / float(h)
dim = (int(w * r), height)
else:
r = width / float(w)
dim = (width, int(h * r))
resized = cv2.resize(image, dim, interpolation=inter)
return resized
[docs]
def corner_detect(image, sigma=1, threshold=0.01):
"""
Detects corners in an image using Harris corner detection.
Args:
image (ndarray): The image in which to detect corners.
sigma (float): The standard deviation of the Gaussian filter.
threshold (float): The threshold for detecting corners.
Returns:
list: A list of detected corners.
"""
# height, width = image.shape
# shape = (height, width)
# Calculate the dx,dy gradients of the image (np.gradient doesnt work)
dx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=5)
dy = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=5)
# Get angle for rotation
_, ang = cv2.cartToPolar(dx, dy, angleInDegrees=True)
# Square the derivatives (A,B,C of H) and apply apply gaussian filters to each
sigma = (sigma, sigma)
Ixx = cv2.GaussianBlur(dx * dx, sigma, 0)
Ixy = cv2.GaussianBlur(dx * dy, sigma, 0)
Iyy = cv2.GaussianBlur(dy * dy, sigma, 0)
H = np.array([[Ixx, Ixy], [Ixy, Iyy]])
# Find the determinate
num = (H[0, 0] * H[1, 1]) - (H[0, 1] * H[1, 0])
# Find the trace
denom = H[0, 0] + H[1, 1]
# Find the response using harmonic mean of the eigenvalues (Brown et. al. variation)
R = np.nan_to_num(num / denom)
# Adaptive non-maximum suppression, keep the top 1% of values and remove non-maximum points in a 9x9 neighbourhood
R_flat = R[:].flatten()
# Get number of values in top threshold %
N = int(len(R_flat) * threshold)
# Get values in top threshold %
top_k_percentile = np.partition(R_flat, -N)[-N:]
# Find lowest value in top threshold %
minimum = np.min(top_k_percentile)
# Set all values less than this to 0
R[R < minimum] = 0
# Set non-maximum points in an SxS neighbourhood to 0
s = 9
for h in range(R.shape[0] - s):
for w in range(R.shape[1] - s):
maximum = np.max(R[h:h + s + 1, w:w + s + 1])
for i in range(h, h + s + 1):
for j in range(w, w + s + 1):
if R[i, j] != maximum:
R[i, j] = 0
# Return harris corners in [H, W, R] format
features = list(np.where(R > 0))
features.append(ang[np.where(R > 0)])
corners = zip(*features)
return list(corners)
[docs]
def draw_corners(corners, image):
"""
Draws detected corners on an image.
Args:
corners (list): The list of detected corners.
image (ndarray): The image on which to draw the corners.
Returns:
ndarray: The image with the corners drawn on it.
"""
i = 0
for h, w in corners:
cv2.circle(image, (w, h), 3, (0, 0, 255), -1)
# caption = '{},{}'.format(h, w)
cv2.putText(image, str(i), (w - 5, h + 10), cv2.FONT_HERSHEY_COMPLEX, 0.3, (0, 255, 0))
i += 1
return image
[docs]
def get_corner_list(corners):
"""
Extracts corner coordinates from the detected corners.
Args:
corners (list): The list of detected corners.
Returns:
list: A list of coordinates of the corners.
"""
coor = []
for h, w, r in corners:
caption = '{},{}'.format(h, w)
res = tuple(map(int, caption.split(',')))
coor.append(res)
return coor
[docs]
def distance(point_a, point_b):
"""
Calculates the distance between two points.
Args:
point_a (tuple): The first point.
point_b (tuple): The second point.
Returns:
float: The distance between the points.
"""
x0, y0 = point_a
x1, y1 = point_b
return math.fabs(x0 - x1) + math.fabs(y0 - y1)
[docs]
def draw_matches(matches, img_left, img_right, verbose=False):
"""
Draws lines between matched points in two images.
Args:
matches (list): A list of matched points.
img_left (ndarray): The left image.
img_right (ndarray): The right image.
verbose (bool): If True, displays the image with matches.
Returns:
ndarray: The image with matches drawn on it.
"""
# Determine the max height
height = max(img_left.shape[0], img_right.shape[0])
# Width is the two images side-by-side
width = img_left.shape[1] + img_right.shape[1]
img_out = np.zeros((height, width, 3), dtype=np.uint8)
# Place the images in the empty image
img_out[0:img_left.shape[0], 0:img_left.shape[1], :] = img_left
img_out[0:img_right.shape[0], img_left.shape[1]:, :] = img_right
# The right image coordinates are offset since the image is no longer at (0,0)
ow = img_left.shape[1]
# Draw a line between the matched pairs in green
for p1, p2 in matches:
p1o = (int(p1[1]), int(p1[0]))
p2o = (int(p2[1] + ow), int(p2[0]))
color = list(np.random.random(size=3) * 256)
cv2.line(img_out, p1o, p2o, color, thickness=2)
if verbose:
print("Press enter to continue ... ")
cv2.imshow("matches", img_out)
cv2.waitKey(0)