From 1b85731287fdc1640377ef2295b1719b1ad2e60f Mon Sep 17 00:00:00 2001 From: DanicaSTFC <138598662+DanicaSTFC@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:10:43 +0100 Subject: [PATCH] Improve point-cloud file format handling (#262) - Add `openpyxl` to recipe files - Allow csv and xlxs formats in point-cloud file & add error dialog --- CHANGES.md | 2 ++ recipe/dev_environment.yml | 3 ++- recipe/meta.yaml | 2 ++ src/idvc/dvc_interface.py | 34 ++++++++++++++++++++++++++----- src/idvc/pointcloud_conversion.py | 5 +---- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 293241ee..0a56df9f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ # ChangeLog ## vx.x.x +* Add `openpyxl` to recipe files #262 +* Allow csv and xlxs formats in point-cloud file & add error dialog #262 * Set registration-box-size default and help text #259 * Make dimensionality 3D the default #256 * Edit README.md to include Prof. Bay citations and ref to DVC executable #255 diff --git a/recipe/dev_environment.yml b/recipe/dev_environment.yml index c5378a57..5551e4d5 100644 --- a/recipe/dev_environment.yml +++ b/recipe/dev_environment.yml @@ -3,7 +3,8 @@ channels: - ccpi - paskino - conda-forge -dependencies: +dependencies: + - openpyxl - python - numpy - scipy diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 400fb042..482e190f 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -34,8 +34,10 @@ requirements: - numpy - typing_extensions # [py<=37] run: + - openpyxl - python - numpy + - scipy - ccpi-viewer >=22.4.0 - ccpi-dvc >=22.0.0 - natsort diff --git a/src/idvc/dvc_interface.py b/src/idvc/dvc_interface.py index 2a688cde..1c48fe46 100644 --- a/src/idvc/dvc_interface.py +++ b/src/idvc/dvc_interface.py @@ -14,6 +14,7 @@ # Author: Edoardo Pasca (UKRI-STFC) import os +from openpyxl import load_workbook import sys import PySide2 from PySide2 import QtCore, QtGui, QtWidgets @@ -99,6 +100,7 @@ from idvc.utils.AutomaticRegistration import AutomaticRegistration +allowed_point_cloud_file_formats = ('.txt','.csv','.xlsx') class MainWindow(QMainWindow): def __init__(self): QMainWindow.__init__(self) @@ -2895,7 +2897,8 @@ def CreatePointCloudPanel(self): 2 300 750.2 209\n\ etc.\n\ Non-integer voxel locations are admitted, with reference volume interpolation used as needed.\n\ -The first point is significant, as it is used as a global starting point and reference for the rigid_trans variable.") +The first point is significant, as it is used as a global starting point and reference for the rigid_trans variable.\n\ +File format allowed: 'txt', 'csv, 'xlxs'.") pc['roi_browse'].clicked.connect(self.select_pointcloud) self.graphWidgetFL.setWidget(widgetno, QFormLayout.FieldRole, pc['roi_browse']) widgetno += 1 @@ -3072,6 +3075,9 @@ def updatePointCloudPanel(self): self.overlapXValueEntry.setEnabled(False) def select_pointcloud(self): #, label): + """Opens a dialog to select the pointcloud from a file. + Runs the creation or loading of the point cloud in a worker. + Sets the attribut `self.pointcloud_is` to 'loaded'.""" dialogue = QFileDialog() self.roi = None self.roi = dialogue.getOpenFileName(self,"Select a roi")[0] @@ -3089,6 +3095,8 @@ def select_pointcloud(self): #, label): self.pointcloud_is = 'loaded' def PointCloudWorker(self, type, filename = None, disp_file = None, vector_dim = None): + """Runs the worker to create or load the point cloud. + If the format of the point-cloud file is not in the allowed list it displays an error dialog.""" if type == "create": #if not self.pointCloudCreated: self.clearPointCloud() @@ -3100,8 +3108,15 @@ def PointCloudWorker(self, type, filename = None, disp_file = None, vector_dim = self.pointcloud_worker.signals.result.connect(self.DisplayLoadedPointCloud) elif type == "load pointcloud file": self.clearPointCloud() - self.pointcloud_worker = Worker(self.loadPointCloud, self.roi) - self.pointcloud_worker.signals.result.connect(self.DisplayLoadedPointCloud) + if self.roi.endswith(allowed_point_cloud_file_formats): + self.pointcloud_worker = Worker(self.loadPointCloud, self.roi) + self.pointcloud_worker.signals.result.connect(self.DisplayLoadedPointCloud) + else: + error_title = "FILE FORMAT ERROR" + error_text = f"Error reading the point-cloud file {self.roi}. Allowed formats are {allowed_point_cloud_file_formats}." + self.displayFileErrorDialog(message=error_text, title=error_title) + return + elif type == "create without loading": #if not self.pointCloudCreated: self.clearPointCloud() @@ -3414,6 +3429,9 @@ def createPointCloud(self, **kwargs): return True def loadPointCloud(self, *args, **kwargs): + """Loads a pointcloud from file. + File formats allowed are 'txt', 'csv', 'xlxs'. + """ time.sleep(0.1) #required so that progress window displays pointcloud_file = os.path.abspath(args[0]) progress_callback = kwargs.get('progress_callback', None) @@ -3421,8 +3439,14 @@ def loadPointCloud(self, *args, **kwargs): #self.clearPointCloud() #need to clear current pointcloud before we load next one TODO: move outside thread progress_callback.emit(30) self.roi = pointcloud_file - - points = np.loadtxt(self.roi) + if pointcloud_file.endswith('.txt'): + points = np.loadtxt(pointcloud_file) + elif pointcloud_file.endswith('.csv'): + points = np.genfromtxt(pointcloud_file, delimiter=',') + elif pointcloud_file.endswith('.xlsx'): + workbook = load_workbook(pointcloud_file, read_only=True) + sheet = workbook.active + points = np.array(list(sheet.values)) # except ValueError as ve: # print(ve) # return diff --git a/src/idvc/pointcloud_conversion.py b/src/idvc/pointcloud_conversion.py index 6d53c7fd..73523feb 100644 --- a/src/idvc/pointcloud_conversion.py +++ b/src/idvc/pointcloud_conversion.py @@ -429,6 +429,7 @@ def __init__(self): def GetPoints(self): '''Returns the Points''' return self._Points + def SetData(self, value): '''Sets the points from a numpy array or list''' if not isinstance (value, numpy.ndarray) : @@ -441,12 +442,10 @@ def SetData(self, value): def GetData(self): return self._Data - def GetNumberOfPoints(self): '''returns the number of points in the point cloud''' return self._Points.GetNumberOfPoints() - def FillInputPortInformation(self, port, info): # if port == 0: # info.Set(vtk.vtkAlgorithm.INPUT_REQUIRED_DATA_TYPE(), "vtkImageData") @@ -457,7 +456,6 @@ def FillOutputPortInformation(self, port, info): return 1 def RequestData(self, request, inInfo, outInfo): - # print ("Request Data") # output_image = vtk.vtkDataSet.GetData(inInfo[0]) pointPolyData = vtk.vtkPolyData.GetData(outInfo) @@ -472,7 +470,6 @@ def RequestData(self, request, inInfo, outInfo): pointPolyData.SetVerts(self._Vertices) return 1 - def FillCells(self): '''Fills the Vertices''' vertices = self._Vertices