From 1c0857c4545a51f653adb64e906dc775548b42a7 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 30 Nov 2022 15:41:26 +0000 Subject: [PATCH 1/9] fix point cloud creation issues closes #143 --- src/idvc/dvc_interface.py | 57 ++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/idvc/dvc_interface.py b/src/idvc/dvc_interface.py index 715dbbb1..a71a3391 100644 --- a/src/idvc/dvc_interface.py +++ b/src/idvc/dvc_interface.py @@ -60,7 +60,7 @@ import copy -from idvc.io import ImageDataCreator +from idvc.io import ImageDataCreator, getProgress, displayErrorDialogFromWorker from idvc.pointcloud_conversion import cilRegularPointCloudToPolyData, cilNumpyPointCloudToPolyData, PointCloudConverter @@ -78,6 +78,10 @@ import logging +class PrintCallback(object): + '''Class to handle the emit call when no callback is provided''' + def emit(self, *args, **kwargs): + print (args, kwargs) def reduce_displ(raw_displ, min_size, max_size, pzero=False): '''filter the diplacement vectors based on their size''' @@ -2202,7 +2206,7 @@ def loadMask(self, **kwargs): #loading mask from a file def ui_progress_update(self, callback, vtkcaller, event): '''connects the progress from a vtkAlgorithm to a Qt callback from eqt Worker''' - print ("{} progress {}".format(type(vtkcaller), vtkcaller.GetProgress())) + # print ("{} progress {}".format(type(vtkcaller), vtkcaller.GetProgress())) progress = vtkcaller.GetProgress()*100-1 if progress < 0: progress = 0 @@ -2806,21 +2810,24 @@ def PointCloudWorker(self, type, filename = None, disp_file = None, vector_dim = #if not self.pointCloudCreated: self.clearPointCloud() self.pointcloud_worker = Worker(self.createPointCloud, filename = "latest_pointcloud.roi") - self.pointcloud_worker.signals.finished.connect(self.DisplayPointCloud) + self.pointcloud_worker.signals.result.connect(self.DisplayPointCloud) elif type == "load selection": self.clearPointCloud() self.pointcloud_worker = Worker(self.loadPointCloud, os.path.join(tempfile.tempdir, self.pointcloud_parameters['pointcloudList'].currentText())) - self.pointcloud_worker.signals.finished.connect(self.DisplayLoadedPointCloud) + 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.finished.connect(self.DisplayLoadedPointCloud) + self.pointcloud_worker.signals.result.connect(self.DisplayLoadedPointCloud) elif type == "create without loading": #if not self.pointCloudCreated: self.clearPointCloud() self.pointcloud_worker = Worker(self.createPointCloud, filename=filename) self.pointcloud_worker.signals.finished.connect(self.progress_complete) + ff = partial(displayErrorDialogFromWorker, self) + self.pointcloud_worker.signals.error.connect(ff) + self.pointcloud_worker.signals.message.connect(self.updateProgressDialogMessage) self.create_progress_window("Loading", "Loading Pointcloud") self.pointcloud_worker.signals.progress.connect(self.progress) self.progress_window.setValue(10) @@ -2846,8 +2853,16 @@ def reselect_pointcloud(self): self.threadpool.start(self.pointcloud_worker) def progress_complete(self): - #print("FINISHED") - self.progress_window.setValue(100) + try: + self.progress_window.setValue(100) + except NameError as ne: + self.warningDialog(window_title="Warning", message="Error updating progress window progress: {message}".format(message=ne)) + + def updateProgressDialogMessage(self, message): + try: + self.progress_window.setLabelText(message) + except NameError as ne: + self.warningDialog(window_title="Warning", message="Error updating progress window message: {message}".format(message=ne)) def createSavePointCloudWindow(self, save_only): #print("Create save pointcloud window -----------------------------------------------------------------------------------") @@ -2872,7 +2887,9 @@ def createPointCloud(self, **kwargs): ## Create the PointCloud #print("Create point cloud") filename = kwargs.get('filename', "latest_pointcloud.roi") - progress_callback = kwargs.get('progress_callback', None) + progress_callback = kwargs.get('progress_callback', PrintCallback()) + message_callback = kwargs.get('message_callback', PrintCallback()) + subvol_size = kwargs.get('subvol_size',None) # Mask is read from temp file tmpdir = tempfile.gettempdir() @@ -2888,6 +2905,8 @@ def createPointCloud(self, **kwargs): if not self.pointCloudCreated: #print("Not created") pointCloud = cilRegularPointCloudToPolyData() + # attach the progress update callback + pointCloud.AddObserver(vtk.vtkCommand.ProgressEvent, partial(getProgress, progress_callback=progress_callback)) self.pointCloud = pointCloud else: pointCloud = self.pointCloud @@ -2924,14 +2943,16 @@ def createPointCloud(self, **kwargs): pointCloud.SetOverlap(1,float(self.overlapYValueEntry.text())) pointCloud.SetOverlap(2,float(self.overlapZValueEntry.text())) + message_callback.emit('Creating point cloud') pointCloud.Update() self.pointCloud_subvol_size = subvol_size self.pointCloud_overlap = [float(self.overlapXValueEntry.text()), float(self.overlapYValueEntry.text()), float(self.overlapZValueEntry.text())] #print ("pointCloud number of points", pointCloud.GetNumberOfPoints()) - if pointCloud.GetNumberOfPoints() == 0: - return + if pointCloud.GetNumberOfPoints() == 0: + self.pointCloudCreated = False + raise ValueError("No points in pointcloud. Please check your settings and try again.") # Erode the transformed mask because we don't want to have subvolumes outside the mask if not self.pointCloudCreated: @@ -3094,7 +3115,7 @@ def createPointCloud(self, **kwargs): polydata_masker.SetInputConnection(0, t_filter.GetOutputPort()) # polydata_masker.Modified() - + message_callback.emit('Applying mask to pointcloud') polydata_masker.Update() #print("Points in mask now: ", polydata_masker) @@ -3105,8 +3126,8 @@ def createPointCloud(self, **kwargs): array = [] self.pc_no_points = pointcloud.GetNumberOfPoints() if(pointcloud.GetNumberOfPoints() == 0): - self.pointCloud = pointcloud - return (False) + raise ValueError('No points in pointcloud') + if int(mm) == 1: #if point0 is in the mask count = 2 @@ -3124,10 +3145,11 @@ def createPointCloud(self, **kwargs): array.append((count, *pp)) count += 1 + message_callback.emit('Saving pointcloud') np.savetxt(tempfile.tempdir + "/" + filename, array, '%d\t%.3f\t%.3f\t%.3f', delimiter=';') self.roi = filename - return(True) + return True def loadPointCloud(self, *args, **kwargs): @@ -4127,13 +4149,15 @@ def create_config_worker(self): self.config_worker.signals.progress.connect(self.progress) # if single or bulk use the line below, if remote develop new functionality self.config_worker.signals.result.connect(partial (self.run_external_code)) + self.config_worker.signals.message.connect(self.updateProgressDialogMessage) self.threadpool.start(self.config_worker) self.progress_window.setValue(10) def create_run_config(self, **kwargs): os.chdir(tempfile.tempdir) - progress_callback = kwargs.get('progress_callback', None) + progress_callback = kwargs.get('progress_callback', PrintCallback()) + message_callback = kwargs.get('message_callback', PrintCallback()) try: folder_name = self.rdvc_widgets['name_entry'].text() @@ -4194,7 +4218,8 @@ def create_run_config(self, **kwargs): if self.pointcloud_is == 'generated': # we will generate the same pointcloud for each subvolume size pc_subvol_size = self.pointcloud_parameters['pointcloud_size_entry'].text() - if not self.createPointCloud(filename=filename, subvol_size=int(pc_subvol_size)): + if not self.createPointCloud(filename=filename, subvol_size=int(pc_subvol_size), + progress_callback=progress_callback, message_callback=message_callback): return ("pointcloud error") elif self.pointcloud_is == 'loaded': # we will use the file with the pointcloud for each subvol size From 195372aa70836a1d003532634a9a5aac81b2017f Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 30 Nov 2022 15:50:02 +0000 Subject: [PATCH 2/9] adds progress information --- src/idvc/pointcloud_conversion.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/idvc/pointcloud_conversion.py b/src/idvc/pointcloud_conversion.py index a7f6c44b..acaba8fb 100644 --- a/src/idvc/pointcloud_conversion.py +++ b/src/idvc/pointcloud_conversion.py @@ -231,6 +231,8 @@ def CreatePoints2D(self, point_spacing , sliceno, image_data, orientation): # Loop through points in plane bc n_b = offset[0] + # total number of steps + tot = max_b * max_c while n_b < max_b: n_c = offset[1] @@ -253,6 +255,8 @@ def CreatePoints2D(self, point_spacing , sliceno, image_data, orientation): n_c += 1 + self.UpdateProgress((n_c + max_b * n_b )/ tot) + n_b += 1 return 1 @@ -297,6 +301,7 @@ def CreatePoints3D(self, point_spacing , image_data, orientation, sliceno): offset[orientation] = sliceno % point_spacing[orientation] n_x=0 + tot = max_x * max_y * max_z while n_x < max_x: # x axis @@ -312,6 +317,8 @@ def CreatePoints3D(self, point_spacing , image_data, orientation, sliceno): vtkPointCloud.InsertNextPoint( x, y, z ) n_z += 1 + self.UpdateProgress((n_z + max_z * n_y + max_y * max_z * n_x )/ tot) + n_y += 1 n_x += 1 From 387b9471a68c261ab47ca41b86642e3b20f16f4c Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 30 Nov 2022 16:55:15 +0000 Subject: [PATCH 3/9] update changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index e3ca00c7..afbce69c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ * Added button to start tracing * Added estimated time to completion of DVC analysis * Do not allow registration box to extend over the edge of the image (previously this caused the app to crash) +* Added more granular progress update from pointcloud creation step, and bugfixes ## v22.2.0 * Update DVC executable version to v22.0.0 From 570cfcf4a2323dfaf0ec30fd03e4e37a1dbc86da Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 30 Nov 2022 17:07:13 +0000 Subject: [PATCH 4/9] reduce the frequency of progress updates for pointcloud creation --- src/idvc/dvc_interface.py | 16 ++++++++-------- src/idvc/pointcloud_conversion.py | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/idvc/dvc_interface.py b/src/idvc/dvc_interface.py index a84ddf77..0a5951c5 100644 --- a/src/idvc/dvc_interface.py +++ b/src/idvc/dvc_interface.py @@ -2859,14 +2859,14 @@ def PointCloudWorker(self, type, filename = None, disp_file = None, vector_dim = # Show error and allow re-selection of pointcloud if it can't be loaded: search_button = QPushButton('Select Pointcloud') search_button.clicked.connect(self.reselect_pointcloud) - self.e( - '', '', 'This file has been deleted or moved to another location, or cannot be read. Therefore this pointcloud cannot be loaded. \ -Please select a replacement pointcloud file.') - error_title = "READ ERROR" - error_text = "Error reading file: ({filename})".format( - filename=self.roi) - self.pointcloud_worker.signals.error.connect( - lambda: self.displayFileErrorDialog(message=error_text, title=error_title, action_button=search_button)) +# self.e( +# '', '', 'This file has been deleted or moved to another location, or cannot be read. Therefore this pointcloud cannot be loaded. \ +# Please select a replacement pointcloud file.') +# error_title = "READ ERROR" +# error_text = "Error reading file: ({filename})".format( +# filename=self.roi) +# self.pointcloud_worker.signals.error.connect( +# lambda: self.displayFileErrorDialog(message=error_text, title=error_title, action_button=search_button)) # TODO: fix so that closing this window doesn't leave the progress bar going forever def reselect_pointcloud(self): diff --git a/src/idvc/pointcloud_conversion.py b/src/idvc/pointcloud_conversion.py index acaba8fb..ab9ae10e 100644 --- a/src/idvc/pointcloud_conversion.py +++ b/src/idvc/pointcloud_conversion.py @@ -255,9 +255,9 @@ def CreatePoints2D(self, point_spacing , sliceno, image_data, orientation): n_c += 1 - self.UpdateProgress((n_c + max_b * n_b )/ tot) n_b += 1 + self.UpdateProgress(n_b / max_b) return 1 @@ -317,9 +317,8 @@ def CreatePoints3D(self, point_spacing , image_data, orientation, sliceno): vtkPointCloud.InsertNextPoint( x, y, z ) n_z += 1 - self.UpdateProgress((n_z + max_z * n_y + max_y * max_z * n_x )/ tot) - n_y += 1 + self.UpdateProgress((n_z + max_z * n_y + max_y * max_z * n_x )/ tot) n_x += 1 From c1b85c3064bc6a4d0b9fc39ecd3c5187e93f3fe0 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 30 Nov 2022 17:14:40 +0000 Subject: [PATCH 5/9] fix the code for update --- src/idvc/pointcloud_conversion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/idvc/pointcloud_conversion.py b/src/idvc/pointcloud_conversion.py index ab9ae10e..86f997ae 100644 --- a/src/idvc/pointcloud_conversion.py +++ b/src/idvc/pointcloud_conversion.py @@ -255,9 +255,8 @@ def CreatePoints2D(self, point_spacing , sliceno, image_data, orientation): n_c += 1 - n_b += 1 - self.UpdateProgress(n_b / max_b) + self.UpdateProgress((n_c + max_b * n_b ) / tot) return 1 @@ -318,7 +317,7 @@ def CreatePoints3D(self, point_spacing , image_data, orientation, sliceno): n_z += 1 n_y += 1 - self.UpdateProgress((n_z + max_z * n_y + max_y * max_z * n_x )/ tot) + self.UpdateProgress((n_z + max_z * n_y + max_y * max_z * n_x ) / tot) n_x += 1 From 7f4ba4684e3370b7bf6437be14fc747f38abae42 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 30 Nov 2022 17:18:40 +0000 Subject: [PATCH 6/9] remove commented out code [ci skip] --- src/idvc/dvc_interface.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/idvc/dvc_interface.py b/src/idvc/dvc_interface.py index 0a5951c5..6078455f 100644 --- a/src/idvc/dvc_interface.py +++ b/src/idvc/dvc_interface.py @@ -2859,15 +2859,6 @@ def PointCloudWorker(self, type, filename = None, disp_file = None, vector_dim = # Show error and allow re-selection of pointcloud if it can't be loaded: search_button = QPushButton('Select Pointcloud') search_button.clicked.connect(self.reselect_pointcloud) -# self.e( -# '', '', 'This file has been deleted or moved to another location, or cannot be read. Therefore this pointcloud cannot be loaded. \ -# Please select a replacement pointcloud file.') -# error_title = "READ ERROR" -# error_text = "Error reading file: ({filename})".format( -# filename=self.roi) -# self.pointcloud_worker.signals.error.connect( -# lambda: self.displayFileErrorDialog(message=error_text, title=error_title, action_button=search_button)) - # TODO: fix so that closing this window doesn't leave the progress bar going forever def reselect_pointcloud(self): self.progress_complete() From 89b828b5e2f9ebb179bf5a7c72993ffe48812f62 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 30 Nov 2022 23:24:11 +0000 Subject: [PATCH 7/9] make creation of pointcloud more efficient --- src/idvc/dvc_interface.py | 40 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/idvc/dvc_interface.py b/src/idvc/dvc_interface.py index 6078455f..ba098c0c 100644 --- a/src/idvc/dvc_interface.py +++ b/src/idvc/dvc_interface.py @@ -2923,7 +2923,10 @@ def createPointCloud(self, **kwargs): self.pointCloud = pointCloud else: pointCloud = self.pointCloud - + # instead of translating the point cloud so that point0 is in the point cloud + # we add point0 to the point cloud. + if hasattr(self, 'point0'): + pointCloud.SetPoint0(self.point0_world_coords) v = self.vis_widget_2D.frame.viewer orientation = v.getSliceOrientation() pointCloud.SetOrientation(orientation) @@ -3041,6 +3044,7 @@ def createPointCloud(self, **kwargs): # Mask the point cloud with the eroded mask if not self.pointCloudCreated: polydata_masker = cilMaskPolyData() + polydata_masker.AddObserver(vtk.vtkCommand.ProgressEvent, partial(getProgress, progress_callback=progress_callback)) # save reference self.polydata_masker = polydata_masker else: @@ -3089,29 +3093,12 @@ def createPointCloud(self, **kwargs): mm = mask_data.GetScalarComponentAsDouble(int(self.point0_sampled_image_coords[0]),int(self.point0_sampled_image_coords[1]), int(self.point0_sampled_image_coords[2]), 0) - if int(mm) == 1: #if point0 is in the mask - #print("POINT 0 IN MASK") - #Translate pointcloud so that point 0 is in the cloud - if hasattr(self, 'point0'): - pointCloud_points = [] - pointCloud_distances = [] - #print("Point 0: ", self.point0_world_coords) - for i in range (0, pointCloud.GetNumberOfPoints()): - current_point = pointCloud.GetPoints().GetPoint(i) - pointCloud_points.append(current_point) - pointCloud_distances.append((self.point0_world_coords[0]-current_point[0])**2+(self.point0_world_coords[1]-current_point[1])**2+(self.point0_world_coords[2]-current_point[2])**2) - - lowest_distance_index = pointCloud_distances.index(min(pointCloud_distances)) - - #print("The point closest to point 0 is:", pointCloud_points[lowest_distance_index]) - - pointCloud_Translation = (self.point0_world_coords[0]-pointCloud_points[lowest_distance_index][0],self.point0_world_coords[1]-pointCloud_points[lowest_distance_index][1],self.point0_world_coords[2]-pointCloud_points[lowest_distance_index][2]) - - #print("Translation from it is:", pointCloud_Translation) - - transform.Translate(pointCloud_Translation) - #else: - #print("POINT 0 NOT IN MASK") + if int(mm) == 1: + #if point0 is in the mask and it has been added as first point to the point cloud + remove_point0 = False + else: + # should remove point0 from the mask + remove_point0 = True if self.pointCloudCreated: t_filter = self.t_filter @@ -3130,7 +3117,7 @@ def createPointCloud(self, **kwargs): # polydata_masker.Modified() message_callback.emit('Applying mask to pointcloud') polydata_masker.Update() - + message_callback.emit('Applying mask to pointcloud. Done') #print("Points in mask now: ", polydata_masker) self.reader = reader @@ -3159,7 +3146,8 @@ def createPointCloud(self, **kwargs): count += 1 message_callback.emit('Saving pointcloud') - np.savetxt(tempfile.tempdir + "/" + filename, array, '%d\t%.3f\t%.3f\t%.3f', delimiter=';') + start = 0 if remove_point0 is False else 1 + np.savetxt(tempfile.tempdir + "/" + filename, array[start:], '%d\t%.3f\t%.3f\t%.3f', delimiter=';') self.roi = filename return True From f0672db073b63f723726fc07cbadbe7b77d64aa8 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Wed, 30 Nov 2022 23:26:19 +0000 Subject: [PATCH 8/9] add SetPoint0 --- src/idvc/pointcloud_conversion.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/idvc/pointcloud_conversion.py b/src/idvc/pointcloud_conversion.py index 86f997ae..5a15cc95 100644 --- a/src/idvc/pointcloud_conversion.py +++ b/src/idvc/pointcloud_conversion.py @@ -68,10 +68,20 @@ def __init__(self): self._SliceNumber = 0 self._Mode = self.CUBE self._SubVolumeRadius = 1 #: Radius of the subvolume in voxels + self._Point0 = None def GetPoints(self): '''Returns the Points''' return self._Points + + def SetPoint0(self, value): + if not isinstance(value, list): + raise ValueError('Point0 must be a list. Got', value) + if len(value) != 3: + raise ValueError('Point0 must be a list of 3 elements. Got', value) + self._Point0 = value + self.Modified() + def SetMode(self, value): '''Sets the shape mode''' if not value in [self.CIRCLE, self.SQUARE, self.CUBE, self.SPHERE]: @@ -212,6 +222,8 @@ def CreatePoints2D(self, point_spacing , sliceno, image_data, orientation): # print ("dimensions : ", image_dimensions) # print ("point spacing ", point_spacing) + if self._Point0 is not None: + vtkPointCloud.InsertNextPoint(*self._Point0) #label orientation axis as a, with plane being viewed labelled as bc # reduce to 2D on the proper orientation @@ -278,6 +290,9 @@ def CreatePoints3D(self, point_spacing , image_data, orientation, sliceno): image_origin = list ( image_data.GetOrigin() ) image_dimensions = list ( image_data.GetDimensions() ) + if self._Point0 is not None: + vtkPointCloud.InsertNextPoint(*self._Point0) + # the total number of points on X and Y axis max_x = image_dimensions[0] * image_spacing[0] / point_spacing[0] max_y = image_dimensions[1] * image_spacing[1] / point_spacing[1] From e3fabba1fe2d263d709d90920144c491e34cad75 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca Date: Fri, 10 Mar 2023 09:13:27 +0000 Subject: [PATCH 9/9] updated Changes --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 09985212..0805c707 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## vx.x.x * Use os.path.join to create all filepaths, previously in some cases we were forcing "\" or "/" to be in some paths +* More efficient pointcloud creation by not shifting the pointcloud to the make such that point0 is one point of the + created cloud. Point0 is simply added as first point of the cloud even if it does not lie on the regular grid. ## v22.3.0 * Fix bug with size of 'overlap' spinboxes expanding in the vertical direction