ply-parser 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ply_parser-2.0.0/PKG-INFO +72 -0
- ply_parser-2.0.0/README.md +57 -0
- ply_parser-2.0.0/pyproject.toml +19 -0
- ply_parser-2.0.0/setup.cfg +4 -0
- ply_parser-2.0.0/setup.py +26 -0
- ply_parser-2.0.0/src/ply_parser/__init__.py +1 -0
- ply_parser-2.0.0/src/ply_parser/ply_parser.py +105 -0
- ply_parser-2.0.0/src/ply_parser/test_ply_parser.py +226 -0
- ply_parser-2.0.0/src/ply_parser.egg-info/PKG-INFO +72 -0
- ply_parser-2.0.0/src/ply_parser.egg-info/SOURCES.txt +11 -0
- ply_parser-2.0.0/src/ply_parser.egg-info/dependency_links.txt +1 -0
- ply_parser-2.0.0/src/ply_parser.egg-info/top_level.txt +1 -0
- ply_parser-2.0.0/tests/test_ply_parser.py +226 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ply_parser
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: A python module that parses ascii PLY (.ply) (Polygon File Format) files. It includes a class PLYObject for storing PLY file data and a function parse_ply_file() for reading PLY files and creating PLYObject instances and can easily be customized.
|
|
5
|
+
Home-page: https://github.com/Qrishna/ply_parser
|
|
6
|
+
Author: Krishna Bhattarai
|
|
7
|
+
Author-email: Krishna Bhattarai <krishna@kbhattarai.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/Qrishna/ply_parser
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/Qrishna/ply_parser/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.11.4
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
### PLY File Parser
|
|
17
|
+
This Python module provides a simple PLY (Polygon File Format) file parser. It includes a class PLYObject for storing PLY file data and a function parse_ply_file for reading PLY files and creating PLYObject instances.
|
|
18
|
+
|
|
19
|
+
### Usage
|
|
20
|
+
### PLYObject Class
|
|
21
|
+
The PLYObject class represents a PLY object with attributes:
|
|
22
|
+
|
|
23
|
+
`name`: Name of the PLY object.
|
|
24
|
+
|
|
25
|
+
`vertices`: List of vertex coordinates.
|
|
26
|
+
|
|
27
|
+
`faces`: List of face indices.
|
|
28
|
+
|
|
29
|
+
`colors`: List of RGB color values for vertices.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#### parse_ply_file Function
|
|
33
|
+
The parse_ply_file function takes a PLY file as input and returns a PLYObject instance. It reads the file, extracts vertex and face information, and handles color properties. If any inconsistencies or errors are detected in the PLY file, appropriate error messages are displayed.
|
|
34
|
+
```
|
|
35
|
+
file = "data/cube_colors.ply"
|
|
36
|
+
parsed_object = parse_ply_file(file)
|
|
37
|
+
if parsed_object:
|
|
38
|
+
print("name:", parsed_object.name)
|
|
39
|
+
print("total_vertices:", len(parsed_object.vertices))
|
|
40
|
+
print("total_faces:", len(parsed_object.faces))
|
|
41
|
+
print("total_colors:", len(parsed_object.colors))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Requirements
|
|
45
|
+
`Python 3.11.x`
|
|
46
|
+
|
|
47
|
+
### PLY File Format Support
|
|
48
|
+
This parser currently supports PLY files with the following characteristics:
|
|
49
|
+
|
|
50
|
+
`ASCII format`
|
|
51
|
+
|
|
52
|
+
`Vertex coordinates (X, Y, Z)`
|
|
53
|
+
|
|
54
|
+
`Face indices (triangles and quads)`
|
|
55
|
+
|
|
56
|
+
`Vertex colors`
|
|
57
|
+
|
|
58
|
+
### Testing
|
|
59
|
+
```
|
|
60
|
+
python3 -m unittest tests/test_ply_parser.py -v
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Limitations
|
|
64
|
+
The parser assumes the input PLY file follows the standard specifications.
|
|
65
|
+
It may not handle non-standard or corrupted PLY files gracefully.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
### Contribution
|
|
69
|
+
Feel free to contribute by opening issues or submitting pull requests. Bug reports, suggestions, and improvements are welcome.
|
|
70
|
+
|
|
71
|
+
### License
|
|
72
|
+
This PLY file parser is licensed under the MIT License. See the LICENSE file for details.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
### PLY File Parser
|
|
2
|
+
This Python module provides a simple PLY (Polygon File Format) file parser. It includes a class PLYObject for storing PLY file data and a function parse_ply_file for reading PLY files and creating PLYObject instances.
|
|
3
|
+
|
|
4
|
+
### Usage
|
|
5
|
+
### PLYObject Class
|
|
6
|
+
The PLYObject class represents a PLY object with attributes:
|
|
7
|
+
|
|
8
|
+
`name`: Name of the PLY object.
|
|
9
|
+
|
|
10
|
+
`vertices`: List of vertex coordinates.
|
|
11
|
+
|
|
12
|
+
`faces`: List of face indices.
|
|
13
|
+
|
|
14
|
+
`colors`: List of RGB color values for vertices.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#### parse_ply_file Function
|
|
18
|
+
The parse_ply_file function takes a PLY file as input and returns a PLYObject instance. It reads the file, extracts vertex and face information, and handles color properties. If any inconsistencies or errors are detected in the PLY file, appropriate error messages are displayed.
|
|
19
|
+
```
|
|
20
|
+
file = "data/cube_colors.ply"
|
|
21
|
+
parsed_object = parse_ply_file(file)
|
|
22
|
+
if parsed_object:
|
|
23
|
+
print("name:", parsed_object.name)
|
|
24
|
+
print("total_vertices:", len(parsed_object.vertices))
|
|
25
|
+
print("total_faces:", len(parsed_object.faces))
|
|
26
|
+
print("total_colors:", len(parsed_object.colors))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Requirements
|
|
30
|
+
`Python 3.11.x`
|
|
31
|
+
|
|
32
|
+
### PLY File Format Support
|
|
33
|
+
This parser currently supports PLY files with the following characteristics:
|
|
34
|
+
|
|
35
|
+
`ASCII format`
|
|
36
|
+
|
|
37
|
+
`Vertex coordinates (X, Y, Z)`
|
|
38
|
+
|
|
39
|
+
`Face indices (triangles and quads)`
|
|
40
|
+
|
|
41
|
+
`Vertex colors`
|
|
42
|
+
|
|
43
|
+
### Testing
|
|
44
|
+
```
|
|
45
|
+
python3 -m unittest tests/test_ply_parser.py -v
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Limitations
|
|
49
|
+
The parser assumes the input PLY file follows the standard specifications.
|
|
50
|
+
It may not handle non-standard or corrupted PLY files gracefully.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Contribution
|
|
54
|
+
Feel free to contribute by opening issues or submitting pull requests. Bug reports, suggestions, and improvements are welcome.
|
|
55
|
+
|
|
56
|
+
### License
|
|
57
|
+
This PLY file parser is licensed under the MIT License. See the LICENSE file for details.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
[project]
|
|
3
|
+
name = "ply_parser"
|
|
4
|
+
version = "2.0.0"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name="Krishna Bhattarai", email="krishna@kbhattarai.com" },
|
|
7
|
+
]
|
|
8
|
+
description = "A python module that parses ascii PLY (.ply) (Polygon File Format) files. It includes a class PLYObject for storing PLY file data and a function parse_ply_file() for reading PLY files and creating PLYObject instances and can easily be customized."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
"Homepage" = "https://github.com/Qrishna/ply_parser"
|
|
19
|
+
"Bug Tracker" = "https://github.com/Qrishna/ply_parser/issues"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name='ply_parser',
|
|
8
|
+
version='2.0.0',
|
|
9
|
+
author='Krishna Bhattarai',
|
|
10
|
+
author_email='krishna@kbhattarai.com',
|
|
11
|
+
description='A python module that parses ascii PLY (.ply) (Polygon File Format) files. It includes a class PLYObject for storing PLY file data and a function parse_ply_file() for reading PLY files and creating PLYObject instances and can easily be customized. ',
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/Qrishna/ply_parser",
|
|
15
|
+
project_urls={
|
|
16
|
+
"Bug Tracker": "https://github.com/Qrishna/ply_parser/issues"
|
|
17
|
+
},
|
|
18
|
+
package_dir={'': "src"},
|
|
19
|
+
packages=find_packages("src"),
|
|
20
|
+
classifiers=[
|
|
21
|
+
'Programming Language :: Python :: 3',
|
|
22
|
+
'License :: OSI Approved :: MIT License',
|
|
23
|
+
'Operating System :: OS Independent',
|
|
24
|
+
],
|
|
25
|
+
python_requires='>=3.11.4',
|
|
26
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .ply_parser import *
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Krishna Bhattarai
|
|
3
|
+
# Nov 23
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PLYObject:
|
|
8
|
+
def __init__(self, vertices, faces, colors, name=None):
|
|
9
|
+
self.name = name
|
|
10
|
+
self.vertices = vertices
|
|
11
|
+
self.faces = faces
|
|
12
|
+
self.colors = colors
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_file_data(file_name):
|
|
16
|
+
with open(file_name, 'r') as file:
|
|
17
|
+
data = file.readlines()
|
|
18
|
+
return data
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_ply_file_data(lines):
|
|
22
|
+
expected_faces = None
|
|
23
|
+
expected_vertices = None
|
|
24
|
+
vertices, faces, colors = [], [], []
|
|
25
|
+
|
|
26
|
+
vertex_count = 0
|
|
27
|
+
faces_count = 0
|
|
28
|
+
|
|
29
|
+
property_red = 0
|
|
30
|
+
property_blue = 0
|
|
31
|
+
property_green = 0
|
|
32
|
+
|
|
33
|
+
for x in range(0, len(lines)):
|
|
34
|
+
line = lines[x].strip()
|
|
35
|
+
if not line or line == '':
|
|
36
|
+
continue
|
|
37
|
+
if line.startswith("format"):
|
|
38
|
+
file_format = line.strip().split()[1]
|
|
39
|
+
if file_format.lower() != "ascii":
|
|
40
|
+
# print("File format is not ascii! It is: %s" % file_format)
|
|
41
|
+
raise ValueError("File format is not ascii! It is: %s" % file_format)
|
|
42
|
+
|
|
43
|
+
if line.startswith("comment"):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if "element vertex" in line:
|
|
47
|
+
expected_vertices = int(re.match(r'^element\s+vertex\s+(\d+)', line).group(1))
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
if "property uchar red" in line:
|
|
51
|
+
property_red += 1
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if "property uchar green" in line:
|
|
55
|
+
property_green += 1
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
if "property uchar blue" in line:
|
|
59
|
+
property_blue += 1
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if re.match(r'^element\s+face\s+\d+', line):
|
|
63
|
+
expected_faces = int(re.match(r'^element\s+face\s+(\d+)', line).group(1))
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
elif "end_header" in line:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if re.match(r'^([-|\d]+(\.\d+)?)\s+([-|\d]+(\.\d+)?)\s+([-|\d]+(\.\d+)?)', line) and vertex_count != expected_vertices:
|
|
70
|
+
vertex_count += 1
|
|
71
|
+
|
|
72
|
+
if property_red and property_green and property_blue:
|
|
73
|
+
vertex = line.strip().split()[0:6]
|
|
74
|
+
vertices.append([float(vertex[0]), float(vertex[1]), float(vertex[2])])
|
|
75
|
+
colors.append([float(vertex[3]), float(vertex[4]), float(vertex[5])])
|
|
76
|
+
else:
|
|
77
|
+
vertex = line.strip().split()[0:3]
|
|
78
|
+
vertices.append([float(vertex[0]), float(vertex[1]), float(vertex[2])])
|
|
79
|
+
|
|
80
|
+
if re.match(r'^[34]\s+\d+\s+\d+\s\d+', line) and faces_count != expected_faces:
|
|
81
|
+
faces_count += 1
|
|
82
|
+
if line[0].startswith('3'):
|
|
83
|
+
face = line.strip().split()[1:4]
|
|
84
|
+
faces.append([int(face[0]), int(face[1]), int(face[2])])
|
|
85
|
+
elif line[0].startswith('4'):
|
|
86
|
+
face = line.strip().split()[1:5]
|
|
87
|
+
faces.append([int(face[0]), int(face[1]), int(face[2]), int(face[3])])
|
|
88
|
+
|
|
89
|
+
if len(vertices) != expected_vertices:
|
|
90
|
+
# print("Error: total vertices read: %s does not match expected vertices: %s" % (len(vertices), expected_vertices))
|
|
91
|
+
raise ValueError("Error: total vertices read: %s does not match expected vertices: %s" % (len(vertices), expected_vertices))
|
|
92
|
+
|
|
93
|
+
if len(faces) != expected_faces:
|
|
94
|
+
# print("Error: total faces read: %s does not match expected faces: %s" % (len(faces), expected_faces))
|
|
95
|
+
raise ValueError("Error: total faces read: %s does not match expected faces: %s" % (len(faces), expected_faces))
|
|
96
|
+
|
|
97
|
+
return [vertices, faces, colors]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_ply_file(file_name):
|
|
101
|
+
file_data_lines = get_file_data(file_name)
|
|
102
|
+
parsed_data = parse_ply_file_data(file_data_lines)
|
|
103
|
+
if parsed_data:
|
|
104
|
+
v, f, c = parsed_data[0], parsed_data[1], parsed_data[2]
|
|
105
|
+
return PLYObject(v, f, c, file_name.strip())
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from ply_parser import *
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestPLYParsing(unittest.TestCase):
|
|
6
|
+
def test_parse_ply_data_with_vertices_and_faces(self):
|
|
7
|
+
data = """ply
|
|
8
|
+
format ascii 1.0
|
|
9
|
+
comment Some comment
|
|
10
|
+
element vertex 3
|
|
11
|
+
property float x
|
|
12
|
+
property float y
|
|
13
|
+
property float z
|
|
14
|
+
element face 1
|
|
15
|
+
end_header
|
|
16
|
+
1.0 2.0 3.0
|
|
17
|
+
4.0 5.0 6.0
|
|
18
|
+
7.0 8.0 9.0
|
|
19
|
+
3 0 1 2"""
|
|
20
|
+
|
|
21
|
+
result = parse_ply_file_data(data.splitlines())
|
|
22
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
23
|
+
self.assertEqual(len(vertices), 3)
|
|
24
|
+
self.assertEqual(len(faces), 1)
|
|
25
|
+
self.assertEqual(len(colors), 0)
|
|
26
|
+
|
|
27
|
+
def test_parse_ply_data_with_vertices_colors_and_faces(self):
|
|
28
|
+
data = """ply
|
|
29
|
+
format ascii 1.0
|
|
30
|
+
element vertex 3
|
|
31
|
+
property float x
|
|
32
|
+
property float y
|
|
33
|
+
property float z
|
|
34
|
+
property uchar red
|
|
35
|
+
property uchar green
|
|
36
|
+
property uchar blue
|
|
37
|
+
element face 1
|
|
38
|
+
property list uchar int vertex_indices
|
|
39
|
+
end_header
|
|
40
|
+
1.0 2.0 3.0 255 0 0
|
|
41
|
+
4.0 5.0 6.0 0 255 0
|
|
42
|
+
7.0 8.0 9.0 0 0 255
|
|
43
|
+
3 0 1 2"""
|
|
44
|
+
|
|
45
|
+
result = parse_ply_file_data(data.splitlines())
|
|
46
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
47
|
+
self.assertEqual(len(vertices), 3)
|
|
48
|
+
self.assertEqual(len(faces), 1)
|
|
49
|
+
self.assertEqual(len(colors), 3)
|
|
50
|
+
|
|
51
|
+
def test_distinguish_and_count_lines_for_vertices_and_faces(self):
|
|
52
|
+
data = """ply
|
|
53
|
+
format ascii 1.0
|
|
54
|
+
element vertex 3
|
|
55
|
+
property float x
|
|
56
|
+
property float y
|
|
57
|
+
property float z
|
|
58
|
+
element face 3
|
|
59
|
+
property list uchar int vertex_indices
|
|
60
|
+
end_header
|
|
61
|
+
1.0 2.0 3.0
|
|
62
|
+
4.0 5.0 6.0
|
|
63
|
+
7.0 8.0 9.0
|
|
64
|
+
3 2 1 2
|
|
65
|
+
3 2 2 3
|
|
66
|
+
3 1 2 3"""
|
|
67
|
+
|
|
68
|
+
result = parse_ply_file_data(data.splitlines())
|
|
69
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
70
|
+
self.assertEqual(len(vertices), 3)
|
|
71
|
+
self.assertEqual(vertices, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
72
|
+
self.assertEqual(len(faces), 3)
|
|
73
|
+
self.assertEqual(faces, [[2, 1, 2], [2, 2, 3], [1, 2, 3]])
|
|
74
|
+
self.assertEqual(len(colors), 0)
|
|
75
|
+
|
|
76
|
+
def test_handle_face_lines_starting_with_4(self):
|
|
77
|
+
data = """ply
|
|
78
|
+
format ascii 1.0
|
|
79
|
+
element vertex 3
|
|
80
|
+
property float x
|
|
81
|
+
property float y
|
|
82
|
+
property float z
|
|
83
|
+
element face 3
|
|
84
|
+
property list uchar int vertex_indices
|
|
85
|
+
end_header
|
|
86
|
+
1.0 2.0 3.0
|
|
87
|
+
4.0 5.0 6.0
|
|
88
|
+
7.0 8.0 9.0
|
|
89
|
+
4 2 1 2 2
|
|
90
|
+
4 2 2 3 1
|
|
91
|
+
4 1 2 3 2"""
|
|
92
|
+
|
|
93
|
+
result = parse_ply_file_data(data.splitlines())
|
|
94
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
95
|
+
self.assertEqual(len(vertices), 3)
|
|
96
|
+
self.assertEqual(vertices, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
97
|
+
self.assertEqual(len(faces), 3)
|
|
98
|
+
self.assertEqual(faces, [[2, 1, 2, 2], [2, 2, 3, 1], [1, 2, 3, 2]])
|
|
99
|
+
self.assertEqual(len(colors), 0)
|
|
100
|
+
|
|
101
|
+
def test_parse_ply_data_with_no_vertices_or_faces(self):
|
|
102
|
+
data = """ply
|
|
103
|
+
format ascii 1.0
|
|
104
|
+
element vertex 0
|
|
105
|
+
element face 0
|
|
106
|
+
end_header"""
|
|
107
|
+
|
|
108
|
+
result = parse_ply_file_data(data.splitlines())
|
|
109
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
110
|
+
self.assertEqual(len(vertices), 0)
|
|
111
|
+
self.assertEqual(len(faces), 0)
|
|
112
|
+
self.assertEqual(len(colors), 0)
|
|
113
|
+
|
|
114
|
+
def test_handle_invalid_ply_format(self):
|
|
115
|
+
invalid_format_data = """ply
|
|
116
|
+
format binary 1.0
|
|
117
|
+
element vertex 3
|
|
118
|
+
property float x
|
|
119
|
+
property float y
|
|
120
|
+
property float z
|
|
121
|
+
end_header
|
|
122
|
+
1.0 2.0 3.0
|
|
123
|
+
4.0 5.0 6.0
|
|
124
|
+
7.0 8.0 9.0"""
|
|
125
|
+
|
|
126
|
+
with self.assertRaises(ValueError) as context:
|
|
127
|
+
parse_ply_file_data(invalid_format_data.splitlines())
|
|
128
|
+
self.assertEqual(str(context.exception), 'File format is not ascii! It is: binary')
|
|
129
|
+
|
|
130
|
+
def test_handle_incomplete_vertex_data(self):
|
|
131
|
+
incomplete_vertex_data = """ply
|
|
132
|
+
format ascii 1.0
|
|
133
|
+
element vertex 3
|
|
134
|
+
property float x
|
|
135
|
+
property float y
|
|
136
|
+
property float z
|
|
137
|
+
end_header
|
|
138
|
+
1.0 2.0 3.0
|
|
139
|
+
4.0 5.0 6.0"""
|
|
140
|
+
|
|
141
|
+
with self.assertRaises(ValueError) as context:
|
|
142
|
+
parse_ply_file_data(incomplete_vertex_data.splitlines())
|
|
143
|
+
self.assertEqual(str(context.exception), 'Error: total vertices read: 2 does not match expected vertices: 3')
|
|
144
|
+
|
|
145
|
+
def test_parse_ply_colors_data_if_described_in_header(self):
|
|
146
|
+
data = """ply
|
|
147
|
+
format ascii 1.0
|
|
148
|
+
element vertex 3
|
|
149
|
+
property float x
|
|
150
|
+
property float y
|
|
151
|
+
property float z
|
|
152
|
+
property uchar red
|
|
153
|
+
property uchar green
|
|
154
|
+
property uchar blue
|
|
155
|
+
element face 1
|
|
156
|
+
end_header
|
|
157
|
+
1.0 2.0 3.0 255 0 0
|
|
158
|
+
4.0 5.0 6.0 0 255 0
|
|
159
|
+
7.0 8.0 9.0 0 0 255
|
|
160
|
+
3 0 1 2"""
|
|
161
|
+
|
|
162
|
+
result = parse_ply_file_data(data.splitlines())
|
|
163
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
164
|
+
self.assertEqual(len(vertices), 3)
|
|
165
|
+
self.assertEqual(len(faces), 1)
|
|
166
|
+
self.assertEqual(len(colors), 3)
|
|
167
|
+
self.assertEqual(colors, [[255, 0, 0], [0, 255, 0], [0, 0, 255]])
|
|
168
|
+
|
|
169
|
+
def test_not_parse_colors_if_color_property_not_described(self):
|
|
170
|
+
data = """ply
|
|
171
|
+
format ascii 1.0
|
|
172
|
+
element vertex 3
|
|
173
|
+
property float x
|
|
174
|
+
property float y
|
|
175
|
+
property float z
|
|
176
|
+
element face 1
|
|
177
|
+
end_header
|
|
178
|
+
1.0 2.0 3.0 255 0 0
|
|
179
|
+
4.0 5.0 6.0 0 255 0
|
|
180
|
+
7.0 8.0 9.0 0 0 255
|
|
181
|
+
3 0 1 2"""
|
|
182
|
+
|
|
183
|
+
result = parse_ply_file_data(data.splitlines())
|
|
184
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
185
|
+
self.assertEqual(len(vertices), 3)
|
|
186
|
+
self.assertEqual(len(faces), 1)
|
|
187
|
+
self.assertEqual(len(colors), 0)
|
|
188
|
+
|
|
189
|
+
def test_parse_ply_data_with_negative_coordinates(self):
|
|
190
|
+
data = """ply
|
|
191
|
+
format ascii 1.0
|
|
192
|
+
element vertex 3
|
|
193
|
+
property float x
|
|
194
|
+
property float y
|
|
195
|
+
property float z
|
|
196
|
+
element face 1
|
|
197
|
+
end_header
|
|
198
|
+
-1.0 -2.0 -3.0
|
|
199
|
+
-4.0 -5.0 -6.0
|
|
200
|
+
-7.0 -8.0 -9.0
|
|
201
|
+
3 0 1 2"""
|
|
202
|
+
|
|
203
|
+
result = parse_ply_file_data(data.splitlines())
|
|
204
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
205
|
+
self.assertEqual(len(vertices), 3)
|
|
206
|
+
self.assertEqual(len(faces), 1)
|
|
207
|
+
self.assertEqual(len(colors), 0)
|
|
208
|
+
|
|
209
|
+
def test_handle_incomplete_face_data(self):
|
|
210
|
+
incomplete_face_data = """ply
|
|
211
|
+
format ascii 1.0
|
|
212
|
+
element vertex 3
|
|
213
|
+
element face 2
|
|
214
|
+
end_header
|
|
215
|
+
1.0 2.0 3.0
|
|
216
|
+
4.0 5.0 6.0
|
|
217
|
+
7.0 8.0 9.0
|
|
218
|
+
3 0 1 2"""
|
|
219
|
+
|
|
220
|
+
with self.assertRaises(ValueError) as context:
|
|
221
|
+
parse_ply_file_data(incomplete_face_data.splitlines())
|
|
222
|
+
self.assertEqual(str(context.exception), 'Error: total faces read: 1 does not match expected faces: 2')
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == '__main__':
|
|
226
|
+
unittest.main()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ply-parser
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: A python module that parses ascii PLY (.ply) (Polygon File Format) files. It includes a class PLYObject for storing PLY file data and a function parse_ply_file() for reading PLY files and creating PLYObject instances and can easily be customized.
|
|
5
|
+
Home-page: https://github.com/Qrishna/ply_parser
|
|
6
|
+
Author: Krishna Bhattarai
|
|
7
|
+
Author-email: Krishna Bhattarai <krishna@kbhattarai.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/Qrishna/ply_parser
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/Qrishna/ply_parser/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.11.4
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
### PLY File Parser
|
|
17
|
+
This Python module provides a simple PLY (Polygon File Format) file parser. It includes a class PLYObject for storing PLY file data and a function parse_ply_file for reading PLY files and creating PLYObject instances.
|
|
18
|
+
|
|
19
|
+
### Usage
|
|
20
|
+
### PLYObject Class
|
|
21
|
+
The PLYObject class represents a PLY object with attributes:
|
|
22
|
+
|
|
23
|
+
`name`: Name of the PLY object.
|
|
24
|
+
|
|
25
|
+
`vertices`: List of vertex coordinates.
|
|
26
|
+
|
|
27
|
+
`faces`: List of face indices.
|
|
28
|
+
|
|
29
|
+
`colors`: List of RGB color values for vertices.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#### parse_ply_file Function
|
|
33
|
+
The parse_ply_file function takes a PLY file as input and returns a PLYObject instance. It reads the file, extracts vertex and face information, and handles color properties. If any inconsistencies or errors are detected in the PLY file, appropriate error messages are displayed.
|
|
34
|
+
```
|
|
35
|
+
file = "data/cube_colors.ply"
|
|
36
|
+
parsed_object = parse_ply_file(file)
|
|
37
|
+
if parsed_object:
|
|
38
|
+
print("name:", parsed_object.name)
|
|
39
|
+
print("total_vertices:", len(parsed_object.vertices))
|
|
40
|
+
print("total_faces:", len(parsed_object.faces))
|
|
41
|
+
print("total_colors:", len(parsed_object.colors))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Requirements
|
|
45
|
+
`Python 3.11.x`
|
|
46
|
+
|
|
47
|
+
### PLY File Format Support
|
|
48
|
+
This parser currently supports PLY files with the following characteristics:
|
|
49
|
+
|
|
50
|
+
`ASCII format`
|
|
51
|
+
|
|
52
|
+
`Vertex coordinates (X, Y, Z)`
|
|
53
|
+
|
|
54
|
+
`Face indices (triangles and quads)`
|
|
55
|
+
|
|
56
|
+
`Vertex colors`
|
|
57
|
+
|
|
58
|
+
### Testing
|
|
59
|
+
```
|
|
60
|
+
python3 -m unittest tests/test_ply_parser.py -v
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Limitations
|
|
64
|
+
The parser assumes the input PLY file follows the standard specifications.
|
|
65
|
+
It may not handle non-standard or corrupted PLY files gracefully.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
### Contribution
|
|
69
|
+
Feel free to contribute by opening issues or submitting pull requests. Bug reports, suggestions, and improvements are welcome.
|
|
70
|
+
|
|
71
|
+
### License
|
|
72
|
+
This PLY file parser is licensed under the MIT License. See the LICENSE file for details.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/ply_parser/__init__.py
|
|
5
|
+
src/ply_parser/ply_parser.py
|
|
6
|
+
src/ply_parser/test_ply_parser.py
|
|
7
|
+
src/ply_parser.egg-info/PKG-INFO
|
|
8
|
+
src/ply_parser.egg-info/SOURCES.txt
|
|
9
|
+
src/ply_parser.egg-info/dependency_links.txt
|
|
10
|
+
src/ply_parser.egg-info/top_level.txt
|
|
11
|
+
tests/test_ply_parser.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ply_parser
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from ply_parser import *
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestPLYParsing(unittest.TestCase):
|
|
6
|
+
def test_parse_ply_data_with_vertices_and_faces(self):
|
|
7
|
+
data = """ply
|
|
8
|
+
format ascii 1.0
|
|
9
|
+
comment Some comment
|
|
10
|
+
element vertex 3
|
|
11
|
+
property float x
|
|
12
|
+
property float y
|
|
13
|
+
property float z
|
|
14
|
+
element face 1
|
|
15
|
+
end_header
|
|
16
|
+
1.0 2.0 3.0
|
|
17
|
+
4.0 5.0 6.0
|
|
18
|
+
7.0 8.0 9.0
|
|
19
|
+
3 0 1 2"""
|
|
20
|
+
|
|
21
|
+
result = parse_ply_file_data(data.splitlines())
|
|
22
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
23
|
+
self.assertEqual(len(vertices), 3)
|
|
24
|
+
self.assertEqual(len(faces), 1)
|
|
25
|
+
self.assertEqual(len(colors), 0)
|
|
26
|
+
|
|
27
|
+
def test_parse_ply_data_with_vertices_colors_and_faces(self):
|
|
28
|
+
data = """ply
|
|
29
|
+
format ascii 1.0
|
|
30
|
+
element vertex 3
|
|
31
|
+
property float x
|
|
32
|
+
property float y
|
|
33
|
+
property float z
|
|
34
|
+
property uchar red
|
|
35
|
+
property uchar green
|
|
36
|
+
property uchar blue
|
|
37
|
+
element face 1
|
|
38
|
+
property list uchar int vertex_indices
|
|
39
|
+
end_header
|
|
40
|
+
1.0 2.0 3.0 255 0 0
|
|
41
|
+
4.0 5.0 6.0 0 255 0
|
|
42
|
+
7.0 8.0 9.0 0 0 255
|
|
43
|
+
3 0 1 2"""
|
|
44
|
+
|
|
45
|
+
result = parse_ply_file_data(data.splitlines())
|
|
46
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
47
|
+
self.assertEqual(len(vertices), 3)
|
|
48
|
+
self.assertEqual(len(faces), 1)
|
|
49
|
+
self.assertEqual(len(colors), 3)
|
|
50
|
+
|
|
51
|
+
def test_distinguish_and_count_lines_for_vertices_and_faces(self):
|
|
52
|
+
data = """ply
|
|
53
|
+
format ascii 1.0
|
|
54
|
+
element vertex 3
|
|
55
|
+
property float x
|
|
56
|
+
property float y
|
|
57
|
+
property float z
|
|
58
|
+
element face 3
|
|
59
|
+
property list uchar int vertex_indices
|
|
60
|
+
end_header
|
|
61
|
+
1.0 2.0 3.0
|
|
62
|
+
4.0 5.0 6.0
|
|
63
|
+
7.0 8.0 9.0
|
|
64
|
+
3 2 1 2
|
|
65
|
+
3 2 2 3
|
|
66
|
+
3 1 2 3"""
|
|
67
|
+
|
|
68
|
+
result = parse_ply_file_data(data.splitlines())
|
|
69
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
70
|
+
self.assertEqual(len(vertices), 3)
|
|
71
|
+
self.assertEqual(vertices, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
72
|
+
self.assertEqual(len(faces), 3)
|
|
73
|
+
self.assertEqual(faces, [[2, 1, 2], [2, 2, 3], [1, 2, 3]])
|
|
74
|
+
self.assertEqual(len(colors), 0)
|
|
75
|
+
|
|
76
|
+
def test_handle_face_lines_starting_with_4(self):
|
|
77
|
+
data = """ply
|
|
78
|
+
format ascii 1.0
|
|
79
|
+
element vertex 3
|
|
80
|
+
property float x
|
|
81
|
+
property float y
|
|
82
|
+
property float z
|
|
83
|
+
element face 3
|
|
84
|
+
property list uchar int vertex_indices
|
|
85
|
+
end_header
|
|
86
|
+
1.0 2.0 3.0
|
|
87
|
+
4.0 5.0 6.0
|
|
88
|
+
7.0 8.0 9.0
|
|
89
|
+
4 2 1 2 2
|
|
90
|
+
4 2 2 3 1
|
|
91
|
+
4 1 2 3 2"""
|
|
92
|
+
|
|
93
|
+
result = parse_ply_file_data(data.splitlines())
|
|
94
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
95
|
+
self.assertEqual(len(vertices), 3)
|
|
96
|
+
self.assertEqual(vertices, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
97
|
+
self.assertEqual(len(faces), 3)
|
|
98
|
+
self.assertEqual(faces, [[2, 1, 2, 2], [2, 2, 3, 1], [1, 2, 3, 2]])
|
|
99
|
+
self.assertEqual(len(colors), 0)
|
|
100
|
+
|
|
101
|
+
def test_parse_ply_data_with_no_vertices_or_faces(self):
|
|
102
|
+
data = """ply
|
|
103
|
+
format ascii 1.0
|
|
104
|
+
element vertex 0
|
|
105
|
+
element face 0
|
|
106
|
+
end_header"""
|
|
107
|
+
|
|
108
|
+
result = parse_ply_file_data(data.splitlines())
|
|
109
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
110
|
+
self.assertEqual(len(vertices), 0)
|
|
111
|
+
self.assertEqual(len(faces), 0)
|
|
112
|
+
self.assertEqual(len(colors), 0)
|
|
113
|
+
|
|
114
|
+
def test_handle_invalid_ply_format(self):
|
|
115
|
+
invalid_format_data = """ply
|
|
116
|
+
format binary 1.0
|
|
117
|
+
element vertex 3
|
|
118
|
+
property float x
|
|
119
|
+
property float y
|
|
120
|
+
property float z
|
|
121
|
+
end_header
|
|
122
|
+
1.0 2.0 3.0
|
|
123
|
+
4.0 5.0 6.0
|
|
124
|
+
7.0 8.0 9.0"""
|
|
125
|
+
|
|
126
|
+
with self.assertRaises(ValueError) as context:
|
|
127
|
+
parse_ply_file_data(invalid_format_data.splitlines())
|
|
128
|
+
self.assertEqual(str(context.exception), 'File format is not ascii! It is: binary')
|
|
129
|
+
|
|
130
|
+
def test_handle_incomplete_vertex_data(self):
|
|
131
|
+
incomplete_vertex_data = """ply
|
|
132
|
+
format ascii 1.0
|
|
133
|
+
element vertex 3
|
|
134
|
+
property float x
|
|
135
|
+
property float y
|
|
136
|
+
property float z
|
|
137
|
+
end_header
|
|
138
|
+
1.0 2.0 3.0
|
|
139
|
+
4.0 5.0 6.0"""
|
|
140
|
+
|
|
141
|
+
with self.assertRaises(ValueError) as context:
|
|
142
|
+
parse_ply_file_data(incomplete_vertex_data.splitlines())
|
|
143
|
+
self.assertEqual(str(context.exception), 'Error: total vertices read: 2 does not match expected vertices: 3')
|
|
144
|
+
|
|
145
|
+
def test_parse_ply_colors_data_if_described_in_header(self):
|
|
146
|
+
data = """ply
|
|
147
|
+
format ascii 1.0
|
|
148
|
+
element vertex 3
|
|
149
|
+
property float x
|
|
150
|
+
property float y
|
|
151
|
+
property float z
|
|
152
|
+
property uchar red
|
|
153
|
+
property uchar green
|
|
154
|
+
property uchar blue
|
|
155
|
+
element face 1
|
|
156
|
+
end_header
|
|
157
|
+
1.0 2.0 3.0 255 0 0
|
|
158
|
+
4.0 5.0 6.0 0 255 0
|
|
159
|
+
7.0 8.0 9.0 0 0 255
|
|
160
|
+
3 0 1 2"""
|
|
161
|
+
|
|
162
|
+
result = parse_ply_file_data(data.splitlines())
|
|
163
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
164
|
+
self.assertEqual(len(vertices), 3)
|
|
165
|
+
self.assertEqual(len(faces), 1)
|
|
166
|
+
self.assertEqual(len(colors), 3)
|
|
167
|
+
self.assertEqual(colors, [[255, 0, 0], [0, 255, 0], [0, 0, 255]])
|
|
168
|
+
|
|
169
|
+
def test_not_parse_colors_if_color_property_not_described(self):
|
|
170
|
+
data = """ply
|
|
171
|
+
format ascii 1.0
|
|
172
|
+
element vertex 3
|
|
173
|
+
property float x
|
|
174
|
+
property float y
|
|
175
|
+
property float z
|
|
176
|
+
element face 1
|
|
177
|
+
end_header
|
|
178
|
+
1.0 2.0 3.0 255 0 0
|
|
179
|
+
4.0 5.0 6.0 0 255 0
|
|
180
|
+
7.0 8.0 9.0 0 0 255
|
|
181
|
+
3 0 1 2"""
|
|
182
|
+
|
|
183
|
+
result = parse_ply_file_data(data.splitlines())
|
|
184
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
185
|
+
self.assertEqual(len(vertices), 3)
|
|
186
|
+
self.assertEqual(len(faces), 1)
|
|
187
|
+
self.assertEqual(len(colors), 0)
|
|
188
|
+
|
|
189
|
+
def test_parse_ply_data_with_negative_coordinates(self):
|
|
190
|
+
data = """ply
|
|
191
|
+
format ascii 1.0
|
|
192
|
+
element vertex 3
|
|
193
|
+
property float x
|
|
194
|
+
property float y
|
|
195
|
+
property float z
|
|
196
|
+
element face 1
|
|
197
|
+
end_header
|
|
198
|
+
-1.0 -2.0 -3.0
|
|
199
|
+
-4.0 -5.0 -6.0
|
|
200
|
+
-7.0 -8.0 -9.0
|
|
201
|
+
3 0 1 2"""
|
|
202
|
+
|
|
203
|
+
result = parse_ply_file_data(data.splitlines())
|
|
204
|
+
vertices, faces, colors = result[0], result[1], result[2]
|
|
205
|
+
self.assertEqual(len(vertices), 3)
|
|
206
|
+
self.assertEqual(len(faces), 1)
|
|
207
|
+
self.assertEqual(len(colors), 0)
|
|
208
|
+
|
|
209
|
+
def test_handle_incomplete_face_data(self):
|
|
210
|
+
incomplete_face_data = """ply
|
|
211
|
+
format ascii 1.0
|
|
212
|
+
element vertex 3
|
|
213
|
+
element face 2
|
|
214
|
+
end_header
|
|
215
|
+
1.0 2.0 3.0
|
|
216
|
+
4.0 5.0 6.0
|
|
217
|
+
7.0 8.0 9.0
|
|
218
|
+
3 0 1 2"""
|
|
219
|
+
|
|
220
|
+
with self.assertRaises(ValueError) as context:
|
|
221
|
+
parse_ply_file_data(incomplete_face_data.splitlines())
|
|
222
|
+
self.assertEqual(str(context.exception), 'Error: total faces read: 1 does not match expected faces: 2')
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == '__main__':
|
|
226
|
+
unittest.main()
|