diff --git a/contrib/fix_ply/README.md b/contrib/fix_ply/README.md new file mode 100644 index 00000000..e6e8a9da --- /dev/null +++ b/contrib/fix_ply/README.md @@ -0,0 +1,19 @@ +# Fix Ply + +Use to translate a modified ply into a compatible format for subsequent steps in ODM. Via Jaime Chacoff, https://community.opendronemap.org/t/edited-point-cloud-with-cloudcompare-wont-rerun-from-odm-meshing/21449/6 + +The basic idea is to process through ODM until the point cloud is created, use a 3rd party tool, like CloudCompare to edit the point cloud, and then continue processing in OpenDroneMap. + +This useful bit of python will convert the PLY exported from CloudCompare back into a compatible format for continued processing in OpenDroneMap. + +1. Run project in WebODM and add this to your settings: `end-with: odm-filterpoints` +1. Once complete, go to your NodeODM container and copy `/var/www/data/[Task ID]/odm-filterpoints` directory +1. Open CloudCompare and from `odm-filterpoints` directory you've copied, open `point_cloud.ply` +1. In the box that pops up, add a scalar field `vertex - views` +1. To see the actual colours again - select the point cloud, then in properties change colours from "Scalar field" to "RGB" +1. Make your changes to the point cloud +1. Compute normals (Edit > Normals > Compute) +1. Save PLY file as ASCII +1. Run Python file above to fix PLY file and convert to binary +1. Copy `odm_filterpoints` directory (or just `point_cloud.ply`) back into NodeODM container +1. Restart project in WebODM "From Meshing" (don't forget to edit settings to remove `end-with: odm-filterpoints` or it's not going to do anything). diff --git a/contrib/fix_ply/fix_ply.py b/contrib/fix_ply/fix_ply.py new file mode 100644 index 00000000..26ae1afb --- /dev/null +++ b/contrib/fix_ply/fix_ply.py @@ -0,0 +1,68 @@ +import os +import logging +from plyfile import PlyData, PlyElement +import numpy as np + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def pcd_ascii_to_binary_ply(ply_file: str, binary_ply: str) -> None: + """Converts ASCII PLY to binary, ensuring 'views' is present and of type uchar. + Raises ValueError if neither 'scalar_views' nor 'views' is found. + """ + + try: + logging.info(f"Reading ASCII PLY file: {ply_file}") + ply_data: PlyData = PlyData.read(ply_file) + except FileNotFoundError: + logging.error(f"File not found: {ply_file}") + return + except Exception as e: + logging.error(f"Error reading PLY file: {e}") + return + + new_elements: list[PlyElement] = [] + + for element in ply_data.elements: + new_data = element.data.copy() + + if 'scalar_views' in element.data.dtype.names: + new_data['views'] = new_data['scalar_views'].astype('u1') + del new_data['scalar_views'] + elif 'views' in element.data.dtype.names: + new_data['views'] = new_data['views'].astype('u1') + else: + raise ValueError(f"Neither 'scalar_views' nor 'views' found - did you import them when opened the file in CloudCompare?") + + + new_element = PlyElement.describe(new_data, element.name) + new_elements.append(new_element) + + new_ply_data = PlyData(new_elements, text=False) + + try: + logging.info(f"Writing binary PLY file: {binary_ply}") + new_ply_data.write(binary_ply) + except Exception as e: + logging.error(f"Error writing PLY file: {e}") + return + + logging.info("PLY conversion complete.") + + +if __name__ == '__main__': + + # Parameters + base: str = os.path.dirname(os.path.abspath(__file__)) + ply_file: str = os.path.join(base, 'point_cloud_ascii.ply') + binary_ply_file: str = os.path.join(base, 'point_cloud.ply') + + if not os.path.exists(ply_file): + logging.error(f"Input file not found: {ply_file}") + exit(1) # Exit with error code + + try: + pcd_ascii_to_binary_ply(ply_file, binary_ply_file) + except ValueError as e: + logging.error(f"PLY conversion failed: {e}") + exit(1) # Exit with error code to indicate failure