Source code for ObjectDetection

######################################################################
#          O  o   o o-o   o--o   o-o  o   o o--o o-o   o--o          #
#         / \ |\  | |  \  |   | o   o |\ /| |    |  \  |             #
#        o---o| \ | |   O O-Oo  |   | | O | O-o  |   O O-o           #
#        |   ||  \| |  /  |  \  o   o |   | |    |  /  |             #
#        o   oo   o o-o   o   o  o-o  o   o o--o o-o   o--o          #
######################################################################
#
# ANDROMEDE
# Copyright (C) 2023 Toulouse INP
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details :
# <http://www.gnu.org/licenses/>.
#
######################################################################

""" This module manages the detection of object on video frame.
It allows the choice of the method to mots appropriate to discriminate the particles from the free surface.
"""

from PySide6.QtWidgets import QProgressDialog
from PySide6.QtGui import Qt
import cv2
import numpy as np
import numpy.matlib
import time


# detect objects and store inside results.objs variable which contains information for each frame 'id_frame'
# a numpy array with one line for each object 'id_obj' detected
# results.objs[id_frame][id_obj] = [id_traj, x, y, size, intensity]
# id_traj is set to -1 by default before motion detection


[docs] def select_method(param): """Function for detection method selection Several methods are possible. The good feature to track method is the most common method based on the shi method. It is original method with the opyflow software. The threshold is the simplest but needs the user calibration. The Dog Method is the original method for tractrac software The Histogram is dedicated to track of specific object. :param dict: dictionnary parameters :rtype: int """ id_method = 0 if param.dict['OD']['method'] == 'GoodFeatureToTrack': id_method = 1 if param.dict['OD']['method'] == 'Threshold': id_method = 2 if param.dict['OD']['method'] == 'DoG': id_method = 3 if param.dict['OD']['method'] == 'Histogram': id_method = 4 if param.dict['OD']['method'] == 'none': id_method = 0 if param.dict['OD']['method'] == 'Manual': id_method = 5 return id_method
def process_object_detection_all(video, params, id_method): elapsed_time = time.time() objs = [] if id_method == 4: params = track_manual_detection_init(video.imgs[0], params) video.outputConsole.appendPlainText('Object detection...') progress = QProgressDialog('Object Detection', 'Stop', 0, 100) progress.setMinimumDuration(0) progress.setWindowModality(Qt.WindowModal) progress.setWindowTitle('Object Detection') num_images = len(video.imgs) for i in range(num_images): progress.setLabelText('Processing frame ' + str(i)) objs.append(process_object_detection(video.imgs[i], params, id_method)) progress.setValue(int(100 * (i + 1) / num_images)) if progress.wasCanceled(): break elapsed_time = time.time() - elapsed_time video.outputConsole.appendPlainText(str(len(objs)) + ' images processed in ' + str(round(elapsed_time, 2)) + ' seconds\n') video.update_process_status(3) return objs
[docs] def process_object_detection(img, params, id_method): """Processing selected detection method :param img: frame treated :param dict: dictionnary parameters :param id_method: detection method :return: list of detected particles positions [ID trajectory, X position, Y position, size, intensity] :rtype: list """ id_traj, x, y, size, intensity = [[] for _ in range(5)] if id_method == 1: x, y = method_GoodFeatureToTrack(img, params) size = np.zeros(len(x)) intensity = np.zeros(len(x)) elif id_method == 2: x, y, size, intensity = method_threshold(img, params) elif id_method == 3: x, y = method_DoG(img, params) size = np.zeros(len(x)) intensity = np.zeros(len(x)) elif id_method == 4: x, y, size, intensity = histogram_detection(img, params) if id_method > 0: id_traj = np.full(shape=len(x), fill_value=-1) return np.transpose(np.stack((id_traj, x, y, size, intensity)))
[docs] def method_GoodFeatureToTrack(img, params): """Shi and Tomasi Method directly availiable in openCV library :param img: frame treated :param max_corner: nombre maximal de détection par image :param quality_level: :param min_distance: :param block_size: :return: list of detected particles positions [X position, Y position] :rtype: list """ new_objs = cv2.goodFeaturesToTrack(img, maxCorners=params.dict['OD']['max_corner'], qualityLevel=params.dict['OD']['quality_level'], minDistance=params.dict['OD']['min_distance'], blockSize=params.dict['OD']['block_size']) x, y = np.array([cc[0, 0] for cc in new_objs]), np.array([cc[0, 1] for cc in new_objs]) return x, y
[docs] def method_threshold(img, params): """Frame thresholding with binarization to extract position, area and intensity of each particle :param img: frame treated :param intensity_min: minimal intensity of selected particles(in level 0-255) :param intensity_max: maximal intensity of selected particles (in level 0-255) :param size_min: minimal length of selected particles (in pixel) :param size_max: maximal length of selected particles (in pixel) :param lo: minimal intensity of selected particles for color frame (3 values ex: RGB, HSV) :param hi: maximal intensity of selected particles for color frame (3 values ex: RGB, HSV) :return: list of detected particles positions [ID trajectory, X position, Y position, size, intensity] :rtype: list """ x, y, z, intensity = [[] for _ in range(4)] if params.dict['IP']['color_choice']: _, filtered_img_temp1 = cv2.threshold(img, params.dict['OD']['intensity_min'], 1, cv2.THRESH_BINARY) _, filtered_img_temp2 = cv2.threshold(img, params.dict['OD']['intensity_max'], 1, cv2.THRESH_BINARY) filtered_img = cv2.bitwise_and(filtered_img_temp1, img.max() - filtered_img_temp2) else: filtered_img = cv2.inRange(img, params.dict['OD']['lo'], params.dict['OD']['hi']) contours, hierarchy = cv2.findContours(filtered_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) for i in range(0, len(contours)): ((x_obj, y_obj), rayon) = cv2.minEnclosingCircle(contours[i]) area = cv2.contourArea(contours[i]) int_moy = img[int(y_obj), int(x_obj)] if params.dict['OD']['size_max'] ** 2 > area > params.dict['OD']['size_min'] ** 2: x.append(x_obj) y.append(y_obj) z.append(area) intensity.append(int_moy) return x, y, z, intensity
[docs] def method_DoG(img, params): """Original method of tractrac software. The detection is obtained by the convolution of the frame by a double gaussian function. :param img: frame treated :param peak_conv_size: size of the gaussian for filtering (in pixel) :param type_particle: Light/Dark particle :param peak_sub_pix: method for sub-pixel detection No method, Quadratic ,Gaussian :param peak_th_auto: (True/False) use automatic threshold to find maximum value on the convoluted frame :param peak_th: threshold to find maximum value on the convoluted frame :param peak_neigh: size of filter for the convoluted frame (pixel) :return: list of detected particles positions [X position, Y position] :rtype: list """ def blobDetection(): """ Perform the frame convolution """ # LoG or DoG detection kernel scale = params.dict['OD']['peak_conv_size'] size = (int(np.maximum(3, (int(scale * 3.) // 2) * 2 + 1)), int(np.maximum(3, (int(scale * 3.) // 2) * 2 + 1))) f_img = cv2.GaussianBlur(img, size, scale * 0.8, cv2.BORDER_REPLICATE) f_img = cv2.GaussianBlur(f_img, size, scale * 1.2, cv2.BORDER_REPLICATE) - f_img if params.dict['OD']['type_particle'] == 'Dark particle': f_img -= f_img return f_img def maximaThreshold(a, n): """ Find maxima on the convoluted frame """ # Find n*n local max method = params.dict['OD']['peak_sub_pix'] a = np.float64(a) if a.max() < params.dict['OD']['peak_th']: print('Warning : peak_th ({:1.4f}) is above the maximum image convoluted value ({:1.4f}).'.format( params.dict['OD']['peak_th'], a.max())) r = np.random.rand(np.shape(a)[0], np.shape(a)[1]) * 1e-5 mask = np.ones((n, n), np.uint8) b = cv2.dilate(a + r, mask, iterations=1) c = ((a + r == b) & (a > params.dict['OD']['peak_th'])) [y, x] = np.where(c) def subPix2nd(a2): """ Compute the subpixel position """ def sub2ind(array_shape, rows, cols): return np.uint32(rows * array_shape[1] + cols) n_pen = np.floor(n / 2.) pencil = np.arange(-n_pen, n_pen + 1) x2 = np.matlib.repmat(pencil, np.size(x), 1) y_h = np.zeros(x2.shape) y_v = np.zeros(x2.shape) for i in range(0, len(pencil)): id_v = sub2ind(np.shape(a), np.maximum(0, np.minimum(a.shape[0] - 1, y + pencil[i])), x) id_h = sub2ind(np.shape(a), y, np.maximum(0, np.minimum(a.shape[1] - 1, x + pencil[i]))) y_v[:, i] = a2.flat[id_v] y_h[:, i] = a2.flat[id_h] # 2nd order poly a+bx+cx^2=0 s2 = np.sum(pencil ** 2.) s4 = np.sum(pencil ** 4.) b_h = np.sum(y_h * x2, 1) / s2 c_h = -(-s2 * np.sum(y_h, 1) + n * np.sum(x2 ** 2. * y_h, 1)) / (s2 ** 2. - s4 * n) b_v = np.sum(y_v * x2, 1) / s2 c_v = -(-s2 * np.sum(y_v, 1) + n * np.sum(x2 ** 2. * y_v, 1)) / (s2 ** 2. - s4 * n) c_h[c_h == 0] = 1e-8 c_v[c_v == 0] = 1e-8 # Peaks on hor and vert axis d_h = -b_h / c_h / 2. d_v = -b_v / c_v / 2. return d_h, d_v if method != 'No method': # Remove points on border w = np.shape(a)[1] h = np.shape(a)[0] nb = np.floor(n / 2.) id_bound = np.where((y < h - nb) & (y >= nb) & (x < w - nb) & (x >= nb)) x = np.array(np.float64(x[id_bound])).reshape(-1) y = np.array(np.float64(y[id_bound])).reshape(-1) # Subpixel Refinement Method # 2nd order poly fit on the logarithm of diagonal of a d_x = np.zeros(x.shape) d_y = np.zeros(x.shape) if method == 'Quadratic': [d_x, d_y] = subPix2nd(np.real(np.log(a - np.min(a) + 1e-8))) # Gaussian if method == 'Gaussian': [d_x, d_y] = subPix2nd(a) # Quadratic # Take only peaks that moved less than 0.5 id_good = np.where((np.abs(d_x) < 0.5) & (np.abs(d_y) < 0.5)) # print x,y,Dx,Dy x = x[id_good] + d_x[id_good] y = y[id_good] + d_y[id_good] return x, y # DoG method if img.dtype == 'uint8': img = np.float32(img) / 256. # Always convert to float Images if img.dtype == 'uint16': img = np.float32(img) / 2. ** 16 # Always convert to float filtered_img = blobDetection() # Replace threshold if auto and not Shi and Tomasi if params.dict['OD']['peak_th_auto']: params.dict['OD']['peak_th'] = np.mean(filtered_img) + 0.5 * np.std(filtered_img) return maximaThreshold(filtered_img, 1 + 2 * int(params.dict['OD']['peak_neigh']))
[docs] def track_manual_detection_init(img, params): """ Define the histogram of the selected ROI :param nbr_classes: number of class in the histogram :param color_choice: type of image color (RGB,HSV,gray) :param color_band: band for the histogram measurement (RGB,HSV,gray) :return: size of the ROI selected :rtype: list (height, width) :return: histogram value in selected ROI :rtype: list """ cv2.namedWindow('ROI', flags=cv2.WINDOW_NORMAL | cv2.WINDOW_FREERATIO) nbr_classes = params.dict['OD']['nbr_classes'] roi_x, roi_y, roi_w, roi_h = cv2.selectROI('ROI', img, False, False) roi = img[roi_y: roi_y + roi_h, roi_x: roi_x + roi_w] params.dict['OD']['roi_detect'] = [roi_w, roi_h] if len(roi) > 0: if params.dict['IP']['color_choice'] == 1: roi_hist = cv2.calcHist([roi], [0], None, [nbr_classes], [0, nbr_classes]) else: if params.dict['IP']['color_choice'] == 2: img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) roi_hsv = img[roi_y: roi_y + roi_h, roi_x: roi_x + roi_w] roi_hist = cv2.calcHist([roi_hsv], [int(params.dict['OD']['color_band']) - 1], None, [int(nbr_classes)], [0, int(nbr_classes)]) cv2.normalize(roi_hist[0, :], roi_hist[0, :], 0, 255, cv2.NORM_MINMAX) params.dict['OD']['roi_hist'] = roi_hist cv2.destroyAllWindows() return params
[docs] def histogram_detection(img, params): """ Find area corresponding to selected histogram :param nbr_classes: number of class in the histogram :param roi_detect: size of the ROI selected :param color_choice: type of image color (RGB,HSV,gray) :param color_band: band for the histogram measurement (RGB,HSV,gray) :return: list of detected particles positions [ID trajectory, X position, Y position, size, intensity] :rtype: list """ nbr_classes = params.dict['OD']['nbr_classes'] min_size = (np.min(params.dict['OD']['roi_detect']) * 0.8) ** 2 max_size = (np.max(params.dict['OD']['roi_detect']) * 1.2) ** 2 if params.dict['IP']['color_choice'] == 1: mask = cv2.calcBackProject([img], [0], params.dict['OD']['roi_hist'], [0, nbr_classes], 1) else: if params.dict['IP']['color_choice'] == 2: img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask = cv2.calcBackProject([img], [int(params.dict['OD']['color_band']) - 1], params.dict['OD']['roi_hist'], [0, int(nbr_classes)], 1) _, mask2 = cv2.threshold(mask, 0, 255, cv2.THRESH_BINARY) mask2 = cv2.erode(mask2, None, iterations=1) mask2 = cv2.dilate(mask2, None, iterations=1) image2 = cv2.bitwise_and(img, img, mask=mask2) contours, hierarchy = cv2.findContours(mask2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) x, y, z, intensity = [[] for _ in range(4)] for i in range(0, len(contours)): ((x_obj, y_obj), rayon) = cv2.minEnclosingCircle(contours[i]) area = cv2.contourArea(contours[i]) if params.dict['IP']['color_choice'] == 1: image3 = image2 else: image3 = image2[:, :, int(params.dict['OD']['color_band'] - 1)] int_moy = image3[int(y_obj), int(x_obj)] if max_size > area > min_size: x.append(x_obj) y.append(y_obj) z.append(area) intensity.append(int_moy) return x, y, z, intensity
def manual_detection(objs, obj_id, x_manual, y_manual): if not objs.shape[0]: objs = np.zeros((1, 5)) objs[0, 0:3] = [obj_id, x_manual, y_manual] elif objs.shape[0] == 1: if objs[0,0] == obj_id: objs[0, 1:3] = [x_manual, y_manual] else: objs = np.vstack([objs, np.array((obj_id, x_manual, y_manual, 0, 0))]) else: id_line = np.where( objs[:, 0] == obj_id)[0] if id_line.size > 0: objs[id_line[0], 1:3] = [x_manual, y_manual] else: objs = np.vstack([objs, np.array((obj_id, x_manual, y_manual, 0, 0))]) return objs