Skip to content

Commit ec60ebe

Browse files
tomashynekpix4d_concourse_developers
and
pix4d_concourse_developers
authored
Publish pyopf (#12)
Co-authored-by: pix4d_concourse_developers <[email protected]>
1 parent 770ce65 commit ec60ebe

File tree

6 files changed

+107
-38
lines changed

6 files changed

+107
-38
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

7+
## 1.3.1
8+
9+
### Fixed
10+
11+
- `height` in ROI extension was fixed to `thickness` to comply with OPF specification
12+
- Fix bug causing GlTFPointCloud instances to inherit previous instance nodes
13+
714
## 1.3.0
815

916
### Added

examples/compute_reprojection_error.py

100644100755
Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,28 @@ def parse_args() -> argparse.Namespace:
185185
)
186186

187187
parser.add_argument(
188-
"opf_path", type=str, help="[REQUIRED] The path to your project.opf file."
188+
"--opf_path", type=str, help="[REQUIRED] The path to your project.opf file."
189189
)
190190

191-
return parser.parse_args()
191+
parser.add_argument(
192+
"--point_type",
193+
type=str,
194+
choices=["mtps", "gcps"],
195+
help="[REQUIRED] Wheter to use MTPs or GCPs",
196+
)
197+
198+
parser.add_argument(
199+
"--use_input_3d_coordinates",
200+
action="store_true",
201+
help="Use input 3d coordinates instead of calibrated ones. Only applicable if point_type is set to gcps",
202+
)
203+
204+
args = parser.parse_args()
205+
206+
if args.use_input_3d_coordinates and args.point_type == "mtps":
207+
raise ValueError("MTPs have no input 3d coordinates")
208+
209+
return args
192210

193211

194212
def main():
@@ -198,25 +216,37 @@ def main():
198216

199217
project = pyopf.resolve.resolve(pyopf.io.load(args.opf_path))
200218

201-
input_gcps = project.input_control_points.gcps
202-
projected_gcps = project.projected_control_points.projected_gcps
219+
if args.point_type == "mtps":
220+
input_points = project.input_control_points.mtps
221+
else:
222+
input_points = project.input_control_points.gcps
223+
224+
if args.use_input_3d_coordinates:
225+
projected_input_points = project.projected_control_points.projected_gcps
203226

204-
# alternatively, we can also use the optimized coordinates for the GCPs
205-
# projected_gcps = project.calibration.calibrated_control_points.points
227+
calibrated_control_points = project.calibration.calibrated_control_points.points
206228

207229
calibrated_cameras = project.calibration.calibrated_cameras.cameras
208230
sensors = project.calibration.calibrated_cameras.sensors
209231

210-
# == for all gcps, compute the reprojection error of all marks and the mean ==
232+
# == for all points, compute the reprojection error of all marks and the mean ==
211233

212-
for gcp in input_gcps:
234+
for point in input_points:
213235

214-
# get the corresponding projected gcp
215-
scene_gcp = find_object_with_given_id(projected_gcps, gcp.id)
236+
if args.use_input_3d_coordinates:
237+
scene_point = find_object_with_given_id(projected_input_points, point.id)
238+
else:
239+
scene_point = find_object_with_given_id(calibrated_control_points, point.id)
240+
241+
if scene_point is None:
242+
print(point.id, "not calibrated")
243+
continue
244+
245+
scene_point_3d_coordinates = scene_point.coordinates
216246

217247
all_reprojection_errors = []
218248

219-
for mark in gcp.marks:
249+
for mark in point.marks:
220250

221251
# find the corresponding calibrated camera
222252
calibrated_camera = find_object_with_given_id(
@@ -230,22 +260,25 @@ def main():
230260
internal_parameters = calibrated_sensor.internals
231261

232262
# project the 3d point on the image
233-
gcp_on_image = project_point(
234-
calibrated_camera, internal_parameters, scene_gcp.coordinates
263+
point_on_image = project_point(
264+
calibrated_camera, internal_parameters, scene_point_3d_coordinates
235265
)
236266

237267
# compute reprojection error
238-
reprojection_error = gcp_on_image - mark.position_px
268+
reprojection_error = point_on_image - mark.position_px
239269

240270
all_reprojection_errors.append(reprojection_error)
241271

242-
# compute the mean of the norm of the reprojection errors
243-
all_reprojection_errors = np.array(all_reprojection_errors)
244-
mean_reprojection_error = np.mean(
245-
np.apply_along_axis(np.linalg.norm, 1, all_reprojection_errors)
246-
)
272+
if len(all_reprojection_errors) > 0:
273+
# compute the mean of the norm of the reprojection errors
274+
all_reprojection_errors = np.array(all_reprojection_errors)
275+
mean_reprojection_error = np.mean(
276+
np.apply_along_axis(np.linalg.norm, 1, all_reprojection_errors)
277+
)
247278

248-
print(gcp.id, mean_reprojection_error)
279+
print(point.id, mean_reprojection_error)
280+
else:
281+
print(point.id, "no marks")
249282

250283

251284
if __name__ == "__main__":

pyproject.toml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"
66

77
[tool.poetry]
88
name = "pyopf"
9-
version = "1.3.0"
9+
version = "1.3.1"
1010
description = "Python library for I/O and manipulation of projects under the Open Photogrammetry Format (OPF)"
1111
authors = [
1212
"Pix4D",
@@ -39,6 +39,35 @@ pygltflib = "*"
3939
python-dateutil = "*"
4040
simplejson = "*"
4141

42+
[tool.poetry.dependencies.laspy]
43+
version = "2.4.1"
44+
optional = true
45+
46+
[tool.poetry.dependencies.plyfile]
47+
version = "0.9"
48+
optional = true
49+
50+
[tool.poetry.dependencies.pyproj]
51+
version = "3.6.0"
52+
optional = true
53+
54+
[tool.poetry.dependencies.shapely]
55+
version = "*"
56+
optional = true
57+
58+
[tool.poetry.dependencies.tqdm]
59+
version = "^4.65.0"
60+
optional = true
61+
62+
[tool.poetry.extras]
63+
tools = [
64+
"laspy",
65+
"plyfile",
66+
"pyproj",
67+
"shapely",
68+
"tqdm",
69+
]
70+
4271
[tool.poetry.scripts]
4372
opf_crop = "opf_tools.crop.cropper:main"
4473
opf_undistort = "opf_tools.undistort.undistorter:main"

src/opf_tools/crop/cropper.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class RoiPolygons:
4747
roi: Pix4DRegionOfInterest
4848
outer_boundary: Polygon
4949
inner_boundaries: list[Polygon]
50-
height: Optional[float]
50+
thickness: Optional[float]
5151

5252
def __init__(self, roi: Pix4DRegionOfInterest, matrix: Optional[np.ndarray] = None):
5353
"""Construct a RoiPolygons wrapper for a region of interest.
@@ -73,7 +73,7 @@ def __init__(self, roi: Pix4DRegionOfInterest, matrix: Optional[np.ndarray] = No
7373
_make_polygon(boundary, roi) for boundary in roi.plane.inner_boundaries
7474
]
7575

76-
self.height = roi.height
76+
self.thickness = roi.thickness
7777
self.matrix = matrix
7878

7979
self.roi = roi
@@ -94,14 +94,14 @@ def _is_inside_elevation_bounds(self, point: np.ndarray) -> bool:
9494
"""Check if a point is within the elevation bounds of the region of interest.
9595
The point must be in the same system of coordinates as the boudnaries.
9696
"""
97-
if self.height is None:
97+
if self.thickness is None:
9898
return True
9999

100100
elevation_difference = point[2] - self.roi.plane.vertices3d[0][2]
101101

102102
elevation_along_normal = elevation_difference * self.roi.plane.normal_vector[2]
103103

104-
return elevation_along_normal > 0 and elevation_along_normal < self.height
104+
return elevation_along_normal > 0 and elevation_along_normal < self.thickness
105105

106106
def is_inside(self, point: np.ndarray) -> bool:
107107
"""Check if a point is inside the ROI.

src/pyopf/ext/pix4d_region_of_interest.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,46 +11,45 @@
1111

1212

1313
class Pix4DRegionOfInterest(ExtensionItem):
14-
1514
"""Definition of a region of interest: a planar polygon with holes and an optional
16-
height, defined as a the distance from the plane in the normal direction. All the
15+
thickness, defined as a the distance from the plane in the normal direction. All the
1716
points on the hemispace where the normal lies that project inside the polygon and is at a
18-
distance less than the height of the ROI, is considered to be within.
17+
distance less than the thickness of the ROI, is considered to be within.
1918
"""
2019

2120
plane: Plane
22-
"""The height of the ROI volume, defined as a limit distance from the plane in the normal
23-
direction. If not specified, the height is assumed to be infinite.
21+
thickness: Optional[float]
22+
"""The thickness of the ROI volume, defined as a limit distance from the plane in the normal
23+
direction. If not specified, the thickness is assumed to be infinite.
2424
"""
25-
height: Optional[float]
2625

2726
def __init__(
2827
self,
2928
plane: Plane,
30-
height: Optional[float],
29+
thickness: Optional[float],
3130
pformat: ExtensionFormat = format,
3231
version: VersionInfo = version,
3332
) -> None:
3433
super(Pix4DRegionOfInterest, self).__init__(format=pformat, version=version)
3534

3635
assert self.format == format
3736
self.plane = plane
38-
self.height = height
37+
self.thickness = thickness
3938

4039
@staticmethod
4140
def from_dict(obj: Any) -> "Pix4DRegionOfInterest":
4241
base = ExtensionItem.from_dict(obj)
4342
plane = Plane.from_dict(obj["plane"])
44-
height = from_union([from_float, from_none], obj.get("height"))
45-
result = Pix4DRegionOfInterest(plane, height, base.format, base.version)
43+
thickness = from_union([from_float, from_none], obj.get("thickness"))
44+
result = Pix4DRegionOfInterest(plane, thickness, base.format, base.version)
4645
result._extract_unknown_properties_and_extensions(obj)
4746
return result
4847

4948
def to_dict(self) -> dict:
5049
result = super(Pix4DRegionOfInterest, self).to_dict()
5150
result["plane"] = to_class(Plane, self.plane)
52-
if self.height is not None:
53-
result["height"] = from_union([to_float, from_none], self.height)
51+
if self.thickness is not None:
52+
result["thickness"] = from_union([to_float, from_none], self.thickness)
5453
return result
5554

5655

src/pyopf/pointcloud/pcl.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ class GlTFPointCloud:
499499

500500
_format: CoreFormat
501501
_version: VersionInfo
502-
nodes: list[Node] = []
502+
nodes: list[Node]
503503
metadata: Optional["Metadata"] # noqa: F821 # type: ignore
504504

505505
mode_type = Literal[
@@ -560,6 +560,7 @@ def _open_accessors(
560560
return accessors
561561

562562
def __init__(self):
563+
self.nodes = []
563564
self._format = CoreFormat.GLTF_MODEL
564565
self._version = FormatVersion.GLTF_OPF_ASSET
565566

0 commit comments

Comments
 (0)