diff --git a/doc/_static/example_files/subtractive_gh_v1.gh b/doc/_static/example_files/subtractive_gh_v1.gh new file mode 100644 index 00000000..bdc5feb1 Binary files /dev/null and b/doc/_static/example_files/subtractive_gh_v1.gh differ diff --git a/doc/_static/example_files/subtractive_rh_v1.3dm b/doc/_static/example_files/subtractive_rh_v1.3dm new file mode 100644 index 00000000..a570d244 Binary files /dev/null and b/doc/_static/example_files/subtractive_rh_v1.3dm differ diff --git a/doc/_static/example_files/subtractive_rh_v1.3dmbak b/doc/_static/example_files/subtractive_rh_v1.3dmbak new file mode 100644 index 00000000..0b942da2 Binary files /dev/null and b/doc/_static/example_files/subtractive_rh_v1.3dmbak differ diff --git a/doc/_static/gh_components/img_overview_dfareas.png b/doc/_static/gh_components/img_overview_dfareas.png new file mode 100644 index 00000000..7ac92e74 Binary files /dev/null and b/doc/_static/gh_components/img_overview_dfareas.png differ diff --git a/doc/_static/tutorials/fig_subtractive_detail_viz.png b/doc/_static/tutorials/fig_subtractive_detail_viz.png new file mode 100644 index 00000000..53cf99fd Binary files /dev/null and b/doc/_static/tutorials/fig_subtractive_detail_viz.png differ diff --git a/doc/_static/tutorials/fig_subtractive_graph_viz.png b/doc/_static/tutorials/fig_subtractive_graph_viz.png new file mode 100644 index 00000000..2125db35 Binary files /dev/null and b/doc/_static/tutorials/fig_subtractive_graph_viz.png differ diff --git a/doc/_static/tutorials/fig_subtractive_high_res_overview.png b/doc/_static/tutorials/fig_subtractive_high_res_overview.png new file mode 100644 index 00000000..9b6a869a Binary files /dev/null and b/doc/_static/tutorials/fig_subtractive_high_res_overview.png differ diff --git a/doc/_static/tutorials/fig_subtractive_log.png b/doc/_static/tutorials/fig_subtractive_log.png new file mode 100644 index 00000000..3e497de8 Binary files /dev/null and b/doc/_static/tutorials/fig_subtractive_log.png differ diff --git a/doc/_static/tutorials/fig_subtractive_square.png b/doc/_static/tutorials/fig_subtractive_square.png new file mode 100644 index 00000000..a7cd6948 Binary files /dev/null and b/doc/_static/tutorials/fig_subtractive_square.png differ diff --git a/doc/_static/tutorials/fig_subtractive_start.png b/doc/_static/tutorials/fig_subtractive_start.png new file mode 100644 index 00000000..bf09885b Binary files /dev/null and b/doc/_static/tutorials/fig_subtractive_start.png differ diff --git a/doc/_static/tutorials/fig_subtrative_high_res_2.png b/doc/_static/tutorials/fig_subtrative_high_res_2.png new file mode 100644 index 00000000..92c85a46 Binary files /dev/null and b/doc/_static/tutorials/fig_subtrative_high_res_2.png differ diff --git a/doc/_static/tutorials/fig_subtrative_high_res_3.png b/doc/_static/tutorials/fig_subtrative_high_res_3.png new file mode 100644 index 00000000..b7596aa6 Binary files /dev/null and b/doc/_static/tutorials/fig_subtrative_high_res_3.png differ diff --git a/doc/_static/tutorials/fig_subtrative_high_res_4.png b/doc/_static/tutorials/fig_subtrative_high_res_4.png new file mode 100644 index 00000000..9f5204b4 Binary files /dev/null and b/doc/_static/tutorials/fig_subtrative_high_res_4.png differ diff --git a/doc/_static/tutorials/fig_subtrative_high_res_5.png b/doc/_static/tutorials/fig_subtrative_high_res_5.png new file mode 100644 index 00000000..bb4e9541 Binary files /dev/null and b/doc/_static/tutorials/fig_subtrative_high_res_5.png differ diff --git a/doc/_static/tutorials/fig_subtrative_high_res_6.png b/doc/_static/tutorials/fig_subtrative_high_res_6.png new file mode 100644 index 00000000..48742ebf Binary files /dev/null and b/doc/_static/tutorials/fig_subtrative_high_res_6.png differ diff --git a/doc/_static/tutorials/fig_subtrative_high_res_7.png b/doc/_static/tutorials/fig_subtrative_high_res_7.png new file mode 100644 index 00000000..bc92b212 Binary files /dev/null and b/doc/_static/tutorials/fig_subtrative_high_res_7.png differ diff --git a/doc/segmentation_intro.md b/doc/assembly-evaluation.md similarity index 81% rename from doc/segmentation_intro.md rename to doc/assembly-evaluation.md index 52d9024d..45249ec7 100644 --- a/doc/segmentation_intro.md +++ b/doc/assembly-evaluation.md @@ -1,4 +1,4 @@ -(segmentation_intro)= -# Segmentation +(assemblyeval)= +# Assembly evaluation /// explain with simple example how to use the segmentation components of diffCheck and why we need them. It can be slit in two parts: single element - full structure \ No newline at end of file diff --git a/doc/assembly_intro.md b/doc/assembly_intro.md deleted file mode 100644 index 236bcee5..00000000 --- a/doc/assembly_intro.md +++ /dev/null @@ -1,4 +0,0 @@ -(assembly_intro)= -# DfAssembly - -/// introductio nand example on how to build an assembly with diffCheck (showing also deconstructing and a general scheme of the assembly architecture) \ No newline at end of file diff --git a/doc/compute_error_intro.md b/doc/compute_error_intro.md deleted file mode 100644 index f886abca..00000000 --- a/doc/compute_error_intro.md +++ /dev/null @@ -1,5 +0,0 @@ -(compute_error_intro)= -# Compute cloud-CAD error - -/// tutorial on how to compute the error between a point cloud and a CAD model with diffCheck component. It can be slit in two parts: single element - full structure - diff --git a/doc/image.png b/doc/image.png new file mode 100644 index 00000000..cea5c95f Binary files /dev/null and b/doc/image.png differ diff --git a/doc/index.rst b/doc/index.rst index 3ccf9bbf..3310be26 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -154,7 +154,6 @@ The software is developed by the `Laboratory of Timber Construction (IBOIS)`_ an :maxdepth: 2 :caption: Getting Started - installation quickstart tutorials diff --git a/doc/installation.md b/doc/installation.md deleted file mode 100644 index 279e6925..00000000 --- a/doc/installation.md +++ /dev/null @@ -1,21 +0,0 @@ -(installation)= -# Installing diffCheck - -Using `diffCheck` requires *Rhino* and *Grasshopper*. The plug-in can be installed simply by: - -* Open *Rhino* and tpe on the command bar: - ``` - _PackageManager - ``` -* search for `diffCheck` and click on install. -* launch *Grasshopper* you will find the a new tab in the toolbar. -* drop on the canvas the [test component](gh_DFTester) and connect it to a point cloud and a CAD model. - -

- -

- - -```{important} -For now, diffCheck is only supported on Windows ❖. \ No newline at end of file diff --git a/doc/joints-evaluation.md b/doc/joints-evaluation.md new file mode 100644 index 00000000..0f2942f8 --- /dev/null +++ b/doc/joints-evaluation.md @@ -0,0 +1,168 @@ +(jointeval)= +# Joints evaluation + +## Objective + +The following tutorial will guide you through the evaluation of joints in timber construction with DF. As a studycase we fabricated 3 beams presenting the same joints with: + +- a chainsaw, by hand +- a circular saw, by hand +- a CNC Maka machine + +the goal being to evaluate the quality of the joints and the accuracy of the fabrication processes. In DF the evaluation of the joint is dual: + +- i) the *evaluation of the joint itself*: by comparing the CAD model of the joint with the scan of the joint informs us about the quality of the positioning of the joints on the beam. +- ii) the *evaluation of the joint's faces*: by comparing the CAD model of the joint's faces with the scan of the joint informs us about the quality of the fabrication of the joint itself. + +In the following we will see how to quantify and obtain data for these two metrics. + +
+ +```{eval-rst} +.. raw:: html + + + + + + Download .gh file + +``` + + +--- + +## Steps + +### 1. Input the data +First things first, let's import your cleaned scan and corresponding polysurface model in Rhino. + +

+ +

+ +### 2. Build the DFAssembly +Here we convert the model of our structure into the internal datatype of diffcheck, DFAssembly. This component detects the joints and their faces. + +

+ +

+ +```{hint} +If you are evaluating round sections e.g. logs, you can set the `i_is_roundwood` input to `True` in the `DFBuildAssembly` component. This will allow DF to detect automatically the joints on the roundwood. + +

+ +

+``` + +> DF's components: +> * [`DFAssebmly`](gh_DFBuildAssembly) + +### 3. Registration of CAD and scan +The registration is the process of aligning the CAD model with the scan. This is done by selecting corresponding points on the CAD model and the scan and find a transformation that minimizes the distance between them. + +

+ +

+ +> DF's components: +> * [`DFBrepToCloud`](gh_DFBrepToCloud) +> * [`DFCloudVoxelDownsample`](gh_DFCloudVoxelDownsample) +> * [`DFCloudNormalEstimator`](gh_DFCloudNormalEstimator) +> * [`DFRANSACGlobalRegistration`](gh_DFRANSACGlobalRegistration) +> * [`DFICPRegistration`](gh_DFICPRegistration) + +### 4. Segmentation of the scan +Once the scan and the CAD model are aligned, we can segment the scan to isolate the parts of the raw point cloud of the scan that corresponds tothe joints. + +

+ +

+ +> DF's components: +> * [`DFCloudNormalSegmentator`](gh_DFCloudNormalSegmentator) +> * [`DFRemoveStatisticalOutliers`](gh_DFRemoveStatisticalOutliers) +> * [`DFJointSegmentator`](gh_DFJointSegmentator) +> * [`DFColorizeCloud`](gh_DFColorizeCloud) + +### 6. Error computation +At this point we can compute the error between the CAD model and the scan. The error is computed as the distance between the closest point on the CAD model and the scan. The current DF's output metrics are: + +* *distance* : the distance between the closest point on the CAD model and the scan +* *mean* : the mean distance between the closest point on the CAD model and the scan +* *max_deviation* : the maximum distance between the closest point on the CAD model and the scan +* *min_deviation* : the minimum distance between the closest point on the CAD model and the scan +* *std_deviation* : the standard deviation of the distance between the closest point on the CAD model and the scan + +

+ +

+ +> DF's components: +> * [`DFCloudMeshDistance`](gh_DFCloudMeshDistance) + +### 7. Error Visulization +DF allows you to quickly visualize the errors in the Rhino viewport. The color of the points represents the distance between the CAD model and the scan. The color scale can be adjusted to better visualize the error. We also provide a graph that shows the distribution of the errors. + +

+ +

+ +
+
+ subtr detail +
View on the visualization of the analysed clouds on the CAD model itself. To not that only the points considered as valid are considered for the analysis.
+
+
+ subtr graph +
View of the graph of the corresponding distribution of the total error directly in Rhino.
+
+
+ +> DF's components: +> * [`DFVisualizationSettings`](gh_DFVisualizationSettings) +> * [`DFVisualization`](gh_DFVisualization) + +### 8. Export the results +The results can be also exported in a CSV file for further analysis or documentation. + +

+ +

+ +CSV can be exporting the value per joint.. + +| Joint ID | Min Deviation | Max Deviation | Std Deviation | RMSE | +|----------|---------------|---------------|---------------|-------| +| 0--0--0 | 0 | 0.006 | 0.0015 | 0.0023| +| 0--1--0 | 0 | 0.0064 | 0.0011 | 0.0024| +| 0--2--0 | 0.0001 | 0.0091 | 0.0019 | 0.0028| +| 0--3--0 | 0 | 0.0061 | 0.0012 | 0.0018| +| 0--4--0 | 0.0001 | 0.0062 | 0.0009 | 0.0021| + +.. or per face + +| Joint Face ID | Min Deviation | Max Deviation | Std Deviation | RMSE | Mean | +|---------------|---------------|---------------|---------------|-------|-------| +| 0--0--0 | 0 | 0.0032 | 0.0006 | 0.0009| 0.0007| +| 0--0--1 | 0.2933 | 0.6587 | 0.1164 | 0.4882| 0.4741| +| 0--0--2 | MISSING_PCD | MISSING_PCD | MISSING_PCD | MISSING_PCD| MISSING_PCD| +| 0--0--3 | MISSING_PCD | MISSING_PCD | MISSING_PCD | MISSING_PCD| MISSING_PCD| +| 0--0--4 | MISSING_PCD | MISSING_PCD | MISSING_PCD | MISSING_PCD| MISSING_PCD| +| 0--0--5 | MISSING_PCD | MISSING_PCD | MISSING_PCD | MISSING_PCD| MISSING_PCD| +| 0--0--6 | 0.2602 | 0.3317 | 0.0171 | 0.2991| 0.2986| +| 0--0--7 | 0.1829 | 0.2453 | 0.0176 | 0.2076| 0.2069| +| 0--0--8 | 0.0107 | 0.2884 | 0.0897 | 0.1512| 0.1217| +| ... | ... | ... | ... | ...| ... | + +> DF's components: +> * [`DFCsvExporter`](gh_DFCsvExporter) \ No newline at end of file diff --git a/doc/key-concepts.md b/doc/key-concepts.md index 3b10c370..e007f15b 100644 --- a/doc/key-concepts.md +++ b/doc/key-concepts.md @@ -1,4 +1,45 @@ (key-concepts)= # Key-concepts -/// add description here about the general structure of diffCheck with schemes and general functioning \ No newline at end of file +logo + +## A modular structure + +To make DF broadly applicable and allow users to customize their evaluation pipelines, we designed the plugin with a modular structure. By assembling and combining different components, users can tailor their workflows to fit specific project needs. + +

+ +

+ +By combining different components, users can create a wide range of evaluation pipelines. The plugin includes components for: + +- **Data preparation**: Import and preprocess data from different sources. +- **Data analysis**: Compute metrics and visualize results. +- **Data manipulation**: Arrange and cluster scans or convert one datatype (e.g. brep) to another (e.g. cloud). +- **Data export**: Export results to different formats. + +Have a look at [our documentation for all the components](gh_components). + +logo + +## Evaluation pipelines + +In DF, evaluation pipelines to compare CAD and scans are *sequences of components that process data from input to output. They can be as simple as a single component or as complex as a chain of multiple components.* Normally it is composed of the following steps: + +```{mermaid} +flowchart LR + A{Data input} --> + B(Data cleaning) --> + C(Registration) --> + D(Error Computation) --> + E{Data export} + D --> F[Visualization] +``` + +To show how this works in DF, we provide two examples of evaluation pipelines we have done for timber construction by combining different components: + +- [Evaluation of joins in timber construction](joints-evaluation) +- [Evaluation of assembled structures in timber construction](assembly-evaluation) + +> You can create your own evaluation pipelines by combining different components and let us know if you want to share them with the community or you need a specific component that is not available yet, send an email to [diffcheckorg@gmail.com](mailto:diffcheckorg@gmail.com)! \ No newline at end of file diff --git a/doc/pre-processing.md b/doc/pre-processing.md deleted file mode 100644 index ba8d693e..00000000 --- a/doc/pre-processing.md +++ /dev/null @@ -1,4 +0,0 @@ -(pre-processing)= -# Pre-processing - -/// a brief intro on how to use the pre-processing section to clean the scanned point cloud \ No newline at end of file diff --git a/doc/quickstart.md b/doc/quickstart.md index 68954dd1..5e980c72 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -1,4 +1,21 @@ (qstart)= # Quickstart -/// a brief rer-intro on how to install and a basic example of a working full and small pipeline of diffCheck \ No newline at end of file +Using `diffCheck` requires *Rhino* and *Grasshopper*. The plug-in can be installed simply by: + +* Open *Rhino* and tpe on the command bar: + ``` + _PackageManager + ``` +* search for `diffCheck` and click on install. +* launch *Grasshopper* you will find the a new tab in the toolbar. +* drop on the canvas the [test component](gh_DFTester) and connect it to a point cloud and a CAD model. + +

+ +

+ + +```{important} +For now, diffCheck is only supported on Windows ❖. \ No newline at end of file diff --git a/doc/tutorials.rst b/doc/tutorials.rst index dec17d25..cd093a16 100644 --- a/doc/tutorials.rst +++ b/doc/tutorials.rst @@ -8,7 +8,5 @@ diffCheck tutorials quickstart key-concepts - pre-processing - assembly_intro - segmentation_intro - compute_error_intro \ No newline at end of file + joints-evaluation + assembly-evaluation \ No newline at end of file diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index 3d09b8a6..84a71244 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -179,13 +179,13 @@ namespace diffCheck::segmentation } if (correspondingSegment == nullptr) { - DIFFCHECK_WARN("No segment found for the face. Skipping the face."); + DIFFCHECK_WARN("No segment found for the face. Returning an empty point cloud for this face."); + faceSegments.push_back(facePoints); continue; } bool hasColors = correspondingSegment->GetNumColors() > 0; for (Eigen::Vector3d point : correspondingSegment->Points) { - bool pointInFace = false; if (face->IsPointOnFace(point, associationThreshold)) { facePoints->Points.push_back(point); @@ -279,7 +279,8 @@ namespace diffCheck::segmentation if (correspondingSegment == nullptr) { - DIFFCHECK_WARN("No segment found for the face. Skipping the face."); + DIFFCHECK_WARN("No segment found for the face. Returning an empty point cloud for this face."); + faceSegments.push_back(facePoints); continue; } bool hasColors = correspondingSegment->GetNumColors() > 0; diff --git a/src/gh/components/DF_build_assembly/code.py b/src/gh/components/DF_build_assembly/code.py index 0761d778..0be4fa94 100644 --- a/src/gh/components/DF_build_assembly/code.py +++ b/src/gh/components/DF_build_assembly/code.py @@ -16,6 +16,10 @@ def RunScript(self, i_breps : System.Collections.Generic.IList[Rhino.Geometry.Brep], i_is_roundwood : bool): beams: typing.List[DFBeam] = [] + + if i_assembly_name is None or i_breps is None: + return None + if i_is_roundwood is None: i_is_roundwood = False diff --git a/src/gh/components/DF_cloud_cloud_distance/code.py b/src/gh/components/DF_cloud_cloud_distance/code.py index f42cb471..494c2959 100644 --- a/src/gh/components/DF_cloud_cloud_distance/code.py +++ b/src/gh/components/DF_cloud_cloud_distance/code.py @@ -34,4 +34,4 @@ def RunScript(self, # calculate distances o_results = df_error_estimation.df_cloud_2_df_cloud_comparison(df_cloud_source_list, df_cloud_target_list) - return o_results.distances, o_results.distances_rmse, o_results.distances_max_deviation, o_results.distances_min_deviation, o_results.distances_sd_deviation, o_results + return o_results.distances, o_results.distances_mean, o_results.distances_rmse, o_results.distances_max_deviation, o_results.distances_min_deviation, o_results.distances_sd_deviation, o_results diff --git a/src/gh/components/DF_cloud_cloud_distance/metadata.json b/src/gh/components/DF_cloud_cloud_distance/metadata.json index 9471eda2..27e3c8da 100644 --- a/src/gh/components/DF_cloud_cloud_distance/metadata.json +++ b/src/gh/components/DF_cloud_cloud_distance/metadata.json @@ -59,6 +59,14 @@ "sourceCount": 0, "graft": false }, + { + "name": "o_mean", + "nickname": "o_mean", + "description": "The mean of the distances.", + "optional": false, + "sourceCount": 0, + "graft": false + }, { "name": "o_rmse", "nickname": "o_rmse", diff --git a/src/gh/components/DF_cloud_mesh_distance/code.py b/src/gh/components/DF_cloud_mesh_distance/code.py index 8e6a8c97..9acb1e5a 100644 --- a/src/gh/components/DF_cloud_mesh_distance/code.py +++ b/src/gh/components/DF_cloud_mesh_distance/code.py @@ -1,25 +1,23 @@ -"""Computes the distance between a point cloud and a mesh""" #! python3 -# r: diffCheck==1.0.0 +import System import Rhino -import Grasshopper from ghpythonlib.componentbase import executingcomponent as component from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML +import ghpythonlib.treehelpers as th import diffCheck -from diffCheck import df_cvt_bindings from diffCheck import df_error_estimation class DFCloudMeshDistance(component): def RunScript(self, - i_cloud_source: Grasshopper.DataTree[Rhino.Geometry.PointCloud], - i_assembly, - i_signed_flag: bool, - i_swap: bool, - i_analysis_resolution: float): + i_cloud_source: System.Collections.Generic.List[Rhino.Geometry.PointCloud], + i_assembly, + i_signed_flag: bool, + i_swap: bool, + i_analysis_resolution: float): if i_cloud_source is None or i_assembly is None: return None, None, None, None, None, None @@ -28,37 +26,29 @@ def RunScript(self, scalef = diffCheck.df_util.get_doc_2_meters_unitf() i_analysis_resolution = 0.1 / scalef - # if the input is Gh tree, flatten it - flat_list = [] - for branch in i_cloud_source.Branches: - flat_list.extend(list(branch)) - i_cloud_list = flat_list - - # Based on cloud source input + beam size, decide whether to calculate joints or entire assembly and output respective message - if len(i_assembly.beams) == len(i_cloud_list): + if len(i_assembly.beams) == len(i_cloud_source): ghenv.Component.Message = "Per Beam" # noqa: F821 rh_mesh_target_list = [beam.to_mesh(i_analysis_resolution) for beam in i_assembly.beams] - elif len(i_assembly.all_joints) == len(i_cloud_list): + elif len(i_assembly.all_joints) == len(i_cloud_source): ghenv.Component.Message = "Per Joint" # noqa: F821 rh_mesh_target_list = [joint.to_mesh(i_analysis_resolution) for joint in i_assembly._all_joints] - elif len(i_assembly.all_joint_faces) == len(i_cloud_list): + elif len(i_assembly.all_joint_faces) == len(i_cloud_source): ghenv.Component.Message = "Per Joint Face" # noqa: F821 - rh_mesh_target_list = [joint_face.to_mesh() for joint_face in i_assembly.all_joint_faces] + rh_mesh_target_list = [] + for joint in i_assembly._all_joints: + for face in joint.faces: + rh_mesh_target_list.append(face.to_mesh()) else: ghenv.Component.AddRuntimeMessage(RML.Warning, "The input number of objects to compare matches neither the number of beams nor the number of joints") # noqa: F821 return None, None, None, None, None, None - #conversion to DFCloud - df_cloud_source_list = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(rh_cloud) for rh_cloud in i_cloud_list] - - # calculate distances - o_result = df_error_estimation.df_cloud_2_rh_mesh_comparison(i_assembly, df_cloud_source_list, rh_mesh_target_list, i_signed_flag, i_swap) # noqa: F821 + o_result = df_error_estimation.rh_cloud_2_rh_mesh_comparison( + i_assembly, + i_cloud_source, + rh_mesh_target_list, + i_signed_flag, + i_swap) # noqa: F821 - # distances to tree - distances_tree = Grasshopper.DataTree[object]() - for i, sublist in enumerate(o_result.distances): - for j, item in enumerate(sublist): - path = Grasshopper.Kernel.Data.GH_Path(i) - distances_tree.Add(item, path) + distances_gh_tree = th.list_to_tree(o_result.distances) - return distances_tree, o_result.distances_rmse, o_result.distances_max_deviation, o_result.distances_min_deviation, o_result.distances_sd_deviation, o_result + return distances_gh_tree, o_result.distances_mean, o_result.distances_rmse, o_result.distances_max_deviation, o_result.distances_min_deviation, o_result.distances_sd_deviation, o_result diff --git a/src/gh/components/DF_cloud_mesh_distance/metadata.json b/src/gh/components/DF_cloud_mesh_distance/metadata.json index f8eb1d68..1216ce73 100644 --- a/src/gh/components/DF_cloud_mesh_distance/metadata.json +++ b/src/gh/components/DF_cloud_mesh_distance/metadata.json @@ -20,10 +20,11 @@ "optional": true, "allowTreeAccess": true, "showTypeHints": true, - "scriptParamAccess": "tree", + "scriptParamAccess": "list", "wireDisplay": "default", "sourceCount": 0, - "typeHintID": "pointcloud" + "typeHintID": "pointcloud", + "flatten": true }, { "name": "i_assembly", @@ -83,6 +84,14 @@ "sourceCount": 0, "graft": false }, + { + "name": "o_mean", + "nickname": "o_mean", + "description": "The mean of the distances.", + "optional": false, + "sourceCount": 0, + "graft": false + }, { "name": "o_rmse", "nickname": "o_rmse", diff --git a/src/gh/components/DF_cloud_normal_estimator/code.py b/src/gh/components/DF_cloud_normal_estimator/code.py index 38f6743a..fac8533b 100644 --- a/src/gh/components/DF_cloud_normal_estimator/code.py +++ b/src/gh/components/DF_cloud_normal_estimator/code.py @@ -13,6 +13,9 @@ def RunScript(self, i_switch_mode: bool): o_cloud = Rhino.Geometry.PointCloud() + if i_cloud is None: + return None + df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(i_cloud) if i_knn is None: diff --git a/src/gh/components/DF_cloud_size_downsample/code.py b/src/gh/components/DF_cloud_size_downsample/code.py index 57eabccc..362e0cf9 100644 --- a/src/gh/components/DF_cloud_size_downsample/code.py +++ b/src/gh/components/DF_cloud_size_downsample/code.py @@ -8,7 +8,11 @@ from diffCheck import df_cvt_bindings class DFCloudSizeDownsample(component): - def RunScript(self, i_cloud: Rhino.Geometry.PointCloud, i_size: float) -> Rhino.Geometry.PointCloud: + def RunScript(self, + i_cloud: Rhino.Geometry.PointCloud, + i_size: float) -> Rhino.Geometry.PointCloud: + if i_cloud is None or i_size is None: + return None df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(i_cloud) df_cloud.downsample_by_size(i_size) o_cloud = df_cvt_bindings.cvt_dfcloud_2_rhcloud(df_cloud) diff --git a/src/gh/components/DF_cloud_uniform_downsample/code.py b/src/gh/components/DF_cloud_uniform_downsample/code.py index ec0cfdc4..73d40419 100644 --- a/src/gh/components/DF_cloud_uniform_downsample/code.py +++ b/src/gh/components/DF_cloud_uniform_downsample/code.py @@ -8,7 +8,11 @@ from diffCheck import df_cvt_bindings class DFCloudUniformDownsample(component): - def RunScript(self, i_cloud: Rhino.Geometry.PointCloud, i_every_k_points: int) -> Rhino.Geometry.PointCloud: + def RunScript(self, + i_cloud: Rhino.Geometry.PointCloud, + i_every_k_points: int) -> Rhino.Geometry.PointCloud: + if i_cloud is None or i_every_k_points is None: + return None df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(i_cloud) df_cloud.uniform_downsample(i_every_k_points) o_cloud = df_cvt_bindings.cvt_dfcloud_2_rhcloud(df_cloud) diff --git a/src/gh/components/DF_cloud_voxel_downsample/code.py b/src/gh/components/DF_cloud_voxel_downsample/code.py index 2544ba62..e4370add 100644 --- a/src/gh/components/DF_cloud_voxel_downsample/code.py +++ b/src/gh/components/DF_cloud_voxel_downsample/code.py @@ -8,7 +8,11 @@ from diffCheck import df_cvt_bindings class DFCloudVoxelDownsample(component): - def RunScript(self, i_cloud: Rhino.Geometry.PointCloud, i_voxel_size: float) -> Rhino.Geometry.PointCloud: + def RunScript(self, + i_cloud: Rhino.Geometry.PointCloud, + i_voxel_size: float) -> Rhino.Geometry.PointCloud: + if i_cloud is None or i_voxel_size is None: + return None df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(i_cloud) df_cloud.voxel_downsample(i_voxel_size) o_cloud = df_cvt_bindings.cvt_dfcloud_2_rhcloud(df_cloud) diff --git a/src/gh/components/DF_colorize_cloud/code.py b/src/gh/components/DF_colorize_cloud/code.py index 4beab843..a72a31d2 100644 --- a/src/gh/components/DF_colorize_cloud/code.py +++ b/src/gh/components/DF_colorize_cloud/code.py @@ -7,8 +7,13 @@ import numpy as np + class DFColorizeCloud(component): def RunScript(self, i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud]): + + if i_clouds is None: + return None + for cloud in i_clouds: random_color = System.Drawing.Color.FromArgb( np.random.randint(0, 255), diff --git a/src/gh/components/DF_csv_exporter/code.py b/src/gh/components/DF_csv_exporter/code.py index 287f5ce5..0f1e4ca0 100644 --- a/src/gh/components/DF_csv_exporter/code.py +++ b/src/gh/components/DF_csv_exporter/code.py @@ -3,12 +3,17 @@ from ghpythonlib.componentbase import executingcomponent as component -from diffCheck.df_error_estimation import DFVizResults +from diffCheck.df_error_estimation import DFInvalidData import csv import os class DFCsvExporter(component): + def __init__(self): + super(DFCsvExporter, self).__init__() + self.prefix = "" + self.counter = 0 + def _get_id(self, idx, i_result): """ Get the ID of the element """ counter = 0 @@ -19,7 +24,7 @@ def _get_id(self, idx, i_result): for idx_b, beam in enumerate(i_result.assembly.beams): for idx_j, joint in enumerate(beam.joints): if counter == idx: - return f"{idx_b}--{idx_b}--{idx_j}" + return f"{idx_b}--{idx_j}--{0}" counter += 1 elif self.prefix == "joint_face": for idx_b, beam in enumerate(i_result.assembly.beams): @@ -33,25 +38,30 @@ def _write_csv(self, file_path, rows): """ Write the CSV file """ with open(file_path, mode='w', newline='') as file: writer = csv.writer(file) - writer.writerow([f"{self.prefix} id", "distances", "min_deviation", "max_deviation", "std_deviation", "rmse"]) + writer.writerow([f"{self.prefix} id", "distances", "min_deviation", "max_deviation", "std_deviation", "rmse", "mean"]) writer.writerows(rows) def _prepare_row(self, idx, i_result): """ Prepare a row for the CSV file """ + if i_result.sanity_check[idx].value != DFInvalidData.VALID.value: + invalid_type = i_result.sanity_check[idx].name + return [self._get_id(idx, i_result), invalid_type, invalid_type, invalid_type, invalid_type, invalid_type, invalid_type] + distances = [round(value, 4) for value in i_result.distances[idx]] min_dev = round(i_result.distances_min_deviation[idx], 4) max_dev = round(i_result.distances_max_deviation[idx], 4) std_dev = round(i_result.distances_sd_deviation[idx], 4) rmse = round(i_result.distances_rmse[idx], 4) + mean = round(i_result.distances_mean[idx], 4) distances_str = ";".join(map(str, distances)) - return [self._get_id(idx, i_result), distances_str, min_dev, max_dev, std_dev, rmse] + return [self._get_id(idx, i_result), distances_str, min_dev, max_dev, std_dev, rmse, mean] def RunScript(self, - i_dump: bool, - i_export_dir: str, - i_file_name: str, - i_export_seperate_files: bool, - i_result: DFVizResults): + i_dump: bool, + i_export_dir: str, + i_file_name: str, + i_export_seperate_files: bool, + i_result): if i_dump: os.makedirs(i_export_dir, exist_ok=True) diff --git a/src/gh/components/DF_joint_segmentator/code.py b/src/gh/components/DF_joint_segmentator/code.py index 210e9ccb..0cb1a3aa 100644 --- a/src/gh/components/DF_joint_segmentator/code.py +++ b/src/gh/components/DF_joint_segmentator/code.py @@ -1,6 +1,8 @@ +"""Extracts the joints from a point cloud.""" #! python3 import System +import math import Rhino import ghpythonlib.treehelpers @@ -21,7 +23,8 @@ def RunScript(self, i_assembly, i_angle_threshold: float, i_distance_threshold: float, - i_correspondence_distance: float,): + i_correspondence_distance: float, + i_joint_displacement_tolerance: float): if i_clusters is None or i_assembly is None: return None @@ -31,31 +34,46 @@ def RunScript(self, i_distance_threshold = 0.1 if i_correspondence_distance is None: i_correspondence_distance = 0.005 + if i_joint_displacement_tolerance is None: + i_joint_displacement_tolerance =0.05 if len(i_clusters) == 0: raise ValueError("No clusters given.") if not isinstance(i_clusters[0], Rhino.Geometry.PointCloud): raise ValueError("The input clusters must be PointClouds.") # get number of joints - n_joints = i_assembly.all_joints + n_joints = i_assembly.total_number_joints # prepping the reference meshes df_joints = [[] for _ in range(n_joints)] + rh_joints = [[] for _ in range(n_joints)] for joint in i_assembly.all_joints: for face in joint.faces: face = face.to_mesh() face.Subdivide() face.Faces.ConvertQuadsToTriangles() + rh_joints[joint.id].append(face) df_joints[joint.id].append(df_cvt.cvt_rhmesh_2_dfmesh(face)) - o_reference_point_clouds = [] o_joint_faces_segments = [] df_cloud_clusters = [df_cvt.cvt_rhcloud_2_dfcloud(cluster) for cluster in i_clusters] df_joint_clouds = [] + o_joint_segments = [] + + # compute the center of the joints + rh_joint_centers = [] + for rh_joint in rh_joints: + vertices = [] + for face in rh_joint: + for vertice in face.Vertices: + vertices.append(Rhino.Geometry.Point3d(vertice.X, vertice.Y, vertice.Z)) + joint_center = Rhino.Geometry.BoundingBox(vertices).Center + rh_joint_centers.append([joint_center.X, joint_center.Y, joint_center.Z]) # for each joint, find the corresponding faces, store them as such but also merge them, generate a reference point cloud, and register the merged clusters to the reference point cloud - for df_joint in df_joints: + for i, df_joint in enumerate(df_joints): rh_joint_faces_segments = [] + reference_joint_center = rh_joint_centers[i] # create the reference point cloud ref_df_joint_cloud = diffcheck_bindings.dfb_geometry.DFPointCloud() @@ -66,9 +84,37 @@ def RunScript(self, # find the corresponding clusters and merge them df_joint_cloud = diffcheck_bindings.dfb_geometry.DFPointCloud() - df_joint_face_segments = diffcheck_bindings.dfb_segmentation.DFSegmentation.associate_clusters(i_assembly.contains_cylinders, df_joint, df_cloud_clusters, i_angle_threshold, i_distance_threshold) + df_joint_face_segments = diffcheck_bindings.dfb_segmentation.DFSegmentation.associate_clusters(False, df_joint, df_cloud_clusters, i_angle_threshold, i_distance_threshold) for df_joint_face_segment in df_joint_face_segments: - df_joint_cloud.add_points(df_joint_face_segment) + df_joint_cloud.add_points(df_joint_face_segment) + + # get the center of the segment + if len(df_joint_cloud.points)>0: + df_cloud_bb_points = df_joint_cloud.get_tight_bounding_box() + x, y, z = 0, 0, 0 + for i in range(len(df_cloud_bb_points)): + x += df_cloud_bb_points[i][0] + y += df_cloud_bb_points[i][1] + z += df_cloud_bb_points[i][2] + x = x/8 # because a bb has 8 corners + y = y/8 + z = z/8 + segment_center = [x, y, z] + segment_dist_to_ref = math.sqrt(math.pow(segment_center[0]-reference_joint_center[0], 2) + + math.pow(segment_center[1]-reference_joint_center[1], 2) + + math.pow(segment_center[2]-reference_joint_center[2], 2)) + if segment_dist_to_ref > i_joint_displacement_tolerance: + rh_joint_cloud = df_cvt.cvt_dfcloud_2_rhcloud(df_joint_cloud) + rh_joint_cloud.SetUserString("df_sanity_scan_check", "1") + o_joint_segments.append(rh_joint_cloud) + else: + rh_joint_cloud = df_cvt.cvt_dfcloud_2_rhcloud(df_joint_cloud) + rh_joint_cloud.SetUserString("df_sanity_scan_check", "0") + o_joint_segments.append(rh_joint_cloud) + else: + rh_joint_cloud = df_cvt.cvt_dfcloud_2_rhcloud(df_joint_cloud) + rh_joint_cloud.SetUserString("df_sanity_scan_check", "2") + o_joint_segments.append(rh_joint_cloud) # register the joint faces to the reference point cloud transform = diffcheck_bindings.dfb_registrations.DFRefinedRegistration.O3DICP(df_joint_cloud, ref_df_joint_cloud, max_correspondence_distance = i_correspondence_distance) @@ -78,7 +124,9 @@ def RunScript(self, df_joint_clouds.append(df_joint_cloud) o_joint_faces_segments.append(rh_joint_faces_segments) - o_joint_segments = [df_cvt.cvt_dfcloud_2_rhcloud(df_joint_cloud) for df_joint_cloud in df_joint_clouds] + for rh_joint_faces, rh_joint in zip(o_joint_faces_segments, o_joint_segments): + for joint_face in rh_joint_faces: + joint_face.SetUserString("df_sanity_scan_check", rh_joint.GetUserString("df_sanity_scan_check")) o_gh_tree_joint_faces_segments = ghpythonlib.treehelpers.list_to_tree(o_joint_faces_segments) diff --git a/src/gh/components/DF_joint_segmentator/metadata.json b/src/gh/components/DF_joint_segmentator/metadata.json index 8cc16a32..3738f598 100644 --- a/src/gh/components/DF_joint_segmentator/metadata.json +++ b/src/gh/components/DF_joint_segmentator/metadata.json @@ -72,6 +72,18 @@ "wireDisplay": "default", "sourceCount": 0, "typeHintID": "float" + }, + { + "name": "i_joint_displacement_tolerance", + "nickname": "i_joint_displacement_tolerance", + "description": "The maximum distance for a joint segment to be considered as valid for the reference we are comparing to. The default value is 0.05 (5 cm).", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "float" } ], "outputParameters": [ diff --git a/src/gh/components/DF_normal_segmentator/code.py b/src/gh/components/DF_normal_segmentator/code.py index a63c6dda..bd6823fa 100644 --- a/src/gh/components/DF_normal_segmentator/code.py +++ b/src/gh/components/DF_normal_segmentator/code.py @@ -19,6 +19,10 @@ def RunScript(self, i_knn_neighborhood_size=None, i_radius_neighborhood_size=None ) -> Rhino.Geometry.PointCloud: + + if i_cloud is None: + return None + o_clusters = [] df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(i_cloud) diff --git a/src/gh/components/DF_preview_assembly/code.py b/src/gh/components/DF_preview_assembly/code.py index b5546ec0..7a99cebe 100644 --- a/src/gh/components/DF_preview_assembly/code.py +++ b/src/gh/components/DF_preview_assembly/code.py @@ -11,7 +11,6 @@ from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML import diffCheck -from diffCheck.df_geometries import DFAssembly import diffCheck.diffcheck_bindings import diffCheck.df_util @@ -68,10 +67,7 @@ def __init__(self): input_indx, X_cord, Y_cord) - def RunScript(self, - i_assembly: DFAssembly=None, - i_are_joints_visible: bool=None - ): + def RunScript(self, i_assembly, i_are_joints_visible: bool): if i_assembly is None: return None if i_are_joints_visible is None: @@ -90,55 +86,62 @@ def RunScript(self, # Preview overrides def DrawViewportWires(self, args): - for beam in self._dfassembly.beams: + if self._dfassembly is None: + return + for idx_beam, beam in enumerate(self._dfassembly.beams): ####################################### ## DFBeams ####################################### - # beams' obb - df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() - vertices_pt3d_rh : typing.List[rg.Point3d] = [vertex.to_rg_point3d() for vertex in beam.vertices] - df_cloud.points = [np.array([vertex.X, vertex.Y, vertex.Z]).reshape(3, 1) for vertex in vertices_pt3d_rh] - obb: rg.Brep = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) - # args.Display.DrawBrepWires(obb, System.Drawing.Color.Red) ## keep for debugging - - # axis arrow - obb_faces = obb.Faces - obb_faces = sorted(obb_faces, key=lambda face: rg.AreaMassProperties.Compute(face).Area) - obb_endfaces = obb_faces[:2] - beam_axis = rg.Line(obb_endfaces[0].GetBoundingBox(True).Center, obb_endfaces[1].GetBoundingBox(True).Center) - extension_length = 0.5 * diffCheck.df_util.get_doc_2_meters_unitf() - beam_axis.Extend(extension_length, extension_length) - args.Display.DrawArrow(beam_axis, System.Drawing.Color.Magenta) - - # beam assembly index - anchor_pt: rg.Point3d = beam_axis.From - beam_axis.UnitTangent * 0.5 * extension_length - args.Display.Draw2dText( - str(beam.index_assembly), - System.Drawing.Color.Violet, - anchor_pt, - True, 18) + if len(self._dfassembly.beams) > 1: + # beams' obb + df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud() + vertices_pt3d_rh : typing.List[rg.Point3d] = [vertex.to_rg_point3d() for vertex in beam.vertices] + df_cloud.points = [np.array([vertex.X, vertex.Y, vertex.Z]).reshape(3, 1) for vertex in vertices_pt3d_rh] + obb: rg.Brep = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box()) + # args.Display.DrawBrepWires(obb, System.Drawing.Color.Red) ## keep for debugging + + # axis arrow + obb_faces = obb.Faces + obb_faces = sorted(obb_faces, key=lambda face: rg.AreaMassProperties.Compute(face).Area) + obb_endfaces = obb_faces[:2] + beam_axis = rg.Line(obb_endfaces[0].GetBoundingBox(True).Center, obb_endfaces[1].GetBoundingBox(True).Center) + extension_length = 0.5 * diffCheck.df_util.get_doc_2_meters_unitf() + beam_axis.Extend(extension_length, extension_length) + args.Display.DrawArrow(beam_axis, System.Drawing.Color.Magenta) + + # beam assembly index + anchor_pt: rg.Point3d = beam_axis.From - beam_axis.UnitTangent * 0.5 * extension_length + args.Display.Draw2dText( + str(beam.index_assembly), + System.Drawing.Color.Violet, + anchor_pt, + True, 18) ####################################### ## DFJoints ####################################### if self._are_joints_visible: + clr = self._joint_rnd_clr[idx_beam] for idx_joint, joint in enumerate(beam.joints): joint_faces = joint.faces for idx_face, face in enumerate(joint_faces): + if len(self._dfassembly.beams) == 1: + clr: System.Drawing.Color = System.Drawing.Color.Magenta + face_center = face.to_brep_face().GetBoundingBox(False).Center - args.Display.DrawPoint(face_center, self._joint_rnd_clr[idx_joint]) + args.Display.DrawPoint(face_center, clr) vector_face_center_2_beam_center = face_center - beam.center vector_face_center_2_beam_center.Unitize() - vector_face_center_2_beam_center *= 0.4 * extension_length + vector_face_center_2_beam_center *= 0.4 * 0.5 * diffCheck.df_util.get_doc_2_meters_unitf() ln = rg.Line(face_center, face_center + vector_face_center_2_beam_center) - args.Display.DrawDottedLine(ln, self._joint_rnd_clr[idx_joint]) + args.Display.DrawDottedLine(ln, clr) ghenv.Component.AddRuntimeMessage(RML.Remark, "legend joint naming: the beam index - the joint index - the face index by list order") # noqa: F821 name_face_joint: str = f"{beam.index_assembly}-{joint.id}-{idx_face}" args.Display.Draw2dText( name_face_joint, - self._joint_rnd_clr[idx_joint], + clr, ln.To, True, 18) diff --git a/src/gh/components/DF_visualization/code.py b/src/gh/components/DF_visualization/code.py index 85c90e5e..0269582c 100644 --- a/src/gh/components/DF_visualization/code.py +++ b/src/gh/components/DF_visualization/code.py @@ -1,14 +1,11 @@ #! python3 -import Rhino.Geometry as rg from ghpythonlib.componentbase import executingcomponent as component from diffCheck import df_cvt_bindings from diffCheck import df_visualization from diffCheck.df_visualization import DFVizSettings -from diffCheck.df_error_estimation import DFVizResults -from diffCheck import diffcheck_bindings - +from diffCheck.df_error_estimation import DFVizResults, DFInvalidData class DFVisualization(component): def RunScript(self, @@ -18,22 +15,26 @@ def RunScript(self, if i_result is None or i_viz_settings is None: return None, None, None - values, min_value, max_value = i_result.filter_values_based_on_valuetype(i_viz_settings) - - # check if i_result.source is a list of pointclouds or a mesh - if type(i_result.source[0]) is diffcheck_bindings.dfb_geometry.DFPointCloud: - - # convert to Rhino PCD - o_source = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(src) for src in i_result.source] - - # color geometry + # make a DFVizResult copy to avoid modifying the original result to be exported in csv + result_cp = DFVizResults(i_result.assembly) + exclude_indices = [idx for idx, sanity_val in enumerate(i_result.sanity_check) if sanity_val.value != DFInvalidData.VALID.value] + result_cp.source = [val for idx, val in enumerate(i_result.source) if idx not in exclude_indices] + result_cp.target = [val for idx, val in enumerate(i_result.target) if idx not in exclude_indices] + result_cp.distances = [val for idx, val in enumerate(i_result.distances) if idx not in exclude_indices] + result_cp.distances_mean = [val for idx, val in enumerate(i_result.distances_mean) if idx not in exclude_indices] + result_cp.distances_rmse = [val for idx, val in enumerate(i_result.distances_rmse) if idx not in exclude_indices] + result_cp.distances_max_deviation = [val for idx, val in enumerate(i_result.distances_rmse) if idx not in exclude_indices] + result_cp.distances_min_deviation = [val for idx, val in enumerate(i_result.distances_min_deviation) if idx not in exclude_indices] + result_cp.distances_sd_deviation = [val for idx, val in enumerate(i_result.distances_sd_deviation) if idx not in exclude_indices] + + values, min_value, max_value = result_cp.filter_values_based_on_valuetype(i_viz_settings) + + # check if result_cp.source is a list of pointclouds or a mesh + if result_cp.is_source_cloud: + o_source = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(src) for src in result_cp.source] o_colored_geo = [df_visualization.color_rh_pcd(src, dist, min_value, max_value, i_viz_settings.palette) for src, dist in zip(o_source, values)] - - elif type(i_result.source[0]) is rg.Mesh: - # convert to Rhino Mesh - o_source = i_result.source - - # color geometry + else: + o_source = result_cp.source o_colored_geo = [df_visualization.color_rh_mesh(src, dist, min_value, max_value, i_viz_settings.palette) for src, dist in zip(o_source, values)] o_legend = df_visualization.create_legend(min_value, @@ -44,9 +45,7 @@ def RunScript(self, width=i_viz_settings.legend_width, total_height=i_viz_settings.legend_height) - # add option to create a histogram for each item - - if len(i_result.source) > 1 and i_viz_settings.one_histogram_per_item: + if len(result_cp.source) > 1 and i_viz_settings.one_histogram_per_item: multiple_curves = True else: multiple_curves = False diff --git a/src/gh/components/DF_visualization_settings/code.py b/src/gh/components/DF_visualization_settings/code.py index 34e95809..565f562c 100644 --- a/src/gh/components/DF_visualization_settings/code.py +++ b/src/gh/components/DF_visualization_settings/code.py @@ -123,7 +123,7 @@ def add_plane_object(self, class DFVisualizationSettings(component): def __init__(self): - self.poss_value_types = ["Dist", "RMSE", "MAX", "MIN", "STD"] + self.poss_value_types = ["Dist", "MEAN", "RMSE", "MAX", "MIN", "STD"] self.poss_palettes = ["Jet", "Rainbow", "RdPu", "Viridis"] ghenv.Component.ExpireSolution(True) # noqa: F821 @@ -200,7 +200,7 @@ def RunScript(self, # set default values if i_value_type is not None: if i_value_type not in self.poss_value_types: - ghenv.Component.AddRuntimeMessage(RML.Warning, "Possible values for i_value_type are: dist, RMSE, MAX, MIN, STD") # noqa: F821 + ghenv.Component.AddRuntimeMessage(RML.Warning, "Possible values for i_value_type are: dist, MEAN, RMSE, MAX, MIN, STD") # noqa: F821 return None else: i_value_type = "Dist" @@ -233,17 +233,3 @@ def RunScript(self, i_one_histogram_per_item) return o_viz_settings - -# if __name__ == "__main__": -# com = DFVisualizationSettings() -# o_viz_settings = com.RunScript( -# i_value_type, -# i_palette, -# i_upper_threshold, -# i_lower_threshold, -# i_legend_height, -# i_legend_width, -# i_legend_plane, -# i_histogram_scale_factor, -# i_one_histogram_per_item -# ) diff --git a/src/gh/diffCheck/diffCheck/df_error_estimation.py b/src/gh/diffCheck/diffCheck/df_error_estimation.py index 6c1f2e13..c047e4fc 100644 --- a/src/gh/diffCheck/diffCheck/df_error_estimation.py +++ b/src/gh/diffCheck/diffCheck/df_error_estimation.py @@ -3,12 +3,132 @@ This module contains the utility functions to compute the difference between source and target """ +import typing +from enum import Enum + import numpy as np -from diffCheck import diffcheck_bindings # type: ignore + +import Rhino import Rhino.Geometry as rg + +from diffCheck import diffcheck_bindings # type: ignore +from diffCheck import df_cvt_bindings from diffCheck.df_geometries import DFAssembly +class DFInvalidData(Enum): + """ + Enum to define the type of invalid data for joint or assembly analysis + """ + # healty data + VALID = 0 + # the joint or beam is way to out of the tolerance established in the joint/assembly segmentator + OUT_OF_TOLERANCE = 1 + # we are missing scan points to evaluate + MISSING_PCD = 2 + +class DFVizResults: + """ + This class compiles the resluts of the error estimation into one object + """ + + def __init__(self, assembly): + + self.source = [] + self.target = [] + + self.distances_mean = [] + self.distances_rmse = [] + self.distances_max_deviation = [] + self.distances_min_deviation = [] + self.distances_sd_deviation = [] + self.distances = [] + self.assembly = assembly + + self.sanity_check = [] + + self._is_source_cloud = True # if False it's a mesh + + def add(self, source, target, distances, sanity_check: DFInvalidData = DFInvalidData.VALID): + + self.source.append(source) + self.target.append(target) + + self.sanity_check.append(sanity_check) + + if sanity_check == DFInvalidData.OUT_OF_TOLERANCE: + self.distances_mean.append(DFInvalidData.OUT_OF_TOLERANCE) + self.distances_rmse.append(DFInvalidData.OUT_OF_TOLERANCE) + self.distances_max_deviation.append(DFInvalidData.OUT_OF_TOLERANCE) + self.distances_min_deviation.append(DFInvalidData.OUT_OF_TOLERANCE) + self.distances_sd_deviation.append(DFInvalidData.OUT_OF_TOLERANCE) + self.distances.append(distances.tolist()) + elif sanity_check == DFInvalidData.MISSING_PCD: + self.distances_mean.append(DFInvalidData.MISSING_PCD) + self.distances_rmse.append(DFInvalidData.MISSING_PCD) + self.distances_max_deviation.append(DFInvalidData.MISSING_PCD) + self.distances_min_deviation.append(DFInvalidData.MISSING_PCD) + self.distances_sd_deviation.append(DFInvalidData.MISSING_PCD) + self.distances.append(np.empty(0)) + else: + self.distances_mean.append(np.mean(distances)) + self.distances_rmse.append(np.sqrt(np.mean(distances ** 2))) + self.distances_max_deviation.append(np.max(distances)) + self.distances_min_deviation.append(np.min(distances)) + self.distances_sd_deviation.append(np.std(distances)) + self.distances.append(distances.tolist()) + + def filter_values_based_on_valuetype(self, settings): + + if settings.valueType == "Dist": + valid_sublists = [sublist for sublist in self.distances if len(sublist) > 0] + min_value = min(min(sublist) for sublist in valid_sublists) + max_value = max(max(sublist) for sublist in valid_sublists) + values = self.distances + + elif settings.valueType == "MEAN": + valid_values = [value for value in self.distances_mean if value is not None] + min_value = min(valid_values) + max_value = max(valid_values) + values = self.distances_mean + + elif settings.valueType == "RMSE": + valid_values = [value for value in self.distances_rmse if value is not None] + min_value = min(valid_values) + max_value = max(valid_values) + values = self.distances_rmse + + elif settings.valueType == "MAX": + valid_values = [value for value in self.distances_max_deviation if value is not None] + min_value = min(valid_values) + max_value = max(valid_values) + values = self.distances_max_deviation + + elif settings.valueType == "MIN": + valid_values = [value for value in self.distances_min_deviation if value is not None] + min_value = min(valid_values) + max_value = max(valid_values) + values = self.distances_min_deviation + + elif settings.valueType == "STD": + valid_values = [value for value in self.distances_sd_deviation if value is not None] + min_value = min(valid_values) + max_value = max(valid_values) + values = self.distances_sd_deviation + + # threshold values + if settings.lower_threshold is not None: + min_value = settings.lower_threshold + if settings.upper_threshold is not None: + max_value = settings.upper_threshold + + return values, min_value, max_value + + @property + def is_source_cloud(self): + return type(self.source[0]) is diffcheck_bindings.dfb_geometry.DFPointCloud + + def df_cloud_2_df_cloud_comparison(source_list, target_list): """ Compute the Euclidean distance for every point of a source pcd to its @@ -22,32 +142,61 @@ def df_cloud_2_df_cloud_comparison(source_list, target_list): return results -def df_cloud_2_rh_mesh_comparison(assembly, cloud_source_list, rhino_mesh_target_list, signed_flag, swap): +def rh_cloud_2_rh_mesh_comparison( + assembly: DFAssembly, + rh_cloud_source_list: typing.List[Rhino.Geometry.PointCloud], + rhino_mesh_target_list: typing.List[rg.Mesh], + signed_flag: bool, + swap: bool, + ) -> DFVizResults: """ - Computes distances between a pcd and a mesh + Computes distances between a pcd and a mesh and return the results + + :param assembly: the DFAssembly object + :param rh_cloud_source_list: list of point clouds after segmentation in Rhino format + :param rhino_mesh_target_list: list of rhino meshes + :param signed_flag: flag to compute signed distances + :param swap: this mean we want to visualize the result on the target mesh (or viceversa) + + :return: the results of the comparison """ results = DFVizResults(assembly) - for source, target in zip(cloud_source_list, rhino_mesh_target_list): + for idx, source_rh in enumerate(rh_cloud_source_list): + source_df = df_cvt_bindings.cvt_rhcloud_2_dfcloud(source_rh) + target = rhino_mesh_target_list[idx] + + source_df_pts = source_df.points + + if swap: + source_df, target = target, source_df + + # FIXME: this is a hack to avoid that the assembly segmentator breaks this + # snippet because it is not stamping the rhino pout cloud with the sanity check + # user string value. + sanity_check_value_uncasted = source_rh.GetUserString("df_sanity_scan_check") + sanity_check_value = None + if sanity_check_value_uncasted is None: + sanity_check_value = DFInvalidData.VALID.value + else: + sanity_check_value = int(sanity_check_value_uncasted) - if len(source.points) == 0: - distances = np.empty(0) + if sanity_check_value == DFInvalidData.OUT_OF_TOLERANCE.value: + out_of_tol_distances = np.asarray([DFInvalidData.OUT_OF_TOLERANCE] * len(source_df_pts)) + results.add(source_df, target, out_of_tol_distances, sanity_check=DFInvalidData.OUT_OF_TOLERANCE) + elif sanity_check_value == DFInvalidData.MISSING_PCD.value or len(source_df_pts) == 0: + results.add(source_df, target, np.empty(0), sanity_check=DFInvalidData.MISSING_PCD) else: if swap: # this mean we want to visualize the result on the target mesh - distances = rh_mesh_2_df_cloud_distance(target, source, signed_flag) + distances = rh_mesh_2_df_cloud_distance(source_df, target, signed_flag) else: # this means we want to visualize the result on the source pcd - distances = df_cloud_2_rh_mesh_distance(source, target, signed_flag) - - if swap: - results.add(target, source, distances) - else: - results.add(source, target, distances) + distances = df_cloud_2_rh_mesh_distance(source_df, target, signed_flag) + results.add(source_df, target, distances) return results - def rh_mesh_2_df_cloud_distance(source, target, signed=False): """ Calculate the distance between every vertex of a Rhino Mesh to its closest point on a PCD @@ -66,7 +215,6 @@ def rh_mesh_2_df_cloud_distance(source, target, signed=False): tree.Insert(rg.Point3d(ver[0], ver[1], ver[2]), i) for idx, p in enumerate(source.Vertices): - # find the index on the target that the vertex is closest to search_point = p sphere = rg.Sphere(search_point, distances[idx]*1.0001) #to change later, hack to avoid not finding the point @@ -117,80 +265,3 @@ def df_cloud_2_rh_mesh_distance(source, target, signed=False): distances.append(distance) return np.asarray(distances) - - -class DFVizResults: - """ - This class compiles the resluts of the error estimation into one object - """ - - def __init__(self, assembly): - - self.source = [] - self.target = [] - - self.distances_rmse = [] - self.distances_max_deviation = [] - self.distances_min_deviation = [] - self.distances_sd_deviation = [] - self.distances = [] - self.assembly = assembly - - def add(self, source, target, distances): - - self.source.append(source) - self.target.append(target) - - if distances.size == 0: - self.distances_rmse.append(None) - self.distances_max_deviation.append(None) - self.distances_min_deviation.append(None) - self.distances_sd_deviation.append(None) - self.distances.append(np.empty(0)) - else: - self.distances_rmse.append(np.sqrt(np.mean(distances ** 2))) - self.distances_max_deviation.append(np.max(distances)) - self.distances_min_deviation.append(np.min(distances)) - self.distances_sd_deviation.append(np.std(distances)) - self.distances.append(distances.tolist()) - - def filter_values_based_on_valuetype(self, settings): - - if settings.valueType == "Dist": - - valid_sublists = [sublist for sublist in self.distances if len(sublist) > 0] - - min_value = min(min(sublist) for sublist in valid_sublists) - max_value = max(max(sublist) for sublist in valid_sublists) - values = self.distances - - elif settings.valueType == "RMSE": - - values = self.distances_rmse - min_value = min(values) - max_value = max(values) - - elif settings.valueType == "MAX": - - values = self.distances_max_deviation - min_value = min(values) - max_value = max(values) - - elif settings.valueType == "MIN": - values = self.distances_min_deviation - min_value = min(values) - max_value = max(values) - - elif settings.valueType == "STD": - - values = self.distances_sd_deviation - min_value = min(values) - max_value = max(values) - - # threshold values - if settings.lower_threshold is not None: - min_value = settings.lower_threshold - if settings.upper_threshold is not None: - max_value = settings.upper_threshold - - return values, min_value, max_value diff --git a/src/gh/diffCheck/diffCheck/df_geometries.py b/src/gh/diffCheck/diffCheck/df_geometries.py index f3fead03..f87701fd 100644 --- a/src/gh/diffCheck/diffCheck/df_geometries.py +++ b/src/gh/diffCheck/diffCheck/df_geometries.py @@ -18,7 +18,6 @@ import diffCheck.df_joint_detector import diffCheck.df_util - @dataclass class DFVertex: """ diff --git a/src/gh/diffCheck/diffCheck/df_joint_detector.py b/src/gh/diffCheck/diffCheck/df_joint_detector.py index bef18712..b8fa4678 100644 --- a/src/gh/diffCheck/diffCheck/df_joint_detector.py +++ b/src/gh/diffCheck/diffCheck/df_joint_detector.py @@ -94,7 +94,7 @@ def _find_joint_faces(self, bounding_geometry): faces = {} if self.is_roundwood: for idx, face in enumerate(self.brep.Faces): - faces[idx] = (face, face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) + faces[idx] = (face, face.IsPlanar(1 * sc.doc.ModelAbsoluteTolerance)) else: for idx, face in enumerate(self.brep.Faces): face_centroid = rg.AreaMassProperties.Compute(face).Centroid @@ -102,7 +102,7 @@ def _find_joint_faces(self, bounding_geometry): projected_centroid = face.PointAt(coord[1], coord[2]) faces[idx] = (face, bounding_geometry.IsPointInside(projected_centroid, sc.doc.ModelAbsoluteTolerance, True) - * face.IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance)) + * face.IsPlanar(1 * sc.doc.ModelAbsoluteTolerance)) return faces @@ -127,7 +127,7 @@ def _compute_adjacency_of_faces(self, faces): [adj_face for adj_face in face[0].AdjacentFaces() if faces[adj_face][1] - and faces[adj_face][0].IsPlanar(1000 * sc.doc.ModelAbsoluteTolerance) + and faces[adj_face][0].IsPlanar(1 * sc.doc.ModelAbsoluteTolerance) and adj_face != idx]) return adjacency_of_faces diff --git a/src/gh/diffCheck/diffCheck/df_visualization.py b/src/gh/diffCheck/diffCheck/df_visualization.py index fe3feb7a..039ba861 100644 --- a/src/gh/diffCheck/diffCheck/df_visualization.py +++ b/src/gh/diffCheck/diffCheck/df_visualization.py @@ -159,8 +159,10 @@ def color_rh_mesh(mesh, values, min_value, max_value, palette): elif isinstance(values, list): # If values is a non-empty list mapped_color = palette.value_to_color(values[i], min_value, max_value) - else: + elif values is None: # If values is not None and not empty (assuming it's a single value) + mapped_color = Color.FromArgb(255, 255, 255) + else: mapped_color = palette.value_to_color(values, min_value, max_value) mesh.VertexColors.Add(mapped_color.R, mapped_color.G, mapped_color.B) @@ -281,6 +283,8 @@ def create_histogram_curve(values, min_value, max_value, res=100, bin_size=1, # Count the frequencies of values in each bin for value in values: + if value is None: + continue if value < min_value: value = min_value elif value > max_value: @@ -329,6 +333,8 @@ def create_histogram(values, min_value, max_value, res=100, steps=10, if multiple_curves: for v in values: + if v is None: + continue polyline, max_freq = create_histogram_curve(v, min_value, max_value, res, bin_size, height,scaling_factor, spacing) @@ -337,6 +343,9 @@ def create_histogram(values, min_value, max_value, res=100, steps=10, max_frequency = max_freq else: + if values is None: + return None + polyline, max_frequency = create_histogram_curve(values, min_value, max_value, res, bin_size, height,scaling_factor, spacing)