class DistanceGroup(DModule, QtWidgets.QGroupBox): calculate = QtCore.Signal() delete = QtCore.Signal() def __init__(self, view): self.view = view self.model = view.model DModule.__init__(self) QtWidgets.QGroupBox.__init__(self, "Distance") self.setLayout(QtWidgets.QVBoxLayout()) self.calculate_button = Button("Calculate Distances", self.on_calculate) self.delete_button = Button("Delete Distances", self.on_delete) self.layout().addWidget(self.calculate_button) self.layout().addWidget(self.delete_button) self.update() self.connect_broadcast(Broadcasts.STORE_LOADED, self.on_data_source_changed) self.connect_broadcast(Broadcasts.STORE_DATA_SOURCE_CHANGED, self.on_data_source_changed) self.connect_broadcast(Broadcasts.STORE_DATA_CHANGED, self.on_data_changed) def update(self): self.calculate_button.setEnabled(self.model.has_samples() and (not self.model.has_distance())) self.delete_button.setEnabled(self.model.has_samples() and self.model.has_distance()) @QtCore.Slot() def on_calculate(self): self.calculate.emit() @QtCore.Slot() def on_delete(self): self.delete.emit() def on_data_source_changed(self, *args): self.update() def on_data_changed(self, *args): self.update()
class DescriptorGroup(DModule, QtWidgets.QGroupBox): load_data = QtCore.Signal() cluster_classes_changed = QtCore.Signal() def __init__(self, view): self.view = view self.model = view.model DModule.__init__(self) QtWidgets.QGroupBox.__init__(self, "Class / Descriptors") self.setLayout(QtWidgets.QVBoxLayout()) self.sample_class_combo = Combo(self.on_sample_class_changed) self.cluster_class_combo = Combo(self.on_cluster_class_changed, editable=True) self.node_class_combo = Combo(self.on_node_class_changed, editable=True) classes_frame = QtWidgets.QFrame() classes_frame.setLayout(QtWidgets.QFormLayout()) classes_frame.layout().setContentsMargins(0, 0, 0, 0) classes_frame.layout().addRow(QtWidgets.QLabel("Sample Class:"), self.sample_class_combo) classes_frame.layout().addRow(QtWidgets.QLabel("Cluster Class:"), self.cluster_class_combo) classes_frame.layout().addRow(QtWidgets.QLabel("Node Class:"), self.node_class_combo) self.descriptors_frame = QtWidgets.QFrame() self.descriptors_frame.setLayout(QtWidgets.QFormLayout()) self.descriptors_frame.layout().setContentsMargins(0, 0, 0, 0) self.descriptors_frame.setVisible(False) self.load_data_button = Button("Load Data", self.on_load_data) self.load_descr_button = Button("Load Descriptors...", self.on_load_descriptors) button_frame = QtWidgets.QFrame() button_frame.setLayout(QtWidgets.QHBoxLayout()) button_frame.layout().setContentsMargins(0, 0, 0, 0) button_frame.layout().addWidget(self.load_data_button) button_frame.layout().addWidget(self.load_descr_button) descriptor_box = QtWidgets.QFrame() descriptor_box.setLayout(QtWidgets.QVBoxLayout()) descriptor_box.layout().setContentsMargins(0, 0, 0, 0) self.descriptor_header_button = HeaderButton("Descriptors") descriptor_box.layout().addWidget(self.descriptor_header_button) descriptor_box.layout().addWidget(self.descriptors_frame) self.descriptor_header_button.clicked.connect( self.on_descriptors_clicked) self.layout().addWidget(classes_frame) self.layout().addWidget(descriptor_box) self.layout().addWidget(button_frame) self.load_descriptors() self.update() self.connect_broadcast(Broadcasts.STORE_LOADED, self.on_store_changed) self.connect_broadcast(Broadcasts.STORE_DATA_SOURCE_CHANGED, self.on_store_changed) self.connect_broadcast(Broadcasts.STORE_DATA_CHANGED, self.on_store_changed) def load_descriptors(self, descriptors=None): sample_cls = self.sample_class_combo.get_value() self.model.load_lap_descriptors(descriptors, sample_cls) for row in range(self.descriptors_frame.layout().rowCount())[::-1]: self.descriptors_frame.layout().removeRow(row) if self.model.lap_descriptors is not None: for name in self.model.lap_descriptors: self.descriptors_frame.layout().addRow( QtWidgets.QLabel(" %s" % (name)), QtWidgets.QLabel(".".join( self.model.lap_descriptors[name]))) def update(self): self.load_data_button.setEnabled( self.model.is_connected() and (self.model.lap_descriptors is not None)) def update_classes(self): if self.model.is_connected(): descriptors = set(self.model.descriptor_names) classes = natsorted([ name for name in self.model.classes if name not in descriptors ]) self.sample_class_combo.set_values(classes, "Sample") self.cluster_class_combo.set_values(classes, "CMCluster") self.node_class_combo.set_values(classes, "CMNode") else: self.sample_class_combo.set_values([]) self.cluster_class_combo.set_values([]) self.node_class_combo.set_values([]) self.model.cluster_class = self.cluster_class_combo.get_value() self.model.node_class = self.node_class_combo.get_value() def on_store_changed(self, *args): self.update_classes() self.load_descriptors() self.update() @QtCore.Slot() def on_descriptors_clicked(self): visible = self.descriptors_frame.isVisible() if visible: self.descriptor_header_button.setArrowType(QtCore.Qt.RightArrow) else: self.descriptor_header_button.setArrowType(QtCore.Qt.DownArrow) self.descriptors_frame.setVisible(not visible) @QtCore.Slot() def on_sample_class_changed(self): self.load_descriptors() self.update() self.cluster_classes_changed.emit() @QtCore.Slot() def on_node_class_changed(self): self.model.node_class = self.node_class_combo.get_value() self.update() self.cluster_classes_changed.emit() @QtCore.Slot() def on_cluster_class_changed(self): self.model.cluster_class = self.cluster_class_combo.get_value() self.update() self.cluster_classes_changed.emit() @QtCore.Slot() def on_load_data(self): self.load_data.emit() @QtCore.Slot() def on_load_descriptors(self): url, format = QtWidgets.QFileDialog.getOpenFileUrl( self.view, caption="Import Descriptors", filter="(*.txt)") url = str(url.toString()) if not url: return path = as_path(url) if path is None: return with open(path, "r") as f: _, descriptors = json.load(f) self.load_descriptors(descriptors) self.update()
class View(DModule, QtWidgets.QMainWindow): MODE_IDS = 0x00000001 MODE_DISTANCE_MIN = 0x00000002 MODE_DISTANCE_MAX = 0x00000004 MODE_CLUSTER = 0x00000008 def __init__(self): self.model = None self.registry = None self.mode = None self._loaded = False DModule.__init__(self) QtWidgets.QMainWindow.__init__(self) self.model = Model(self) self.setWindowTitle("CeraMatch") self.setWindowIcon(QtGui.QIcon("res\cm_icon.svg")) self.setStyleSheet("QPushButton {padding: 5px; min-width: 100px;}") self.central_widget = QtWidgets.QWidget(self) self.central_widget.setLayout(QtWidgets.QVBoxLayout()) self.central_widget.layout().setContentsMargins(0, 0, 0, 0) self.setCentralWidget(self.central_widget) self.splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) self.central_widget.layout().addWidget(self.splitter) self.registry = Registry("Deposit") self.image_view = ImageView(self) self.footer_frame = FooterFrame(self) self.descriptor_group = DescriptorGroup(self) self.sort_group = SortGroup(self) self.cluster_group = ClusterGroup(self) self.menu = Menu(self) self.toolbar = ToolBar(self) self.statusbar = StatusBar(self) self.calculate_button = Button("Calculate Distances", self.on_calculate) self.calculate_button.setEnabled(False) self.left_frame = QtWidgets.QFrame(self) self.left_frame.setLayout(QtWidgets.QVBoxLayout()) self.left_frame.layout().setContentsMargins(10, 10, 0, 10) self.right_frame = QtWidgets.QFrame(self) self.right_frame.setLayout(QtWidgets.QVBoxLayout()) self.right_frame.layout().setContentsMargins(0, 0, 0, 0) self.splitter.addWidget(self.left_frame) self.splitter.addWidget(self.right_frame) self.left_frame.layout().addWidget(self.descriptor_group) group = QtWidgets.QGroupBox("Calculate") group.setLayout(QtWidgets.QVBoxLayout()) group.layout().addWidget(self.calculate_button) self.left_frame.layout().addWidget(group) self.left_frame.layout().addWidget(self.sort_group) self.left_frame.layout().addWidget(self.cluster_group) self.left_frame.layout().addStretch() self.right_frame.layout().addWidget(self.image_view) self.right_frame.layout().addWidget(self.footer_frame) self.setStatusBar(self.statusbar) self._loaded = True self.setGeometry(100, 100, 1024, 768) self.on_samples() self.footer_frame.slider_zoom.setValue(100) self.connect_broadcast(Broadcasts.VIEW_ACTION, self.on_update) self.connect_broadcast(Broadcasts.STORE_LOCAL_FOLDER_CHANGED, self.on_update) self.connect_broadcast(Broadcasts.STORE_SAVED, self.on_update) self.connect_broadcast(Broadcasts.STORE_LOADED, self.on_update) self.connect_broadcast(Broadcasts.STORE_DATA_SOURCE_CHANGED, self.on_update) self.connect_broadcast(Broadcasts.STORE_DATA_CHANGED, self.on_update) def get_selected(self): # returns [[sample_id, DResource, label, value, index], ...] return self.image_view.get_selected() def update(self): if not hasattr(self, "descriptor_group"): return self.descriptor_group.update() self.sort_group.update() self.cluster_group.update() self.footer_frame.update() self.toolbar.update() self.image_view.update_() selected = self.get_selected() self.calculate_button.setEnabled(self.model.is_connected() and not self.model.has_distances()) if selected: if self.mode == self.MODE_DISTANCE_MIN: text = "Distance: %s, Sample ID: %s" % (selected[0].value, selected[0].id) else: cluster = selected[0].cluster levels = self.image_view.get_selected_level() if levels: cluster = ".".join(cluster.split(".")[:levels[0]]) if cluster: text = "Cluster: %s, Leaf: %s, Sample ID: %s" % ( cluster, selected[0].value, selected[0].id) else: text = "Sample ID: %s" % (selected[0].id) self.statusbar.message(text) def reload_samples(self): self.model.load_samples() self.update() self.on_samples() def on_update(self, *args): self.update() def on_set_datasource(self, *args): self.reload_samples() def on_calculate(self, *args): self.model.calc_distances() def on_samples(self, *args): self.mode = self.MODE_IDS self.model.sort_by_ids() def on_distance_max(self, *args): self.mode = self.MODE_DISTANCE_MAX self.model.sort_by_distmax() def on_distance_min(self, *args): selected = self.get_selected() if selected: sample_id = selected[0].id elif isinstance(self.model.samples[0].value, float): sample_id = self.model.samples[0].id else: return self.mode = self.MODE_DISTANCE_MIN self.model.sort_by_distance(sample_id) def on_cluster(self, *args): self.mode = self.MODE_CLUSTER if self.model.has_clusters(): self.model.update_clusters() def on_auto_cluster(self, *args): self.mode = self.MODE_CLUSTER self.model.auto_cluster() def on_split_cluster(self, *args): selected = self.image_view.get_selected() if len(selected) != 1: return cluster = selected[0].cluster if cluster is None: return self.model.split_cluster(cluster, selected[0]) def on_join_parent(self, *args): selected = self.image_view.get_selected() if not selected: return self.mode = self.MODE_CLUSTER clusters = set() for sample in selected: if sample.cluster is None: continue clusters.add(sample.cluster) if clusters: for cluster in clusters: self.model.join_cluster_to_parent(cluster, selected[0]) def on_join_children(self, *args): selected = self.image_view.get_selected() if not selected: return self.mode = self.MODE_CLUSTER clusters = set() for sample in selected: if sample.cluster is None: continue clusters.add(sample.cluster) if clusters: levels = self.image_view.get_selected_level() if not levels: return level = max(levels) for cluster in clusters: self.model.join_children_to_cluster(cluster, level, selected[0]) else: return def on_manual_cluster(self, *args): selected = self.image_view.get_selected() if not selected: return self.model.manual_cluster(selected, selected[0]) def on_clear_clusters(self, *args): self.model.clear_clusters() self.model.sort_by_ids() def on_reload(self, *args): if self.mode is None: return if self.mode == self.MODE_IDS: self.on_samples() elif self.mode == self.MODE_DISTANCE_MIN: self.on_distance_min() elif self.mode == self.MODE_DISTANCE_MAX: self.on_distance_max() elif self.mode == self.MODE_CLUSTER: self.on_cluster() def on_prev(self, *args): self.model.browse_distmax(-1) def on_next(self, *args): self.model.browse_distmax(1) def on_zoom(self, value): self.image_view.set_thumbnail_size(value) def on_drop(self, src_ids, tgt_id): self.model.add_to_cluster(src_ids, tgt_id) def closeEvent(self, event): if not self.model.is_saved(): reply = QtWidgets.QMessageBox.question( self, "Exit", "Save changes to database?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel) if reply == QtWidgets.QMessageBox.Yes: self.toolbar.on_save() elif reply == QtWidgets.QMessageBox.No: pass else: event.ignore() return self.model.on_close()
class SortGroup(QtWidgets.QGroupBox): def __init__(self, view): self.view = view self.model = view.model QtWidgets.QGroupBox.__init__(self, "Sort") self.setLayout(QtWidgets.QVBoxLayout()) self.samples_button = Button("Sort by Sample IDs", self.view.on_samples) self.samples_button.setEnabled(False) self.distance_max_button = Button("Sort by Max Distance", self.view.on_distance_max) self.distance_max_button.setEnabled(False) self.distance_min_button = Button("Sort by Min Distance", self.view.on_distance_min) self.distance_min_button.setEnabled(False) self.cluster_button = Button("Sort by Clustering", self.view.on_cluster) self.cluster_button.setEnabled(False) self.layout().addWidget(self.samples_button) self.layout().addWidget(self.distance_max_button) self.layout().addWidget(self.distance_min_button) self.layout().addWidget(self.cluster_button) def update(self): selected = self.view.get_selected() self.samples_button.setEnabled(self.model.is_connected()) self.distance_max_button.setEnabled(self.model.has_distances()) self.distance_min_button.setEnabled( self.model.has_distances() and ((len(selected) > 0) or ((len(self.model.samples) > 0) and isinstance(self.model.samples[0].value, float)))) self.cluster_button.setEnabled(self.model.has_clusters())
class ClusterGroup(DModule, QtWidgets.QGroupBox): cluster = QtCore.Signal() update_tree = QtCore.Signal() add_cluster = QtCore.Signal() delete = QtCore.Signal() def __init__(self, view): self.view = view self.model = view.model DModule.__init__(self) QtWidgets.QGroupBox.__init__(self, "Clustering") self.setLayout(QtWidgets.QVBoxLayout()) self.cluster_button = Button("Auto Cluster", self.on_cluster) self.n_clusters_combo = Combo(self.on_n_clusters_changed) self.limit_edit = LineEdit("0.68") self.n_samples_label = QtWidgets.QLabel("") self.n_clusters_label = QtWidgets.QLabel("") self.update_tree_button = Button("Update Tree", self.on_update_tree) self.add_cluster_button = Button("Make Cluster", self.on_add_cluster) self.delete_button = Button("Clear Clusters", self.on_delete) form_frame = QtWidgets.QFrame() form_frame.setLayout(QtWidgets.QFormLayout()) form_frame.layout().setContentsMargins(0, 0, 0, 0) form_frame.layout().addRow(QtWidgets.QLabel("N Clusters:"), self.n_clusters_combo) form_frame.layout().addRow(QtWidgets.QLabel("Dist. Limit:"), self.limit_edit) form_frame.layout().addRow(QtWidgets.QLabel("Samples:"), self.n_samples_label) form_frame.layout().addRow(QtWidgets.QLabel("Clusters Found:"), self.n_clusters_label) self.layout().addWidget(self.cluster_button) self.layout().addWidget(form_frame) self.layout().addWidget(self.update_tree_button) self.layout().addWidget(self.add_cluster_button) self.layout().addWidget(self.delete_button) self.update() self.connect_broadcast(Broadcasts.STORE_LOADED, self.on_data_source_changed) self.connect_broadcast(Broadcasts.STORE_DATA_SOURCE_CHANGED, self.on_data_source_changed) self.connect_broadcast(Broadcasts.STORE_DATA_CHANGED, self.on_data_changed) def get_limits(self): try: n_clusters = int(self.n_clusters_combo.get_value()) except: n_clusters = None try: limit = float(self.limit_edit.text()) except: limit = 0.68 return n_clusters, limit def update(self): self.cluster_button.setEnabled(self.model.has_cluster_classes() and self.model.has_distance()) self.n_clusters_combo.setEnabled(self.model.has_distance()) self.limit_edit.setEnabled( self.model.has_distance() and (self.n_clusters_combo.get_value() == "By limit")) self.n_samples_label.setText(str(len(self.model.sample_ids))) self.update_tree_button.setEnabled(self.model.has_samples()) self.add_cluster_button.setEnabled( len(self.view.graph_view.get_selected()) > 0) self.delete_button.setEnabled(self.model.has_cluster_classes() and self.model.has_clusters()) def update_n_clusters(self): values = ["By limit"] if self.model.has_distance(): values += list(range(2, len(self.model.sample_ids) + 1)) self.n_clusters_combo.set_values(values) self.update() def update_clusters_found(self, n_clusters): if str(n_clusters).isnumeric(): self.n_clusters_label.setText(str(n_clusters)) else: self.n_clusters_label.setText("") @QtCore.Slot() def on_cluster(self): self.cluster.emit() @QtCore.Slot() def on_update_tree(self): self.update_tree.emit() @QtCore.Slot() def on_add_cluster(self): self.add_cluster.emit() @QtCore.Slot() def on_delete(self): self.delete.emit() @QtCore.Slot() def on_n_clusters_changed(self): self.update() def on_data_source_changed(self, *args): self.update_n_clusters() self.update_clusters_found(None) self.update() def on_data_changed(self, *args): self.update()
class DescriptorGroup(QtWidgets.QGroupBox): def __init__(self, view): self.view = view self.model = view.model self.changed = False QtWidgets.QGroupBox.__init__(self, "Class / Descriptors") self.setLayout(QtWidgets.QVBoxLayout()) self.class_combo = Combo(self.on_changed) self.id_combo = Combo(self.on_changed) self.profile_combo = Combo(self.on_changed) self.radius_combo = Combo(self.on_changed) self.recons_combo = Combo(self.on_changed) self.load_button = Button("Load", self.on_load) form_frame = QtWidgets.QFrame() form_frame.setLayout(QtWidgets.QFormLayout()) form_frame.layout().setContentsMargins(0, 0, 0, 0) cluster_label = QtWidgets.QLineEdit("CMCluster") cluster_label.setReadOnly(True) cluster_label.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Expanding) form_frame.layout().addRow(QtWidgets.QLabel("Sample Class:"), self.class_combo) form_frame.layout().addRow(QtWidgets.QLabel("Cluster Class:"), cluster_label) form_frame.layout().addRow(QtWidgets.QLabel("Sample Descriptors:"), None) form_frame.layout().addRow(QtWidgets.QLabel("Id:"), self.id_combo) form_frame.layout().addRow(QtWidgets.QLabel("Profile:"), self.profile_combo) form_frame.layout().addRow(QtWidgets.QLabel("Radius:"), self.radius_combo) form_frame.layout().addRow(QtWidgets.QLabel("Drawing:"), self.recons_combo) self.layout().addWidget(form_frame) self.layout().addWidget(self.load_button) def update(self): if self.model.is_connected(): self.class_combo.set_values( natsorted(list(self.model.classes.keys())), "Sample") sample_cls = self.class_combo.get_value() if sample_cls: descriptors = self.model.classes[sample_cls].descriptors self.id_combo.set_values(descriptors, "Id") self.profile_combo.set_values(descriptors, "Profile") self.radius_combo.set_values(descriptors, "Radius") self.recons_combo.set_values(descriptors, "Reconstruction") self.load_button.setEnabled(self.model.is_connected() and self.changed and (self.get_values() is not None)) def get_values(self): # return [Sample Class, ID Descriptor, Profile Descriptor, Radius Descriptor, Reconstruction Descriptor] values = [] controls = [ self.class_combo, self.id_combo, self.profile_combo, self.radius_combo, self.recons_combo ] for control in controls: value = control.get_value() if value: values.append(value) if len(values) == len(controls): return values return None def on_changed(self, *args): self.changed = True self.update() def on_load(self, *args): self.changed = False self.view.reload_samples()
class ClusterGroup(QtWidgets.QGroupBox): def __init__(self, view): self.view = view self.model = view.model self.sliders = {} QtWidgets.QGroupBox.__init__(self, "Cluster") self.setLayout(QtWidgets.QVBoxLayout()) self.autoclust_button = Button("Auto Cluster", self.view.on_auto_cluster) self.autoclust_button.setEnabled(False) self.split_button = Button("Auto Split", self.view.on_split_cluster) self.split_button.setEnabled(False) self.join_parent_button = Button("Join to Parent", self.view.on_join_parent) self.join_parent_button.setEnabled(False) self.join_children_button = Button("Join Children", self.view.on_join_children) self.join_children_button.setEnabled(False) self.manual_button = Button("Create Manual", self.view.on_manual_cluster) self.manual_button.setEnabled(False) self.clear_button = Button("Clear All", self.view.on_clear_clusters) self.clear_button.setEnabled(False) self.layout().addWidget(self.autoclust_button) self.layout().addWidget(self.split_button) self.layout().addWidget(self.join_parent_button) self.layout().addWidget(self.join_children_button) self.layout().addWidget(self.manual_button) self.layout().addWidget(self.clear_button) def update(self): selected = self.view.get_selected() has_cluster = ((len(selected) > 0) and (selected[0].has_cluster())) self.autoclust_button.setEnabled(self.model.has_distances()) self.split_button.setEnabled(len(selected) == 1) self.join_parent_button.setEnabled(has_cluster) self.join_children_button.setEnabled( has_cluster and not self.view.image_view.is_list()) self.manual_button.setEnabled(len(selected) > 0) self.clear_button.setEnabled(self.model.has_clusters())