From 228c2e49f09d0d9221fb56f90f24c9ded5a0d7a6 Mon Sep 17 00:00:00 2001 From: ljubicicrobert <71554915+ljubicicrobert@users.noreply.github.com> Date: Tue, 5 Apr 2022 19:58:21 +0200 Subject: [PATCH] Add colorspace.py --- camera_profiles/DJI Mini 2.cpf | 16 +- gui/SSIM Stabilization GUI.application | 2 +- gui/SSIM Stabilization GUI.exe | Bin 354304 -> 355840 bytes gui/SSIM Stabilization GUI.exe.manifest | 4 +- gui/update.ini | 8 +- release_notes.txt | 12 + scripts/__init__.py | 4 +- scripts/colorspaces.py | 258 +++++++++++++++ scripts/filter_frames.py | 421 +++++++++++++----------- scripts/filters.xml | 269 ++++++++------- scripts/inspect_frames.py | 12 +- 11 files changed, 658 insertions(+), 348 deletions(-) create mode 100644 scripts/colorspaces.py diff --git a/camera_profiles/DJI Mini 2.cpf b/camera_profiles/DJI Mini 2.cpf index 6e05bc2..7279f0d 100644 --- a/camera_profiles/DJI Mini 2.cpf +++ b/camera_profiles/DJI Mini 2.cpf @@ -2,16 +2,16 @@ Model = DJI Mini 2 [Intrinsics] -fx = 0.7126984127 -fy = 0.7126984127 -cx = 0.5067482020 -cy = 0.3716643567 +fx = 0.7568749675 +fy = 0.7568749675 +cx = 0.4995236875 +cy = 0.49861405333 [Radial] -k1 = 0.0298194 -k2 = -0.110242 +k1 = -0.00362736 +k2 = -0.00130119 k3 = 0.0 [Tangential] -p1 = 0.0007491 -p2 = 0.0016115 +p1 = 0.000287332 +p2 = -0.00220176 diff --git a/gui/SSIM Stabilization GUI.application b/gui/SSIM Stabilization GUI.application index d345ab5..7e61b40 100644 --- a/gui/SSIM Stabilization GUI.application +++ b/gui/SSIM Stabilization GUI.application @@ -14,7 +14,7 @@ - 5cQEWx+UnFUAP2k7WHa630JMdqJrtd281T/iVQWAS7w= + 3RPsOZN78HtDmcHYz9R2vEDeiPdk86kuVOnX2XWb9DA= diff --git a/gui/SSIM Stabilization GUI.exe b/gui/SSIM Stabilization GUI.exe index 70aa71e94d63a3be48f4237653b8555ca9e44b7b..6d2a40a7f5bcb0dcd90509bf754b7bc5c18bb233 100644 GIT binary patch delta 59721 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 diff --git a/gui/SSIM Stabilization GUI.exe.manifest b/gui/SSIM Stabilization GUI.exe.manifest index 7259d68..105444d 100644 --- a/gui/SSIM Stabilization GUI.exe.manifest +++ b/gui/SSIM Stabilization GUI.exe.manifest @@ -104,14 +104,14 @@ - + - /v7FKLV31aXlizNnHufDdFIzUxggDtTkrLuWv6dbCP0= + 1SUVMnjos5qUF8w1wPSg2INKvq5fIY0VWlQgF7N6+pQ= diff --git a/gui/update.ini b/gui/update.ini index a93bb2b..db578cb 100644 --- a/gui/update.ini +++ b/gui/update.ini @@ -1,8 +1,8 @@ [Updates] -CurrentReleaseTag = v0.3.1.4 -CurrentReleaseName = SSIMS_v0.3.1.4 -CurrentReleaseDate = 2022-03-11 13:36:03.252079 -LastCheckDate = 2022-03-11 13:36:03.252079 +CurrentReleaseTag = v0.3.2.0 +CurrentReleaseName = SSIMS_v0.3.2.0 +CurrentReleaseDate = 2022-04-05 17:57:43.422532 +LastCheckDate = 2022-04-05 17:57:43.422532 DisableUpdateCheck = 0 PauseDays = 0 diff --git a/release_notes.txt b/release_notes.txt index 9f3b2b8..287d92c 100644 --- a/release_notes.txt +++ b/release_notes.txt @@ -1,3 +1,15 @@ +SSIMS_v0.3.2.0 ---------------------------------------------------------------- + +Major changes: +- Added "Explore colorspaces" option in Filter frames form + +Minor changes: +- Added new filters and polished old ones +- RGB model now default for filtering +- Keybindings changed for inspect_images.py +- New DJI Mini 2 parameters from Metashape + + SSIMS_v0.3.1.4 ---------------------------------------------------------------- Minor changes: diff --git a/scripts/__init__.py b/scripts/__init__.py index 4724a26..a4706b6 100644 --- a/scripts/__init__.py +++ b/scripts/__init__.py @@ -8,9 +8,9 @@ __package_name__ = 'SSIMS: Preprocessing tool for UAV image velocimetry' __description__ = 'Preprocessing and video stabilization tool for UAS/UAV image velocimetry based on Structural Similarity (SSIM) Index metric' -__version__ = '0.3.1.4' +__version__ = '0.3.2.0' __status__ = 'beta' -__date_deployed__ = '2022-03-11' +__date_deployed__ = '2022-04-05' __author__ = 'Robert Ljubicic, University of Belgrade - Civil Engineering Faculty' __author_email__ = 'rljubicic@grf.bg.ac.rs' diff --git a/scripts/colorspaces.py b/scripts/colorspaces.py new file mode 100644 index 0000000..e28482d --- /dev/null +++ b/scripts/colorspaces.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +This 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 package 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. + +You should have received a copy of the GNU General Public License +along with this package. If not, you can get eh GNU GPL from +https://www.gnu.org/licenses/gpl-3.0.en.html. + +Created by Robert Ljubicic. +""" + +try: + from __init__ import * + from matplotlib.widgets import Slider + from sys import exit + + import glob + import matplotlib.pyplot as plt + +except Exception as ex: + print('\n[EXCEPTION] Import failed: \n\n' + ' {}'.format(ex)) + input('\nPress ENTER/RETURN to exit...') + exit() + + +def snr(a): + m = np.mean(a) + sd = np.std(a) + return m/sd + + +def update_frame(val): + global current_frame + + current_frame = frames_list[sl_ax_frame_num.val] + get_colospaces(current_frame, xlim, ylim) + + plt.draw() + + +def keypress(event): + if event.key == 'escape': + exit() + + elif event.key == 'down': + if sl_ax_frame_num.val == 0: + sl_ax_frame_num.set_val(num_frames - 1) + else: + sl_ax_frame_num.set_val(sl_ax_frame_num.val - 1) + + elif event.key == 'up': + if sl_ax_frame_num.val == num_frames - 1: + sl_ax_frame_num.set_val(0) + else: + sl_ax_frame_num.set_val(sl_ax_frame_num.val + 1) + + elif event.key == 'pageup': + if sl_ax_frame_num.val >= num_frames - 10: + sl_ax_frame_num.set_val(0) + else: + sl_ax_frame_num.set_val(sl_ax_frame_num.val + 10) + + elif event.key == 'pagedown': + if sl_ax_frame_num.val <= 9: + sl_ax_frame_num.set_val(num_frames - 1) + else: + sl_ax_frame_num.set_val(sl_ax_frame_num.val - 10) + + update_frame(sl_ax_frame_num.val) + + +def get_colospaces(path, xlim, ylim): + img_bgr = cv2.imread(path) + img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) + img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) + img_lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB) + + cs_list = [ + img_rgb, + img_rgb[:, :, 0], + img_rgb[:, :, 1], + img_rgb[:, :, 2], + img_gray, + img_hsv[:, :, 0], + img_hsv[:, :, 1], + img_hsv[:, :, 2], + None, + img_lab[:, :, 0], + img_lab[:, :, 1], + img_lab[:, :, 2], + ] + + for i in range(len(cs_list)): + if cs_list[i] is not None: + img = cs_list[i] + imshow_list[i].set_data(img) + ax_list[i].set_xlim(xlim) + ax_list[i].set_ylim(ylim) + ax_list[i].set_title(cs_names[i]) + + if i == 0: + ax_list[i].set_title(cs_names[i]) + else: + ax_snr = snr(img[ylim[1]: ylim[0], xlim[0]: xlim[1]]) + ax_list[i].set_title('{}, SNR={:.2f}'.format(cs_names[i], ax_snr)) + + +def on_lims_change(event_ax): + global xlim + global ylim + + cid_list = list(event_ax.callbacks.callbacks['ylim_changed'].keys()) + for cid in cid_list: + event_ax.callbacks.disconnect(cid) + + xlim = [int(x) for x in event_ax.get_xlim()] + ylim = [int(y) for y in event_ax.get_ylim()] + + event_ax.set_xlim(xlim) + event_ax.set_ylim(ylim) + + for i in range(1, len(cs_names)): + if cs_names[i] != '': + a = ax_list[i] + ax_title = a.get_title().split(', ')[0] + ax_img = np.array(a.get_images()[0]._A) + ax_img_crop = ax_img[ylim[1]: ylim[0], xlim[0]: xlim[1]] + ax_snr = snr(ax_img_crop) + + a.set_title('{}, SNR={:.2f}'.format(ax_title, ax_snr)) + + event_ax.callbacks.connect('ylim_changed', on_lims_change) + + +if __name__ == '__main__': + try: + parser = ArgumentParser() + parser.add_argument('--folder', type=str, help='Path to image file or folder with images') + parser.add_argument('--ext', type=str, help='Path to image file') + args = parser.parse_args() + + frames_list = glob.glob('{}/*.{}'.format(args.folder, args.ext)) + num_frames = len(frames_list) + first_frame = cv2.imread(frames_list[0], 0) + + h, w = first_frame.shape + xlim = [0, w] + ylim = [h, 0] + + nrows, ncols = 3, 4 + fig, ax = plt.subplots(nrows=nrows, ncols=ncols, sharex=True, sharey=True) + plt.subplots_adjust(left=0.01, right=0.99, top=0.96, bottom=0.06, wspace=0.02, hspace=0.1) + fig.canvas.mpl_connect('key_press_event', keypress) + + legend = 'Use O to zoom and P to pan images,\n' \ + 'Use slider to select frame,\n' \ + 'use UP and DOWN keys to move by +/- 1 frame\n' \ + 'or PageUP and PageDOWN keys to move by +/- 10 frames\n' \ + 'Press ESC or Q to exit' + + legend_toggle = plt.text(0.5, 0.5, legend, + horizontalalignment='center', + verticalalignment='center', + transform=ax[2][0].transAxes, + bbox=dict(facecolor='white', alpha=0.5), + fontsize=9, + ) + + axcolor = 'lightgoldenrodyellow' + valfmt = "%d" + + ax_frame_num = plt.axes([0.2, 0.02, 0.63, 0.03], facecolor=axcolor) + sl_ax_frame_num = Slider(ax_frame_num, 'Frame #\n({} total)'.format(num_frames), 0, num_frames - 1, valinit=0, valstep=1, valfmt=valfmt) + sl_ax_frame_num.on_changed(update_frame) + + cs_names = [ + 'Original RGB', + '[R]GB', + 'R[G]B', + 'RG[B]', + 'Grayscale', + '[H]SV', + 'H[S]V', + 'HS[V]', + '', + '[L*]a*b*', + 'L*[a*]b*', + 'L*a*[b*]', + ] + + ax_list = ax.reshape(-1) + imshow_list = [None] * 12 + + current_frame = frames_list[0] + + img_bgr = cv2.imread(current_frame) + img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) + img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) + img_lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB) + + cs_list = [ + img_rgb, + img_rgb[:, :, 0], + img_rgb[:, :, 1], + img_rgb[:, :, 2], + img_gray, + img_hsv[:, :, 0], + img_hsv[:, :, 1], + img_hsv[:, :, 2], + None, + img_lab[:, :, 0], + img_lab[:, :, 1], + img_lab[:, :, 2], + ] + + for i in range(len(cs_list)): + if cs_list[i] is not None: + img = cs_list[i] + imshow_list[i] = ax_list[i].imshow(img) + ax_list[i].set_xlim(xlim) + ax_list[i].set_ylim(ylim) + ax_list[i].set_title(cs_names[i]) + + if i == 0: + ax_list[i].set_title(cs_names[i]) + else: + ax_snr = snr(img[ylim[1]: ylim[0], xlim[0]: xlim[1]]) + ax_list[i].set_title('{}, SNR={:.2f}'.format(cs_names[i], ax_snr)) + + [a.set_axis_off() for a in ax_list] + [a.callbacks.connect('ylim_changed', on_lims_change) for a in ax_list] + + try: + mng = plt.get_current_fig_manager() + mng.window.state('zoomed') + mng.set_window_title('Inspect frames') + except: + pass + + plt.show() + + except Exception as ex: + print('\n[EXCEPTION] The following exception has occurred: \n\n' + ' {}'.format(ex)) + input('\nPress ENTER/RETURN to exit...') \ No newline at end of file diff --git a/scripts/filter_frames.py b/scripts/filter_frames.py index a838069..5195e74 100644 --- a/scripts/filter_frames.py +++ b/scripts/filter_frames.py @@ -29,6 +29,7 @@ import glob import matplotlib.pyplot as plt import mplcursors + import scipy.stats as stats except Exception as ex: print('\n[EXCEPTION] Import failed: \n\n' @@ -37,164 +38,195 @@ exit() separator = '---' +colormap = 'viridis' + +colorspaces_list = ['rgb', 'hsv', 'lab', 'grayscale'] +color_conv_codes = [ + [[], [41], [45], [7]], + [[55], [], [55, 45], [55, 7]], + [[57], [57, 41], [], [57, 7]], + [[8], [8, 41], [8, 45], []] +] + + +def convert_img(img, from_cs, to_cs): + from_cs_index = colorspaces_list.index(from_cs) + to_cs_index = colorspaces_list.index(to_cs) + + conv_codes = color_conv_codes[from_cs_index][to_cs_index] + + if len(conv_codes) == 0: + return img + + for i, code in enumerate(conv_codes): + img = cv2.cvtColor(img, code) + + return img + + +def is_grayscale(img): + if (img[:, :, 0] == img[:, :, 1]).all() and (img[:, :, 0] == img[:, :, 2]).all(): + return True + else: + return False -def addBackgroundImage(fore: np.ndarray, back: np.ndarray) -> np.ndarray: - background = back.copy().astype(float) - alpha = fore - - try: # Try to read the third dimension of an array. If fails, the array is 2D. - background.shape[2] - dimension = 3 - except IndexError: - dimension = 2 +def func(name, image, params): + return name(image, *params) + + +def negative(img): + print('[FILTER] Convert to image negative') + return ~img - foreground = fore.copy().astype(float) - alpha = alpha.copy().astype(float) / 255 - if dimension == 3: - if len(foreground.shape) == 2: - # Color me purple - foreground = np.stack((foreground,) * 3, -1) - foreground = np.where(foreground == [0., 0., 0.], [0., 0., 0.], [0., 0., 0.]) - alpha = np.stack((alpha,) * 3, -1) - else: - foreground = np.where(foreground == [0., 0., 0.], [0., 0., 0.], [0., 0., 0.]) +def to_grayscale(img): + img_gray = convert_img(img, colorspace, 'grayscale') + print('[FILTER] Convert to grayscale') + return convert_img(img_gray, 'grayscale', colorspace) - foreground = cv2.multiply(alpha, foreground) - alpha = 1.0 - alpha - background = cv2.multiply(alpha, background) - combined = cv2.add(foreground, background) - return combined.astype('uint8') +def to_rgb(img): + print('[FILTER] Convert to RGB colorspace') + return convert_img(img, colorspace, 'rgb') + + +def to_hsv(img): + print('[FILTER] Convert to HSV colorspace') + return convert_img(img, colorspace, 'hsv') + +def to_lab(img): + print('[FILTER] Convert to L*a*b* colorspace') + return convert_img(img, colorspace, 'lab') + -def func(name, image, params): - return name(image, *params) +def select_channel(img, channel=1): + try: + img_single = img[:, :, int(channel)-1] + except IndexError: + print('[ ERROR] Image is already single channel, cannot select channel {}'.format(channel)) + return img + + print('[FILTER] Selecting channel {}'.format(channel)) + return cv2.merge([img_single, img_single, img_single]) + + +def highpass(img, sigma=51): + if sigma % 2 == 1: + sigma += 1 + blur = cv2.GaussianBlur(img, (0, 0), int(sigma)) -def histeq(img): - print('[FILTER] Histogram equalization') - eq = cv2.equalizeHist(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) - return cv2.cvtColor(eq, cv2.COLOR_GRAY2BGR) + print('[FILTER] Highpass filter: sigma={:.0f}'.format(sigma)) + return ~cv2.subtract(cv2.add(blur, 127), img) + + +def normalize_image(img, lower=None, upper=None): + if lower is None: + lower = np.min(img) + if upper is None: + upper = np.max(img) + img_c = img.astype(int) -def clahe(img, clip=2.0, tile=8): - print('[FILTER] CLAHE: clip={:.1f}, tile={:.0f}'.format(clip, tile)) - clahe = cv2.createCLAHE(clipLimit=clip, tileGridSize=(int(tile), int(tile))) - return cv2.cvtColor(clahe.apply(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)), cv2.COLOR_GRAY2BGR) + img_c = ((img_c - np.min(img_c)) / (np.max(img_c) - np.min(img_c)) * 255).astype('uint8') + return img_c -def denoise(img, h=3, hcolor=3, template_size=7, search_size=21): - print('[FILTER] Denoise: h={:.0f}, hcolor={:.0f}, template_size={:.0f}, search_size={:.0f}'.format(h, hcolor, template_size, search_size)) - if template_size % 2 == 1: - template_size += 1 - if search_size % 2 == 1: - search_size += 1 - return cv2.fastNlMeansDenoisingColored(img, None, int(h), int(hcolor), int(template_size), int(search_size)) +def intensity_capping(img, n_std=0.0): + img_g = convert_img(img, colorspace, 'grayscale') + + median = np.median(img_g) + stdev = np.std(img_g) + cap = median - n_std * stdev -def hsv_filter(img, hu=255, hl=0, su=255, sl=0, vu=255, vl=0): - print('[FILTER] HSV: Hu={:.0f}, Hl={:.0f}, Su={:.0f}, Sl={:.0f}, Vu={:.0f}, Vl={:.0f}'.format(hu, hl, su, sl, vu, vl)) - img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) - mask = cv2.inRange(img, (hl, sl, vl), (hu, su, vu)) - return mask - + img_g[img_g > cap] = cap + img_g = normalize_image(img_g, cap, np.max(img_g)) + print('[FILTER] Pixel intensity capping: n_std={:.1f}'.format(n_std)) + return convert_img(img_g, 'grayscale', colorspace) + + def brightness_contrast(img, alpha=1.0, beta=0.0): print('[FILTER] Brightness and contrast: alpha={:.1f}, beta={:.1f}'.format(alpha, beta)) - new = cv2.convertScaleAbs(img, alpha=alpha, beta=beta) - return new + return cv2.convertScaleAbs(img, alpha=alpha, beta=beta) def gamma(img, gamma=1.0): - print('[FILTER] Gamma correction: gamma={:.1f}'.format(gamma)) invGamma = 1.0 / gamma + table = np.array([((i / 255.0) ** invGamma) * 255 for i in np.arange(0, 256)]).astype("uint8") + + print('[FILTER] Gamma correction: gamma={:.1f}'.format(gamma)) return cv2.LUT(img, table) + + +def gaussian_lookup(img, sigma=51): + x = np.arange(0, 256) + pdf = stats.norm.pdf(x, 127, sigma) + + cdf = np.cumsum(pdf) + cdf_norm = np.array([(x - np.min(cdf))/(np.max(cdf) - np.min(cdf)) * 255 for x in cdf]).astype('uint8') + + print('[FILTER] Gaussian lookup filter: sigma={}'.format(sigma)) + return cv2.LUT(img, cdf_norm) + + +def thresholding(img, c1u=255, c1l=0, c2u=255, c2l=0, c3u=255, c3l=0): + mask = cv2.inRange(img, (c1l, c2l, c3l), (c1u, c2u, c3u)) + + print('[FILTER] Thresholding: Channel 1: [{}, {}], Channel 2: [{}, {}], Channel 3: [{}, {}]'.format(c1u, c1l, c2u, c2l, c3u, c3l)) + return mask -def modify_channels(img, r=1.0, g=1.0, b=1.0): - print('[FILTER] Modify channels: c1={:.1f}, c2={:.1f}, c3={:.1f}'.format(r, g, b)) - img_r = cv2.convertScaleAbs(img[:, :, 2], alpha=r, beta=0) - img_g = cv2.convertScaleAbs(img[:, :, 1], alpha=g, beta=0) - img_b = cv2.convertScaleAbs(img[:, :, 0], alpha=b, beta=0) - - if g == 0 and b == 0: - img_g, img_b = img_r, img_r - elif r == 0 and b == 0: - img_r, img_b = img_g, img_g - elif r == 0 and g == 0: - img_r, img_g = img_b, img_b - - return np.dstack([img_b, img_g, img_r]) - - -def grayscale(img): - print('[FILTER] Convert to grayscale') - return cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), cv2.COLOR_GRAY2BGR) - - -def negative(img): - print('[FILTER] Convert to image negative') - return ~img - - -def highpass(img, sigma=51): - print('[FILTER] Highpass filter: sigma={:.0f}'.format(sigma)) - if sigma % 2 == 1: - sigma += 1 - new = img - cv2.GaussianBlur(img, (0, 0), int(sigma)) + 127 - - return new - - -def laplacian(img): - print('[FILTER] Laplacian of an image') - new = cv2.Laplacian(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), cv2.CV_8U, ksize=3) - return cv2.cvtColor(new, cv2.COLOR_GRAY2BGR) - +def denoise(img, ksize=3): + print('[FILTER] Denoise: ksize={}'.format(ksize)) + return cv2.medianBlur(img, ksize) -def intensity_capping(img, n_std=2): - print('[FILTER] Pixel intensity capping: n_std='.format(n_std)) - img_g = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - median = np.median(img_g) - stdev = np.std(img_g) - cap = median + n_std * stdev - img_g[img_g > cap] = cap +def remove_background(img, num_frames_background=10): + num_frames_background = int(num_frames_background) + h, w = img.shape[:2] - return cv2.cvtColor(img_g, cv2.COLOR_GRAY2BGR) + if len(img_list) < num_frames_background: + num_frames_background = len(img_list) + step = len(img_list) // num_frames_background + img_back_path = r'{}/../median_{}.{}'.format(path.dirname(img_list[0]), num_frames_background, args.ext) -def remove_background(img, num_imgs=10): - print('[FILTER] Remove image background: num_imgs={:.0f}'.format(num_imgs)) - num_imgs = int(num_imgs) - new = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype('int') - h, w = new.shape + if path.exists(img_back_path): + back = cv2.imread(img_back_path) + else: + stack = np.ndarray([h, w, 3, num_frames_background], dtype='uint8') - if len(img_list) < num_imgs: - num_imgs = len(img_list) + for i in range(num_frames_background): + stack[:, :, :, i] = cv2.imread(img_list[i*step]) - back_path = r'{}/../median_{}.{}'.format(path.dirname(img_list[0]), num_imgs, args.ext) + back = np.median(stack, axis=3) + cv2.imwrite(img_back_path, back) - if path.exists(back_path): - back = cv2.imread(back_path, 0).astype('int') - else: - stack = np.ndarray([h, w, num_imgs], dtype='int') + print('[FILTER] Remove image background: num_frames_background={:.0f}'.format(num_frames_background)) + return cv2.subtract(back, img) - for i in range(num_imgs): - stack[:, :, i] = cv2.imread(img_list[i], 0) - back = np.median(stack, axis=2).astype('int') - cv2.imwrite(back_path, back.astype('uint8')) +def histeq(img): + img_gray = convert_img(img, colorspace, 'grayscale') + eq = cv2.equalizeHist(img_gray) + + print('[FILTER] Histogram equalization') + return convert_img(eq, 'grayscale', colorspace) - new -= back - new[new < 0] = 0 - new[new > 255] = 255 - return cv2.cvtColor(new.astype('uint8'), cv2.COLOR_GRAY2BGR) +def clahe(img, clip=2.0, tile=8): + clahe = cv2.createCLAHE(clipLimit=clip, tileGridSize=(int(tile), int(tile))) + img_gray = convert_img(img, colorspace, 'grayscale') + img_clahe = clahe.apply(img_gray) + + print('[FILTER] CLAHE: clip={:.1f}, tile={:.0f}'.format(clip, tile)) + return convert_img(img_clahe, 'grayscale', colorspace) def params_to_list(params): @@ -209,7 +241,7 @@ def keypress(event): if event.key == ' ': if is_original: - img_shown.set_data(img_rgb) + img_shown.set_data(img[:, :, 0] if is_grayscale(img) else img) else: img_shown.set_data(original) @@ -223,28 +255,34 @@ def keypress(event): def update_frame(val): global original global img - global img_rgb original = cv2.imread(img_list[sl_ax_frame_num.val]) - img, img_rgb = apply_filters(original, filters_data) + original = cv2.cvtColor(original, cv2.COLOR_BGR2RGB) + + img = apply_filters(original, filters_data) if is_original: - img_shown.set_data(img_rgb) + img_shown.set_data(original) else: - img_shown.set_data(img) + img_shown.set_data(img[:, :, 0] if is_grayscale(img) else img) plt.draw() return def apply_filters(img, filters_data): + global colorspace + for i in range(filters_data.shape[0]): img = func(globals()[filters_data[i][0]], img, params_to_list(filters_data[i][1])) - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + if filters_data[i][0].startswith('to_'): + colorspace = filters_data[i][0].split('_')[1] legend = 'Filters:' + for i in range(filters_data.shape[0]): - func_args_names = globals()[filters_data[i][0]] + func_args_names = globals()[filters_data[i][0]] func_args = inspect.getfullargspec(func_args_names)[0][1:] if filters_data[i][1] != '' else [] legend_values = ['{}={}'.format(p, v) for p, v in zip(func_args, filters_data[i][1].split(','))] legend += '\n ' + filters_data[i][0] + ': ' + ', '.join(legend_values if filters_data[i][1] != '' else '') @@ -257,80 +295,89 @@ def apply_filters(img, filters_data): fontsize=9, ) - return img, img_rgb + return img if __name__ == '__main__': - try: - parser = ArgumentParser() - parser.add_argument('--folder', type=str, help='Path to frames folder') - parser.add_argument('--ext', type=str, help='Frames\' extension', default='jpg') - parser.add_argument('--multi', type=int, help='Path to filter list file', default=0) - args = parser.parse_args() - - img_list = glob.glob(r'{}/*.{}'.format(args.folder, args.ext)) - num_frames = len(img_list) - filters_data = np.loadtxt(args.folder + '/filters.txt', dtype='str', delimiter=r'/', ndmin=2) - - fig, ax = plt.subplots() - fig.canvas.mpl_connect('key_press_event', keypress) - plt.subplots_adjust(bottom=0.13) - plt.axis('off') - - axcolor = 'lightgoldenrodyellow' - valfmt = "%d" - - ax_frame_num = plt.axes([0.2, 0.05, 0.63, 0.03], facecolor=axcolor) - sl_ax_frame_num = Slider(ax_frame_num, f'Frame #\n({num_frames} total)', 0, num_frames - 1, valinit=0, valstep=1, valfmt=valfmt) - sl_ax_frame_num.on_changed(update_frame) - - if args.multi == 0: - img_path = img_list[0] - img = cv2.imread(img_path) - original = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB) - is_original = False - - img, img_rgb = apply_filters(img, filters_data) + parser = ArgumentParser() + parser.add_argument('--folder', type=str, help='Path to frames folder') + parser.add_argument('--ext', type=str, help='Frames\' extension', default='jpg') + parser.add_argument('--multi', type=int, help='Path to filter list file', default=0) + args = parser.parse_args() + + img_list = glob.glob(r'{}/*.{}'.format(args.folder, args.ext)) + num_frames = len(img_list) + filters_data = np.loadtxt(args.folder + '/filters.txt', dtype='str', delimiter=r'/', ndmin=2) + + fig, ax = plt.subplots() + fig.canvas.mpl_connect('key_press_event', keypress) + plt.subplots_adjust(bottom=0.13) + plt.axis('off') + + axcolor = 'lightgoldenrodyellow' + valfmt = "%d" + + ax_frame_num = plt.axes([0.2, 0.05, 0.63, 0.03], facecolor=axcolor) + sl_ax_frame_num = Slider(ax_frame_num, f'Frame #\n({num_frames} total)', 0, num_frames - 1, valinit=0, valstep=1, valfmt=valfmt) + sl_ax_frame_num.on_changed(update_frame) + + if args.multi == 0: + img_path = img_list[0] + img = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB) + colorspace = 'rgb' + + original = img.copy() + is_original = False + + img = apply_filters(img, filters_data) + + try: + mng = plt.get_current_fig_manager() + mng.window.state('zoomed') + mng.set_window_title('Filtering') + except: + pass + + ax.set_title('Use SPACE to toggle between original and filtered image, and Q or ESC to exit') + ax.axis('off') + + if is_grayscale(img): + img_shown = ax.imshow(img[:, :, 0], cmap=colormap) + else: + img_shown = ax.imshow(img) - try: - mng = plt.get_current_fig_manager() - mng.window.state('zoomed') - mng.set_window_title('Filtering') - except: - pass + plt.show() + exit() - ax.set_title('Use SPACE to toggle between original and filtered image, and Q or ESC to exit') - ax.axis('off') - img_shown = ax.imshow(img_rgb) - plt.show() - exit() + else: + filtered_folder = args.folder + '_filtered' - else: - filtered_folder = args.folder + '_filtered' + print('[BEGIN] :STARTING FILTERING: '.ljust(len(separator), '-')) + print(' [INFO] Filtering frames from folder', args.folder + '/') + print(' [INFO] Filters to apply:', [row[0] for row in filters_data]) - print('[BEGIN] :STARTING FILTERING: '.ljust(len(separator), '-')) - print(' [INFO] Filtering frames from folder', args.folder + '/') - print(' [INFO] Filters to apply:', [row[0] for row in filters_data]) + if not path.exists(filtered_folder): + makedirs(filtered_folder) - if not path.exists(filtered_folder): - makedirs(filtered_folder) + for j in range(len(img_list)): + img_path = img_list[j] + img = cv2.imread(img_path) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + colorspace = 'rgb' - for j in range(len(img_list)): - img_path = img_list[j] - img = cv2.imread(img_path) + for i in range(filters_data.shape[0]): + img = func(locals()[filters_data[i][0]], img, params_to_list(filters_data[i][1])) - for i in range(filters_data.shape[0]): - img = func(locals()[filters_data[i][0]], img, params_to_list(filters_data[i][1])) + if filters_data[i][0].startswith('to_'): + colorspace = filters_data[i][0].split('_')[1] - cv2.imwrite('{}/{}'.format(filtered_folder, path.basename(img_path)), img) + img_rgb = convert_img(img, colorspace, 'rgb') + img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR) - print(' [INFO] Filtering frame {}/{} ({:.1f}%)'.format(j, num_frames - 1, j/(num_frames - 1) * 100)) + cv2.imwrite('{}/{}'.format(filtered_folder, path.basename(img_path)), img_bgr) - print(' [END] Filtering complete!') - print(' [END] Results available in folder [{}]!'.format(filtered_folder)) - input('\nPress ENTER/RETURN to exit...') + print(' [INFO] Filtering frame {}/{} ({:.1f}%)'.format(j, num_frames - 1, j/(num_frames - 1) * 100)) - except Exception as ex: - print('\n[EXCEPTION] The following exception has occurred: \n\n' - ' {}'.format(ex)) + print(' [END] Filtering complete!') + print(' [END] Results available in folder [{}]!'.format(filtered_folder)) input('\nPress ENTER/RETURN to exit...') diff --git a/scripts/filters.xml b/scripts/filters.xml index 8e04d2d..43b307d 100644 --- a/scripts/filters.xml +++ b/scripts/filters.xml @@ -1,13 +1,5 @@ - - Grayscale - grayscale - Converts a color image to 8bit grayscale [0..255]. - - - - Negative negative @@ -17,76 +9,50 @@ - Brightness/Contrast - brightness_contrast - Adjusts overall brightness and contrast using linear transformation [Alpha*Y + Beta]. + Convert to Grayscale + to_grayscale + Converts a color image to a single-channel 8bit grayscale [0..255]. - - - Alpha - float - 5.0 - 1.0 - 0.0 - 0.1 - - - Beta - int - 255 - 0 - -255 - 1 - - + - Adjust gamma - gamma - Adjusts gamma exposure of the image using linear transformation [Gamma*Y]. + Convert to RGB + to_rgb + Converts to three-channel 8bit RGB (red-green-blue) colorspace. - - - Gamma - float - 3.0 - 1.0 - 0.0 - 0.1 - - + - Hist. equalization - histeq - Stretches the histogram of the image to improve dynamic range and accentuate details. + Convert to HSV + to_hsv + Converts to three-channel 8bit HSV (hue-saturation-value) colorspace. - CLAHE - clahe - Adaptive version of the histogram equalization with histogram clipping. + Convert to L*a*b* + to_lab + Converts to three-channel 8bit L*a*b* (CIELab) colorspace. + + + + + + Single image channel + select_channel + Select a single channel from a three-channel image. Channel order depends on the image colorspace (RGB, HSV, L*a*b). - Clip limit - float - 10.0 - 2.0 - 0.1 - 0.1 - - - Tile size + Channel number int - 64 - 8 - 4 - 4 + 3 + 1 + 1 + 1 @@ -111,12 +77,54 @@ Intensity capping intensity_capping - Limits local brightness using neighboring mean and variance. + Limits pixel values using global mean and variance. If tracer particles are darker than the water surface, apply negative filter before this one. Num. standard deviations (n) float + 5.0 + 0.0 + -5.0 + 0.1 + + + + + + Brightness/Contrast adj. + brightness_contrast + Adjusts overall brightness and contrast using linear transformation [Alpha*Y + Beta]. + + + + Alpha + float + 5.0 + 1.0 + 0.0 + 0.1 + + + Beta + int + 255 + 0 + -255 + 1 + + + + + + Gamma adjustment + gamma + Adjusts gamma exposure of the image using linear transformation [Gamma*Y]. + + + + Gamma + float 3.0 1.0 0.0 @@ -126,37 +134,46 @@ - Laplacian - laplacian - Calculates a gradient map of a grayscale image using Laplacian, can be useful for detecting tracer particles. + Gaussian CDF lookup + gaussian_lookup + Adjusts exposure using Gaussian cumulative distribution function as a lookup table. - + + + Sigma + int + 250 + 50 + 1 + 1 + + - HSV filter - hsv_filter - Filter image using hue, saturation and value (lightness). If slider values are left at default (upper at max, lower at min), the image is simply transformed to HSV colorspace. + Channel thresholding filter + thresholding + Filter image by thresholding individual image channels (returns a binarized [0, 1] image). - Hue lower + Channel 1 low int - 180 + 255 0 0 1 - Hue upper + Channel 1 high int - 180 - 180 + 255 + 255 0 1 - Saturation lower + Channel 2 low int 255 0 @@ -164,7 +181,7 @@ 1 - Saturation upper + Channel 2 high int 255 255 @@ -172,7 +189,7 @@ 1 - Lightness/value lower + Channel 3 low int 255 0 @@ -180,7 +197,7 @@ 1 - Lightness/value upper + Channel 3 high int 255 255 @@ -193,74 +210,17 @@ Denoise denoise - Removes high frequency content, useful for removing camera noise but is VERY SLOW. + Removes salt-and-pepper type noise with a median filter. - Strength - int - 15 - 3 - 1 - 1 - - - Strength color + Kernel size int - 15 + 31 3 - 1 - 1 - - - Template size - int - 21 - 7 3 2 - - Search area - int - 63 - 21 - 9 - 2 - - - - - - Adjust image channels - modify_channels - Adjust image channels intensities to accentuate different types of details. - - - - Channel #1 - float - 3.0 - 1.0 - 0.0 - 0.1 - - - Channel #2 - float - 3.0 - 1.0 - 0.0 - 0.1 - - - Channel #1 - float - 3.0 - 1.0 - 0.0 - 0.1 - @@ -280,4 +240,37 @@ + + + Hist. equalization + histeq + Stretches the histogram of the image to improve dynamic range and accentuate details. + + + + + + CLAHE + clahe + Adaptive version of the histogram equalization with histogram clipping. + + + + Clip limit + float + 10.0 + 2.0 + 0.1 + 0.1 + + + Tile size + int + 64 + 8 + 4 + 4 + + + diff --git a/scripts/inspect_frames.py b/scripts/inspect_frames.py index 02a2441..51ee7bc 100644 --- a/scripts/inspect_frames.py +++ b/scripts/inspect_frames.py @@ -52,25 +52,25 @@ def keypress(event): if event.key == 'escape': exit() - elif event.key == 'left': + elif event.key == 'down': if sl_ax_frame_num.val == 0: sl_ax_frame_num.set_val(num_frames - 1) else: sl_ax_frame_num.set_val(sl_ax_frame_num.val - 1) - elif event.key == 'right': + elif event.key == 'up': if sl_ax_frame_num.val == num_frames - 1: sl_ax_frame_num.set_val(0) else: sl_ax_frame_num.set_val(sl_ax_frame_num.val + 1) - elif event.key == 'up': + elif event.key == 'pageup': if sl_ax_frame_num.val >= num_frames - 10: sl_ax_frame_num.set_val(0) else: sl_ax_frame_num.set_val(sl_ax_frame_num.val + 10) - elif event.key == 'down': + elif event.key == 'pagedown': if sl_ax_frame_num.val <= 9: sl_ax_frame_num.set_val(num_frames - 1) else: @@ -118,8 +118,8 @@ def keypress(event): sl_ax_frame_num.on_changed(update_frame) legend = 'Use slider to select frame,\n' \ - 'use LEFT and RIGHT keys to move by 1 frame\n' \ - 'or UP and DOWN keys to move by 10 frames\n' \ + 'use UP and DOWN keys to move by +/- 1 frame\n' \ + 'or PageUP and PageDOWN keys to move by +/- 10 frames\n' \ 'Press ESC or Q to exit' legend_toggle = plt.text(0.02, 0.97, legend,