Source code for IQM_Vis.UI.widgets
'''
UI create widgets
'''
# Author: Matt Clifford <matt.clifford@bristol.ac.uk>
# License: BSD 3-Clause License
import re
import os
from functools import partial
import datetime
import numpy as np
from PyQt6.QtWidgets import QPushButton, QLabel, QSlider, QCheckBox, QComboBox, QLineEdit
from PyQt6.QtGui import QIntValidator, QDoubleValidator, QValidator
from PyQt6.QtCore import Qt, pyqtSlot, QLocale
import IQM_Vis
from IQM_Vis.UI.custom_widgets import ClickLabel
from IQM_Vis.utils import gui_utils, plot_utils, image_utils, save_utils
# sub class used by IQM_Vis.main.make_app to initialise widgets and general UI functions for widgets
[docs]class widgets():
[docs] def init_widgets(self, **kwargs):
'''
create all the widgets we need and init params
'''
self._init_widgets()
self.set_image_name_text()
self._init_image_settings()
def _init_widgets(self):
'''
create all the widgets we need and init params
'''
self.export_in_progress = False
self.last_export = None
self.tool_tip_style = """QToolTip {background-color: black; color: white; border: black solid 1px}"""
# first setup the slider data
self.sliders = {'transforms': {}, 'metric_params': {}}
self._init_sliders(self.sliders['transforms'], self.checked_transformations, param_group='transforms')
self._init_sliders(self.sliders['metric_params'], self.metric_params, param_group='metric_params')
self.widget_row = {}
for i, data_store in enumerate(self.data_stores): # create each row of the widgets
self.widget_row[i] = {'images':{}, 'metrics':{}, 'metric_images':{}}
'''image and transformed image'''
for image_name in ['original', 'transformed']:
self.widget_row[i]['images'][image_name] = {}
# image widget
self.widget_row[i]['images'][image_name]['data'] = QLabel(self)
self.widget_row[i]['images'][image_name]['data'].setAlignment(Qt.AlignmentFlag.AlignCenter)
# image label
self.widget_row[i]['images'][image_name]['label'] = QLabel(self)
self.widget_row[i]['images'][image_name]['label'].setAlignment(Qt.AlignmentFlag.AlignCenter)
self.widget_row[i]['images'][image_name]['label'].setText(image_name)
'''metric images'''
for metric_image in data_store.metric_images:
if metric_image in self.checked_metric_images:
self.widget_row[i]['metric_images'][metric_image] = {}
self.widget_row[i]['metric_images'][metric_image]['data'] = QLabel(self)
self.widget_row[i]['metric_images'][metric_image]['data'].setAlignment(Qt.AlignmentFlag.AlignCenter)
# image label
self.widget_row[i]['metric_images'][metric_image]['label'] = QLabel(self)
self.widget_row[i]['metric_images'][metric_image]['label'].setAlignment(Qt.AlignmentFlag.AlignCenter)
'''metrics graphs'''
im_pair_name = gui_utils.get_image_pair_name(data_store)
self.widget_row[i]['metrics']['info'] = {}
self.widget_row[i]['metrics']['info']['label'] = QLabel(self)
self.widget_row[i]['metrics']['info']['label'].setAlignment(Qt.AlignmentFlag.AlignCenter)
self.widget_row[i]['metrics']['info']['label'].setText('Metrics '+im_pair_name)
if self.metrics_info_format == 'graph':
self.widget_row[i]['metrics']['info']['data'] = gui_utils.MplCanvas(size=(self.graph_size/10, self.graph_size/10))
else:
self.widget_row[i]['metrics']['info']['data'] = QLabel(self)
self.widget_row[i]['metrics']['info']['data'].setAlignment(Qt.AlignmentFlag.AlignCenter)
self.widget_row[i]['metrics']['info']['data'].setText('')
# metrics average graphs
if self.metrics_avg_graph:
self.widget_row[i]['metrics']['avg'] = {}
self.widget_row[i]['metrics']['avg']['label'] = QLabel(self)
self.widget_row[i]['metrics']['avg']['label'].setAlignment(Qt.AlignmentFlag.AlignCenter)
self.widget_row[i]['metrics']['avg']['label'].setText('IQM Averages')
self.widget_row[i]['metrics']['avg']['data'] = gui_utils.MplCanvas(size=(self.graph_size/10, self.graph_size/10), polar=True)
self.widget_row[i]['metrics']['avg']['data'].setToolTip('Mean metric value over the range of each transform.')
self.widget_row[i]['metrics']['avg']['data'].setStyleSheet(self.tool_tip_style)
self.widget_row[i]['metrics']['avg']['save_button'] = QPushButton('Adjust or Save', self)
self.widget_row[i]['metrics']['avg']['save_button'].clicked.connect(partial(self.plot_radar_mlp, i))
if self.metric_range_graph:
self.widget_row[i]['metrics']['range'] = {}
self.widget_row[i]['metrics']['range']['label'] = QLabel(self)
self.widget_row[i]['metrics']['range']['label'].setAlignment(Qt.AlignmentFlag.AlignCenter)
self.widget_row[i]['metrics']['range']['label'].setText('Response Profiles')
self.widget_row[i]['metrics']['range']['data'] = gui_utils.MplCanvas(size=(self.graph_size/10, self.graph_size/10))
self.widget_row[i]['metrics']['range']['data'].setToolTip('Single transformation value range for all metrics.')
self.widget_row[i]['metrics']['range']['data'].setStyleSheet(self.tool_tip_style)
self.widget_row[i]['metrics']['range']['save_button'] = QPushButton('Adjust or Save', self)
self.widget_row[i]['metrics']['range']['save_button'].clicked.connect(partial(self.plot_metric_range_mlp, i))
# correlation graphs
self.widget_row[i]['metrics']['correlation'] = {}
self.widget_row[i]['metrics']['correlation']['label'] = QLabel(self)
self.widget_row[i]['metrics']['correlation']['label'].setAlignment(Qt.AlignmentFlag.AlignCenter)
self.widget_row[i]['metrics']['correlation']['label'].setText('Human Correlation')
self.widget_row[i]['metrics']['correlation']['data'] = gui_utils.MplCanvas(size=(self.graph_size/10, self.graph_size/10))
self.widget_row[i]['metrics']['correlation']['data'].setToolTip('Human scores versus IQMs.\nMean shown with points\nStandard deviation shown with bars.\nClick points to show image.')
self.widget_row[i]['metrics']['correlation']['data'].setStyleSheet(self.tool_tip_style)
# JND graphs
self.widget_row[i]['metrics']['JND'] = {}
self.widget_row[i]['metrics']['JND']['label'] = QLabel(self)
self.widget_row[i]['metrics']['JND']['label'].setAlignment(Qt.AlignmentFlag.AlignCenter)
self.widget_row[i]['metrics']['JND']['label'].setText('Just Noticeable Difference')
self.widget_row[i]['metrics']['JND']['data'] = gui_utils.MplCanvas(size=(self.graph_size/10, self.graph_size/10))
self.widget_row[i]['metrics']['JND']['data'].setToolTip(
'Just Noticeable Difference shows the perceivable effect of a distortion for a human subject.')
self.widget_row[i]['metrics']['JND']['data'].setStyleSheet(self.tool_tip_style)
'''buttons'''
self.widget_controls = {'button': {}, 'slider': {}, 'label': {}, 'check_box': {}, 'images': {}}
if (self.metrics_avg_graph or self.metric_range_graph):
# Update graphs
self.widget_controls['button']['force_update'] = QPushButton('Update Graphs', self)
self.widget_controls['button']['force_update'].setToolTip('Update graphs using all the current slider values.')
self.widget_controls['button']['force_update'].setStyleSheet(self.tool_tip_style)
self.widget_controls['button']['force_update'].clicked.connect(self.display_images)
self.widget_controls['button']['force_update'].clicked.connect(partial(self.redo_plots, True))
# Change plot limits
self.widget_controls['check_box']['graph_limits'] = QCheckBox('Squeeze plots')
self.widget_controls['check_box']['graph_limits'].setToolTip('Set the scale of the plots to the metric data range.')
self.widget_controls['check_box']['graph_limits'].setStyleSheet(self.tool_tip_style)
self.widget_controls['check_box']['graph_limits'].setCheckState(Qt.CheckState.Unchecked)
self.widget_controls['check_box']['graph_limits'].stateChanged.connect(self.change_plot_lims)
if self.metric_range_graph:
# buttons to control which metric range graph to show
self.widget_controls['button']['next_metric_graph'] = QPushButton('->', self)
self.widget_controls['button']['next_metric_graph'].clicked.connect(partial(self.change_metric_range_graph, 1))
self.widget_controls['button']['prev_metric_graph'] = QPushButton('<-', self)
self.widget_controls['button']['prev_metric_graph'].clicked.connect(partial(self.change_metric_range_graph, -1))
# buttons to control which correlation graph to show
self.widget_controls['button']['next_correlation_graph'] = QPushButton('->', self)
self.widget_controls['button']['next_correlation_graph'].clicked.connect(partial(self.change_metric_correlations_graph, 1))
self.widget_controls['button']['prev_correlation_graph'] = QPushButton('<-', self)
self.widget_controls['button']['prev_correlation_graph'].clicked.connect(partial(self.change_metric_correlations_graph, -1))
if self.dataset:
# control what image is used from the dataset
self.widget_controls['button']['next_data'] = QPushButton('->', self)
self.widget_controls['button']['next_data'].clicked.connect(partial(self.change_preview_images, 1))
self.widget_controls['button']['prev_data'] = QPushButton('<-', self)
self.widget_controls['button']['prev_data'].clicked.connect(partial(self.change_preview_images, -1))
self.widget_controls['label']['data'] = QLabel(self)
self.widget_controls['label']['data'].setText('Change Image:')
self.widget_controls['label']['data_num'] = QLabel(self)
self.widget_controls['label']['data_num'].setText('1/1')
self.widget_im_num_hash = {}
for im in range(self.num_images_scroll_show):
self.widget_controls['images'][im] = QLabel( self)
self.widget_controls['images'][im].setAlignment(Qt.AlignmentFlag.AlignCenter)
self.widget_controls['images'][im].mousePressEvent = partial(self.change_data_click_im, im)
self.widget_im_num_hash[im] = im # store the ind number of each im widget preview
# launch experiment buttons
self.widget_controls['button']['launch_exp_2AFC'] = QPushButton('Run 2AFC Experiment', self)
self.widget_controls['button']['launch_exp_2AFC'].setToolTip('Two Alternative Forced Choice Experiment')
self.widget_controls['button']['launch_exp_2AFC'].clicked.connect(self.launch_experiment_2AFC)
self.widget_controls['button']['launch_exp_JND'] = QPushButton('Run JND Experiment', self)
self.widget_controls['button']['launch_exp_JND'].setToolTip('Just Noticeable Difference Experiment')
self.widget_controls['button']['launch_exp_JND'].clicked.connect(self.launch_experiment_JND)
# load 2AFC experiment button
self.widget_controls['button']['load_exp'] = QPushButton('Load Experiment', self)
self.widget_controls['button']['load_exp'].clicked.connect(self.load_experiment_from_dir)
# load JNDexperiment button
self.widget_controls['button']['load_exp_JND'] = QPushButton('Load Experiment', self)
self.widget_controls['button']['load_exp_JND'].clicked.connect(self.load_experiment_from_dir_JND)
# export images button
self.widget_controls['button']['export_images'] = QPushButton('Export Images', self)
self.widget_controls['button']['export_images'].clicked.connect(self.export_trans_images)
'''sliders'''
self.params_from_sliders = {}
for param_group, slider_group in self.sliders.items():
self.params_from_sliders[param_group] = {}
self.widget_controls['button'][param_group] = {}
self.widget_controls['button'][param_group]['reset_sliders'] = QPushButton('Reset', self)
if param_group == 'metric_params':
redo_graphs = True
else:
redo_graphs = False
self.widget_controls['button'][param_group]['reset_sliders'].clicked.connect(
partial(self.reset_slider_group, param_group, redo_graphs))
for key, item_sliders in slider_group.items():
self.widget_controls['slider'][key] = {}
self.widget_controls['slider'][key]['data'] = QSlider(Qt.Orientation.Horizontal)
self.widget_controls['slider'][key]['data'].setMinimum(0)
self.widget_controls['slider'][key]['data'].setMaximum(len(item_sliders['values'])-1)
for func in item_sliders['value_change']:
self.widget_controls['slider'][key]['data'].valueChanged.connect(func)
for func in item_sliders['release']:
self.widget_controls['slider'][key]['data'].sliderReleased.connect(func)
self.params_from_sliders['transforms'][key] = item_sliders['init_ind']
self.widget_controls['slider'][key]['label'] = QLabel(self)
self.widget_controls['slider'][key]['label'].setAlignment(Qt.AlignmentFlag.AlignRight)
self.widget_controls['slider'][key]['label'].setText(f"{key}:")
self.widget_controls['slider'][key]['value'] = QLabel(self)
self.widget_controls['slider'][key]['value'].setAlignment(Qt.AlignmentFlag.AlignLeft)
self.widget_controls['slider'][key]['value'].setText(str(self.params_from_sliders['transforms'][key]))
''' experiment options '''
self.widget_experiment_params = {}
for trans_name, deets in self.checked_transformations.items():
self.widget_experiment_params[trans_name] = {}
self.widget_experiment_params[trans_name]['check_box'] = QCheckBox(self)
self.widget_experiment_params[trans_name]['check_box'].setChecked(True)
self.widget_experiment_params[trans_name]['check_box'].stateChanged.connect(partial(self.change_text_exp_trans,
trans=trans_name))
self.widget_experiment_params[trans_name]['name'] = QLabel(self)
self.widget_experiment_params[trans_name]['name'].setText(trans_name)
self.widget_experiment_params[trans_name]['min'] = QLabel(self)
self.widget_experiment_params[trans_name]['min'].setText('min:')
self.widget_experiment_params[trans_name]['min_edit'] = QLineEdit()
self.widget_experiment_params[trans_name]['min_edit'].setValidator(get_float_validator())
self.widget_experiment_params[trans_name]['min_edit'].textChanged.connect(partial(self.colour_lineedit, self.widget_experiment_params[trans_name]['min_edit']))
self.widget_experiment_params[trans_name]['min_edit'].setText(f"{deets['min']}")
self.widget_experiment_params[trans_name]['max'] = QLabel(self)
self.widget_experiment_params[trans_name]['max'].setText('max:')
self.widget_experiment_params[trans_name]['max_edit'] = QLineEdit()
self.widget_experiment_params[trans_name]['max_edit'].setValidator(get_float_validator())
self.widget_experiment_params[trans_name]['max_edit'].textChanged.connect(partial(self.colour_lineedit, self.widget_experiment_params[trans_name]['max_edit']))
self.widget_experiment_params[trans_name]['max_edit'].setText(f"{deets['max']}")
self.widget_experiment_params[trans_name]['steps'] = QLabel(self)
self.widget_experiment_params[trans_name]['steps'].setText('steps:')
self.widget_experiment_params[trans_name]['steps_edit'] = QLineEdit()
self.widget_experiment_params[trans_name]['steps_edit'].setValidator(QIntValidator())
self.widget_experiment_params[trans_name]['steps_edit'].setMaxLength(2)
self.widget_experiment_params[trans_name]['steps_edit'].setText(f"{self.num_step_experiment}")
# change save folder button
self.widget_controls['label']['exp_change_save'] = QLabel(self)
self.widget_controls['label']['exp_change_save'].setText(f'Root Save Folder: {self.default_save_dir}')
self.widget_controls['button']['exp_change_save'] = QPushButton('Change', self)
self.widget_controls['button']['exp_change_save'].clicked.connect(self.change_save_folder)
# dataset name
self.widget_controls['label']['exp_change_dataset_name'] = QLabel(self)
self.widget_controls['label']['exp_change_dataset_name'].setText(f'Dataset Name (JND)')
self.widget_controls['button']['exp_change_dataset_name'] = QLineEdit(self)
self.widget_controls['button']['exp_change_dataset_name'].setText(
self.default_dataset_name)
self.widget_controls['button']['exp_change_dataset_name'].textChanged.connect(
self.change_dataset_name)
self.widget_controls['label']['exp_im_nums'] = QLabel(self)
self.widget_controls['label']['exp_im_nums'].setText('Dataset Range (JND):')
self.widget_controls['label']['exp_im_nums_dash'] = QLabel(self)
self.widget_controls['label']['exp_im_nums_dash'].setText('-')
self.widget_controls['label']['range_edit_lower'] = QLineEdit()
self.widget_controls['label']['range_edit_lower'].setValidator(QIntValidator())
self.exp_range_lower = 1
self.widget_controls['label']['range_edit_lower'].setText(
f"{self.exp_range_lower}")
self.widget_controls['label']['range_edit_lower'].textChanged.connect(self.JND_dataset_range_lower)
self.widget_controls['label']['range_edit_upper'] = QLineEdit()
self.widget_controls['label']['range_edit_upper'].setValidator(QIntValidator())
self.exp_range_upper = len(self.data_stores[0])
self.widget_controls['label']['range_edit_upper'].setText(
f"{self.exp_range_upper}")
self.widget_controls['label']['range_edit_upper'].textChanged.connect(self.JND_dataset_range_upper)
''' export options '''
self.widget_export = {}
for trans_name, deets in self.checked_transformations.items():
self.widget_export[trans_name] = {}
self.widget_export[trans_name]['check_box'] = QCheckBox(self)
self.widget_export[trans_name]['check_box'].setChecked(True)
self.widget_export[trans_name]['check_box'].stateChanged.connect(partial(self.change_text_export_trans,
trans=trans_name))
self.widget_export[trans_name]['name'] = QLabel(self)
self.widget_export[trans_name]['name'].setText(trans_name)
self.widget_export[trans_name]['min'] = QLabel(self)
self.widget_export[trans_name]['min'].setText('min:')
self.widget_export[trans_name]['min_edit'] = QLineEdit()
self.widget_export[trans_name]['min_edit'].setValidator(get_float_validator())
self.widget_export[trans_name]['min_edit'].textChanged.connect(partial(self.colour_lineedit, self.widget_export[trans_name]['min_edit']))
self.widget_export[trans_name]['min_edit'].setText(f"{deets['min']}")
self.widget_export[trans_name]['max'] = QLabel(self)
self.widget_export[trans_name]['max'].setText('max:')
self.widget_export[trans_name]['max_edit'] = QLineEdit()
self.widget_export[trans_name]['max_edit'].setValidator(get_float_validator())
self.widget_export[trans_name]['max_edit'].textChanged.connect(partial(self.colour_lineedit, self.widget_export[trans_name]['max_edit']))
self.widget_export[trans_name]['max_edit'].setText(f"{deets['max']}")
self.widget_export[trans_name]['steps'] = QLabel(self)
self.widget_export[trans_name]['steps'].setText('steps:')
self.widget_export[trans_name]['steps_edit'] = QLineEdit()
self.widget_export[trans_name]['steps_edit'].setValidator(QIntValidator())
self.widget_export[trans_name]['steps_edit'].setMaxLength(2)
self.widget_export[trans_name]['steps_edit'].setText(f"{self.num_steps_range}")
# change save folder button
self.widget_controls['label']['export_change_save'] = QLabel(self)
self.widget_controls['label']['export_change_save'].setText(f'Save Folder: {self.default_save_dir}')
self.widget_controls['button']['export_change_save'] = QPushButton('Change', self)
self.widget_controls['button']['export_change_save'].clicked.connect(self.change_save_folder)
'''
setup/helper functions
'''
def _init_sliders(self, sliders_dict, info_dict, param_group):
'''
generic initialiser of sliders (will change the dicts input rather than returning - like C++ pointers &)
sliders_dict: holds the slider widgets
info_dict: info to initialise the sliders
'''
for key, info_item in info_dict.items():
sliders_dict[key] = {}
sliders_dict[key]['release'] = [self.display_images]
sliders_dict[key]['value_change'] = [partial(self.generic_value_change, key, param_group), self.display_images]
if 'function' in info_item.keys():
sliders_dict[key]['function'] = info_item['function']
if 'init_value' not in info_item.keys():
info_item['init_value'] = 0
sliders_dict[key]['init_value_store'] = info_item['init_value']
if 'values' in info_item.keys():
sliders_dict[key]['values'] = info_item['values']
else:
if 'num_values' not in info_item.keys():
info_item['num_values'] = 41 # make default value for steps in slider range
# see if we need to make odd numbers (for use with kernel sizes)
if 'normalise' in info_item.keys():
# store for if min/max changed later
sliders_dict[key]['normalise'] = info_item['normalise']
self.make_slider_range(sliders_dict, key, info_item['min'], info_item['max'], info_item['num_values'])
# store num steps used
sliders_dict[key]['default_num_steps'] = len(sliders_dict[key]['values'])
# make min/max inputs
width = 40
sliders_dict[key]['min_edit'] = QLineEdit()
sliders_dict[key]['min_edit'].setValidator(get_float_validator())
sliders_dict[key]['min_edit'].textChanged.connect(partial(self.colour_lineedit, sliders_dict[key]['min_edit']))
sliders_dict[key]['min_edit'].setText(f"{min(sliders_dict[key]['values'])}")
sliders_dict[key]['min_edit'].setFixedWidth(width)
sliders_dict[key]['min_edit'].editingFinished.connect(partial(self.edit_slider_vals, sliders_dict, key, info_item))
sliders_dict[key]['min_edit'].editingFinished.connect(partial(self.generic_value_change, key, param_group))
sliders_dict[key]['min_edit'].editingFinished.connect(self.display_images)
sliders_dict[key]['max_edit'] = QLineEdit()
sliders_dict[key]['max_edit'].setValidator(get_float_validator())
sliders_dict[key]['max_edit'].textChanged.connect(partial(self.colour_lineedit, sliders_dict[key]['max_edit']))
sliders_dict[key]['max_edit'].setText(f"{max(sliders_dict[key]['values'])}")
sliders_dict[key]['max_edit'].setFixedWidth(width)
sliders_dict[key]['max_edit'].editingFinished.connect(partial(self.edit_slider_vals, sliders_dict, key, info_item))
sliders_dict[key]['max_edit'].editingFinished.connect(partial(self.generic_value_change, key, param_group))
sliders_dict[key]['max_edit'].editingFinished.connect(self.display_images)
[docs] def make_slider_range(self, sliders_dict, key, _min, _max, num_steps=None):
if num_steps == None:
num_steps = sliders_dict[key]['default_num_steps']
sliders_dict[key]['values'] = np.linspace(_min, _max, num_steps)
# see if we need to make odd numbers (for use with kernel sizes)
if 'normalise' in sliders_dict[key].keys():
if sliders_dict[key]['normalise'] == 'odd':
sliders_dict[key]['values'] = np.round(sliders_dict[key]['values'])
sliders_dict[key]['values'] = sliders_dict[key]['values'][sliders_dict[key]['values']%2 == 1]
# get ind of the initial value to set the slider at
sliders_dict[key]['init_ind'] = np.searchsorted(sliders_dict[key]['values'], sliders_dict[key]['init_value_store'], side='left')
def _init_image_settings(self):
self.widget_settings = {}
# pre processing
self.pre_processing_options = {'None': None,
'Resize 32': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=32),
'Resize 64': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=64),
'Resize 128': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=128),
'Resize 256': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=256),
'Resize 512': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=512),
'Resize 1280': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=1280),
'Resize 1920': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=1920),
'Resize 2560': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=2560),
'Resize 3840': partial(IQM_Vis.utils.image_utils.resize_to_longest_side, side=3840),
}
if not hasattr(self, 'pre_processing_option'):
self.pre_processing_option = 'Resize 256' # init_val
for i, data_store in enumerate(self.data_stores):
if hasattr(data_store, 'image_pre_processing'):
if str(data_store.image_pre_processing) not in [str(f) for f in self.pre_processing_options.values()]:
name = f"Custom {i}"
self.pre_processing_options[name] = data_store.image_pre_processing
self.pre_processing_option = name
combobox_pre = QComboBox()
combobox_pre.addItems(list(self.pre_processing_options.keys()))
combobox_pre.setCurrentText(self.pre_processing_option)
combobox_pre.activated.connect(self.enable_settings_button)
self.widget_settings['image_pre_processing'] = {'widget': combobox_pre, 'label': QLabel('Image Pre Processing:')}
self.change_pre_processing() # apply default
# post processing
self.post_processing_options = {'None': None,
'Crop Centre': partial(IQM_Vis.utils.image_utils.crop_centre, scale_factor=2, keep_size=True),
'Crop Centre (No Resize)': partial(IQM_Vis.utils.image_utils.crop_centre, scale_factor=2, keep_size=False),
}
if not hasattr(self, 'post_processing_option'):
self.post_processing_option = 'None' # init_val
for i, data_store in enumerate(self.data_stores):
if hasattr(data_store, 'image_post_processing'):
if data_store.image_post_processing not in list(self.post_processing_options.values()):
name = f"Custom {i}"
self.post_processing_options[name] = data_store.image_post_processing
self.post_processing_option = name
combobox_post = QComboBox()
combobox_post.addItems(list(self.post_processing_options.keys()))
combobox_post.setCurrentText(self.post_processing_option)
combobox_post.activated.connect(self.enable_settings_button)
self.widget_settings['image_post_processing'] = {'widget': combobox_post, 'label': QLabel('Image Post Processing:')}
self.change_post_processing() # apply default
# image display size
line_edit_display = QLineEdit()
line_edit_display.setValidator(QIntValidator())
line_edit_display.setMaxLength(3)
line_edit_display.setText(str(self.image_display_size))
line_edit_display.textChanged.connect(self.enable_settings_button)
self.widget_settings['image_display_size'] = {'widget': line_edit_display, 'label': QLabel('Image Display Size:')}
# graph display size
line_edit_graph = QLineEdit()
line_edit_graph.setValidator(QIntValidator())
line_edit_graph.setMaxLength(2)
line_edit_graph.setText(str(self.graph_size))
line_edit_graph.textChanged.connect(self.enable_settings_button)
self.widget_settings['graph_display_size'] = {'widget': line_edit_graph, 'label': QLabel('Graph Display Size:')}
# graph/experiment number of steps in the range
line_edit_num_steps = QLineEdit()
line_edit_num_steps.setValidator(QIntValidator())
line_edit_num_steps.setMaxLength(4)
line_edit_num_steps.setText(str(self.num_steps_range))
line_edit_num_steps.textChanged.connect(self.enable_settings_button)
self.widget_settings['graph_num_steps'] = {'widget': line_edit_num_steps, 'label': QLabel('Graph Range Step Size:')}
# image screen calibration
line_edit_rgb = QLineEdit()
line_edit_rgb.setValidator(QIntValidator())
line_edit_rgb.setMaxLength(4)
line_edit_rgb.setText(str(self.rgb_brightness))
line_edit_rgb.textChanged.connect(self.enable_settings_button)
self.widget_settings['image_display_rgb_brightness'] = {
'widget': line_edit_rgb, 'label': QLabel('RGB Max Brightness:')}
line_edit_display = QLineEdit()
line_edit_display.setValidator(QIntValidator())
line_edit_display.setMaxLength(4)
line_edit_display.setText(str(self.display_brightness))
line_edit_display.textChanged.connect(self.enable_settings_button)
self.widget_settings['image_display_display_brightness'] = {
'widget': line_edit_display, 'label': QLabel('Display Max Brightness:')}
# update settings button
self.widget_settings['update_button'] = QPushButton('Apply Settings', self)
# self.settings_text_colour_original = self.widget_settings['update_button'].palette().color(
# QPalette.ColorRole.Text).name() # this doesn't work for some reason ...
self.settings_text_colour_original = 'black'
self.widget_settings['update_button'].clicked.connect(self.update_image_settings)
self.disable_settings_button()
'''
==================== functions to bind to sliders/widgets ====================
'''
# line edit colour based on if valid
[docs] def colour_lineedit(self, widget, txt):
if not is_float(txt):
widget.setStyleSheet("color: red;")
else:
widget.setStyleSheet("color: black;")
# sliders value changes
[docs] def generic_value_change(self, key, param_group):
index = self.widget_controls['slider'][key]['data'].value()
self.params_from_sliders[param_group][key] = self.sliders[param_group][key]['values'][index]
self.display_slider_num(key, param_group) # display the new value ont UI
[docs] def edit_slider_vals(self, sliders_dict, key, info_item):
_min = make_float_from_text(sliders_dict[key]['min_edit'].text())
_max = make_float_from_text(sliders_dict[key]['max_edit'].text())
self.make_slider_range(sliders_dict, key, _min, _max)
info_item['min'] = _min
info_item['max'] = _max
[docs] def display_slider_num(self, key, param_group, disp_len=5):
# display the updated value
value_str = str(self.params_from_sliders[param_group][key])
value_str = gui_utils.str_to_len(value_str, disp_len, '0', plus=True)
self.widget_controls['slider'][key]['value'].setText(value_str)
[docs] def reset_slider_group(self, param_group, redo_plots=False, display_images=True):
for key, item_sliders in self.sliders[param_group].items():
self.widget_controls['slider'][key]['data'].setValue(item_sliders['init_ind'])
if display_images == True:
self.display_images()
if redo_plots == True:
self.redo_plots(calc_range=False)
[docs] def reset_sliders(self):
self.update_images = False # dont calc any images/metrics when updating sliders now is slow
for param_group in self.sliders:
self.reset_slider_group(param_group, False, False)
self.update_images = True # make sure to return to calc images/metrics
self.display_images()
self.redo_plots(calc_range=False)
[docs] def set_image_name_text(self):
for i, data_store in enumerate(self.data_stores):
res = gui_utils.get_resolutions(data_store)
self.widget_row[i]['images']['original']['label'].setText(f"{data_store.get_reference_image_name()}\n{res['reference']}")
self.widget_row[i]['images']['transformed']['label'].setText(f"{gui_utils.get_transformed_image_name(data_store)}\n{res['transform']}")
for metric_image in data_store.metric_images:
if metric_image in self.checked_metric_images:
metric_name = gui_utils.get_metric_image_name(metric_image, data_store)
if len(metric_name) > 20:
metric_name = metric_image
self.widget_row[i]['metric_images'][metric_image]['label'].setText(metric_name)
[docs] def change_plot_lims(self, state):
if state == 2: # 2 is the checked value
self.plot_data_lim = self.data_lims['range_data']
else:
self.plot_data_lim = self.data_lims['fixed']
self.redo_plots(calc_range=False)
[docs] def change_dataset_name(self, txt):
''' change the dataset_name we are using '''
self.default_dataset_name = txt
[docs] def JND_dataset_range_lower(self, txt):
''' change the dataset_name we are using '''
try:
self.exp_range_lower = int(txt)
except:
return
if self.exp_range_lower < 1:
self.exp_range_lower = 1
elif self.exp_range_lower > self.exp_range_upper:
self.exp_range_lower = self.exp_range_upper
self.widget_controls['label']['range_edit_lower'].setText(
f"{self.exp_range_lower}")
[docs] def JND_dataset_range_upper(self, txt):
''' change the dataset_name we are using '''
try:
self.exp_range_upper = int(txt)
except:
return
if self.exp_range_upper < self.exp_range_lower:
self.exp_range_upper = self.exp_range_lower
elif self.exp_range_upper > len(self.data_stores[0]):
self.exp_range_upper = len(self.data_stores[0])
self.widget_controls['label']['range_edit_upper'].setText(
f"{self.exp_range_upper}")
'''
images settings apply
'''
[docs] def enable_settings_button(self):
self.widget_settings['update_button'].setEnabled(True)
if hasattr(self, 'settings_text_colour_original'):
self.widget_settings['update_button'].setStyleSheet(
f"QPushButton {{color: {self.settings_text_colour_original};}}")
[docs] def disable_settings_button(self):
self.widget_settings['update_button'].setEnabled(False)
self.widget_settings['update_button'].setStyleSheet(f"QPushButton {{color: gray;}}")
[docs] def update_image_settings(self):
''' button to apply new image settings '''
if hasattr(self, 'range_worker'):
self.range_worker.stop()
# apply changes to settings
self.change_pre_processing()
self.change_post_processing()
self.change_display_im_size()
self.change_graph_size()
self.change_num_steps()
self.change_display_im_rgb_brightness()
self.change_display_im_display_brightness()
self.change_data(0, _redo_plots=False) # update the image data settings
self.display_images()
if hasattr(self, 'update_UI'):
if self.update_UI == True:
self.construct_UI()
if hasattr(self, 'image_settings_update_plots'):
if self.image_settings_update_plots == True:
# only redo the graphs if nessesary
self.redo_plots(calc_range=True)
self.image_settings_update_plots = False
self.update_UI = False
self.disable_settings_button()
'''
image settings setters
'''
[docs] def change_pre_processing(self, *args):
self.pre_processing_option = self.widget_settings['image_pre_processing']['widget'].currentText(
)
for data_store in self.data_stores:
if hasattr(data_store, 'image_pre_processing'):
if data_store.image_pre_processing != self.pre_processing_options[self.pre_processing_option]:
data_store.image_pre_processing = self.pre_processing_options[
self.pre_processing_option]
self.image_settings_update_plots = True
[docs] def change_post_processing(self, *args):
self.post_processing_option = self.widget_settings['image_post_processing']['widget'].currentText(
)
for data_store in self.data_stores:
if hasattr(data_store, 'image_post_processing'):
if data_store.image_post_processing != self.post_processing_options[self.post_processing_option]:
data_store.image_post_processing = self.post_processing_options[
self.post_processing_option]
self.image_settings_update_plots = True
# self.display_images()
# self.redo_plots(calc_range=False)
[docs] def change_display_im_size(self):
txt = self.widget_settings['image_display_size']['widget'].text()
if txt == '':
txt = 1
new_size = max(1, int(txt))
if new_size != self.image_display_size:
self.image_display_size = new_size
self.update_UI = True
# self.construct_UI()
# self.display_images()
# if old_size > self.image_display_size:
# self.setMaximumSize(self.main_widget.sizeHint())
# if old_size < self.image_display_size:
# self.setMinimumSize(self.main_widget.sizeHint())
[docs] def change_graph_size(self):
txt = self.widget_settings['graph_display_size']['widget'].text()
if txt == '':
txt = 1
new_size = max(1, int(txt))
if new_size != self.graph_size:
self.graph_size = new_size
self.update_UI = True
[docs] def change_num_steps(self):
txt = self.widget_settings['graph_num_steps']['widget'].text()
if txt == '':
txt = 1
new_steps = max(2, int(txt))
if new_steps != self.num_steps_range:
self.num_steps_range = new_steps
self.update_UI = True
[docs] def change_display_im_rgb_brightness(self):
txt = self.widget_settings['image_display_rgb_brightness']['widget'].text()
if txt == '':
txt = 1
self.rgb_brightness = max(1, int(txt))
[docs] def change_display_im_display_brightness(self):
txt = self.widget_settings['image_display_display_brightness']['widget'].text()
if txt == '':
txt = 1
self.display_brightness = max(1, int(txt))
'''
status bar
'''
[docs] def update_progress(self, v):
self.pbar.setValue(v)
if v == 0:
self.status_bar.showMessage('Done', 3000)
[docs] def update_status_bar(self, v, time=None):
if time == None:
self.status_bar.showMessage(v)
elif isinstance(time, int):
self.status_bar.showMessage(v, time)
'''
export controls
'''
[docs] def change_text_export_trans(self, trans):
''' change colour of text when checkbox is pressed '''
checked = self.widget_export[trans]['check_box'].isChecked()
for widget in self.widget_export[trans]:
if widget == 'check_box':
pass
else:
self.widget_export[trans][widget].setEnabled(checked)
if checked == True:
self.widget_export[trans][widget].setStyleSheet(f"QLineEdit {{color: {self.settings_text_colour_original};}}\nQLabel {{color: {self.settings_text_colour_original};}}")
else:
self.widget_export[trans][widget].setStyleSheet(f"QLineEdit {{color: gray;}}\nQLabel {{color: gray;}}")
[docs] def export_trans_images(self):
self.export_in_progress = True
# make save folder
dir_folder = self.get_export_dir(0)
save_folder = os.path.join(
dir_folder, f"images-{str(datetime.datetime.now()).split('.')[0]}")
if not os.path.exists(save_folder):
os.makedirs(save_folder)
# save original image
ref_image = self.data_stores[0].get_reference_image()
image_utils.save_image(ref_image,
os.path.join(save_folder, f"reference.png"))
# make and save transforms
checked_transforms = {}
for trans in self.widget_export:
if self.widget_export[trans]['check_box'].isChecked():
data = {'min': make_float_from_text(self.widget_export[trans]['min_edit'].text()),
'max': make_float_from_text(self.widget_export[trans]['max_edit'].text()),
'num_steps': int(self.widget_export[trans]['steps_edit'].text()),
'function': self.checked_transformations[trans]['function']
}
name = self.widget_export[trans]['name'].text()
checked_transforms[name] = data
export_images = plot_utils.get_all_single_transform_params(
checked_transforms, num_steps='from_dict')
# remove any params with value 0
export_images = [x for x in export_images if not x[list(x.keys())[0]] == 0]
# make and save images
for single_trans in export_images:
trans_name = list(single_trans.keys())[0]
param = single_trans[trans_name]
img = image_utils.get_transform_image(self.data_stores[0],
transform_functions={trans_name: self.checked_transformations[trans_name]},
transform_params={trans_name: param})
trans_info = {'transform_name': trans_name, 'transform_value': param}
image_utils.save_image(img, os.path.join(
save_folder, f'{save_utils.make_name_for_trans(trans_info)}.png'))
self.status_bar.showMessage(f'Images saved to {save_folder}', 5000)
self.export_in_progress = False
self.last_export = save_folder
'''
experiments
'''
[docs] def change_text_exp_trans(self, trans):
''' change colour of text when checkbox is pressed '''
checked = self.widget_experiment_params[trans]['check_box'].isChecked()
for widget in self.widget_experiment_params[trans]:
if widget == 'check_box':
pass
else:
self.widget_experiment_params[trans][widget].setEnabled(checked)
if checked == True:
self.widget_experiment_params[trans][widget].setStyleSheet(f"QLineEdit {{color: {self.settings_text_colour_original};}}\nQLabel {{color: {self.settings_text_colour_original};}}")
else:
self.widget_experiment_params[trans][widget].setStyleSheet(f"QLineEdit {{color: gray;}}\nQLabel {{color: gray;}}")
[docs] def launch_experiment_JND(self):
'''launch the Just Noticable difference experiment'''
# first get all the checked transforms and their parameters
checked_transformation_params = {}
for trans in self.widget_experiment_params:
if self.widget_experiment_params[trans]['check_box'].isChecked():
data = {'min': make_float_from_text(self.widget_experiment_params[trans]['min_edit'].text()),
'max': make_float_from_text(self.widget_experiment_params[trans]['max_edit'].text()),
'num_steps': int(self.widget_experiment_params[trans]['steps_edit'].text()),
'function': self.checked_transformations[trans]['function']}
name = self.widget_experiment_params[trans]['name'].text()
checked_transformation_params[name] = data
# check that we only have one transform selected to JND test to work
if len(checked_transformation_params) != 1:
# show message to UI if more than one transform
self.status_bar.showMessage(
'!!! Select only one tranform/distortion for Just Noticable Difference Experiment !!!', 10000)
else:
# run JND experiment
self.experiment_JND = IQM_Vis.UI.make_experiment_JND(checked_transformation_params,
self.data_stores[0],
self.image_display_size,
self.rgb_brightness,
self.display_brightness,
self.default_save_dir,
self.default_dataset_name,
self.pre_processing_option,
self.post_processing_option,
self.exp_range_lower,
self.exp_range_upper,
self.checked_metrics)
self.experiment_JND.saved_experiment.connect(
self.change_human_scores_after_exp_JND)
self.experiment_JND.show()
self.experiment_JND.showFullScreen()
[docs] def launch_experiment_2AFC(self):
'''Launch the 2 alternate forced choice experiment'''
# first get all the checked transforms and their parameters
checked_transformation_params = {}
for trans in self.widget_experiment_params:
if self.widget_experiment_params[trans]['check_box'].isChecked():
data = {'min': make_float_from_text(self.widget_experiment_params[trans]['min_edit'].text()),
'max': make_float_from_text(self.widget_experiment_params[trans]['max_edit'].text()),
'num_steps': int(self.widget_experiment_params[trans]['steps_edit'].text()),
'function': self.checked_transformations[trans]['function']}
name = self.widget_experiment_params[trans]['name'].text()
checked_transformation_params[name] = data
if self.checked_transformations != {}:
self.experiment_2AFC = IQM_Vis.UI.make_experiment_2AFC(checked_transformation_params,
self.data_stores[0],
self.image_display_size,
self.rgb_brightness,
self.display_brightness,
self.default_save_dir,
self.pre_processing_option,
self.post_processing_option,
self.checked_metrics)
self.experiment_2AFC.saved_experiment.connect(
self.change_human_scores_after_exp_2AFC)
self.experiment_2AFC.show()
self.experiment_2AFC.showFullScreen()
else:
self.status_bar.showMessage('Cannot run experiment without transforms', 5000)
[docs] @pyqtSlot(str)
def change_human_scores_after_exp_2AFC(self, path):
self._change_human_exp_2AFC(path)
[docs] @pyqtSlot(str)
def change_human_scores_after_exp_JND(self, path):
self._change_human_exp_JND(path)
'''
float checking functionality for checkign line edit
'''
[docs]def get_float_validator():
# float_validator = QDoubleValidator()
# float_validator.setLocale(QLocale('en_US')) # so comma cannot be used a decimal place for spanish users
# float_validator.setNotation(QDoubleValidator.Notation(0)) # StandardNotation
# return float_validator
return custom_float_validator()
[docs]def is_float(input_string):
# if input_string == '-' or input_string == '+' or input_string == '':
# return False
# pattern = r'^[-+]?[0-9]*\.?[0-9]*$'
# match = re.match(pattern, input_string)
# if match:
# return True
# return False
try:
float(input_string)
return True
except ValueError:
return False
[docs]def is_almost_float(input_string):
# if input_string == '-' or input_string == '+' or input_string == '':
# return True
# pattern = r'^[-+]?[0-9]*.*[0-9]$'
# match = re.match(pattern, input_string)
# if match:
# return True
# return False
return True
[docs]class custom_float_validator(QValidator):
def __init__(self, parent=None):
super(custom_float_validator, self).__init__(parent)
[docs] def validate(self, string, pos):
if is_float(string):
return QValidator.State(2), string, pos # Acceptable
if is_almost_float(string):
return QValidator.State(1), string, pos # Intermediate
return QValidator.State(0), string, pos # Invalid
[docs]def make_float_from_text(txt):
# get rid of pesky commas
no_commas = txt.replace(',', '')
return float(no_commas)