-
Notifications
You must be signed in to change notification settings - Fork 0
Support DJI distortion params #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
48218d8
074b5ab
707cb6a
a7d95f8
3150e6c
dd07f28
e1c3409
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| from imgparse.types import ( | ||
| AltitudeSource, | ||
| Dimensions, | ||
| DistortionParams, | ||
| Euler, | ||
| PixelCoords, | ||
| Version, | ||
|
|
@@ -206,20 +207,26 @@ def pixel_pitch_meters(self) -> float: | |
|
|
||
| return pixel_pitch | ||
|
|
||
| def focal_length_meters(self, use_calibrated: bool = False) -> float: | ||
| def calibrated_focal_length(self) -> tuple[float, bool]: | ||
| """ | ||
| Get the focal length (in meters) of the sensor that took the image. | ||
| Get the calibrated focal length from xmp data. | ||
|
|
||
| :param use_calibrated: enable to use calibrated focal length if available | ||
| For Sentera sensors, this focal length is in meters. For DJI, it is in pixels. Returns | ||
| a boolean indicating if focal length is in pixels or not. | ||
| """ | ||
| if use_calibrated: | ||
| try: | ||
| return float(self.xmp_data[self.xmp_tags.FOCAL_LEN]) / 1000 | ||
| except KeyError: | ||
| logger.warning( | ||
| "Calibrated focal length not found in XMP. Defaulting to uncalibrated focal length" | ||
| ) | ||
| try: | ||
| fl = float(self.xmp_data[self.xmp_tags.FOCAL_LEN]) | ||
| if self.make() == "Sentera": | ||
| is_in_pixels = False | ||
| fl = fl / 1000 | ||
| else: | ||
| is_in_pixels = True | ||
| return fl, is_in_pixels | ||
| except KeyError: | ||
| raise ParsingError("Calibrated focal length not found in XMP") | ||
|
|
||
| def focal_length_meters(self) -> float: | ||
| """Get the focal length (in meters) of the sensor that took the image.""" | ||
| try: | ||
| return convert_to_float(self.exif_data["EXIF FocalLength"]) / 1000 | ||
| except KeyError: | ||
|
|
@@ -229,9 +236,24 @@ def focal_length_meters(self, use_calibrated: bool = False) -> float: | |
|
|
||
| def focal_length_pixels(self, use_calibrated_focal_length: bool = False) -> float: | ||
| """Get the focal length (in pixels) of the sensor that took the image.""" | ||
| fl = self.focal_length_meters(use_calibrated_focal_length) | ||
| pp = self.pixel_pitch_meters() | ||
| return fl / pp | ||
|
|
||
| def _get_focal_length() -> tuple[float, bool]: | ||
| """Get either the calibrated focal length or the exif focal length.""" | ||
| if use_calibrated_focal_length: | ||
| try: | ||
| return self.calibrated_focal_length() | ||
| except ParsingError: | ||
| logger.warning( | ||
| "Couldn't parse calibrated focal length from xmp. Falling back to exif" | ||
| ) | ||
| return self.focal_length_meters(), False | ||
|
|
||
| fl, is_in_pixels = _get_focal_length() | ||
| if not is_in_pixels: | ||
| pp = self.pixel_pitch_meters() | ||
| return fl / pp | ||
|
|
||
| return fl | ||
|
|
||
| def principal_point(self) -> PixelCoords: | ||
| """Get the principal point (x, y) in pixels of the sensor that took the image.""" | ||
|
|
@@ -251,15 +273,34 @@ def principal_point(self) -> PixelCoords: | |
| "Couldn't find the principal point tag. Sensor might not be supported" | ||
| ) | ||
|
|
||
| def distortion_parameters(self) -> list[float]: | ||
| """Get the radial distortion parameters of the sensor that took the image.""" | ||
| def distortion_parameters(self) -> DistortionParams: | ||
| """ | ||
| Get the radial distortion parameters of the sensor that took the image. | ||
|
|
||
| Returns distortion params in [k1, k2, p1, p2, k3] order. | ||
| """ | ||
| try: | ||
| return list( | ||
| map(float, str(self.xmp_data[self.xmp_tags.DISTORTION]).split(",")) | ||
| ) | ||
| if self.make() == "DJI": | ||
| distortion_data = str(self.xmp_data[self.xmp_tags.DISTORTION]) | ||
|
|
||
| parts = distortion_data.split(";") | ||
| if len(parts) != 2: | ||
| raise ValueError("Invalid dewarp data format: missing semicolon") | ||
|
|
||
| values = [float(v) for v in parts[1].split(",")] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is inconsistent with the Could you update the Sentera approach in 298 to be consistent with the list comprehension used here?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. Also updated the return type to a NamedTuple in OpenCV order |
||
| if len(values) != 9: | ||
| raise ValueError("Expected 9 numeric values after semicolon") | ||
|
|
||
| k1, k2, p1, p2, k3 = values[4:9] | ||
| return DistortionParams(k1, k2, p1, p2, k3) | ||
| elif self.make() == "Sentera": | ||
| distortion_data = str(self.xmp_data[self.xmp_tags.DISTORTION]) | ||
| k1, k2, k3, p1, p2 = [float(v) for v in distortion_data.split(",")] | ||
| return DistortionParams(k1, k2, p1, p2, k3) | ||
| raise ValueError("Sensor isn't supported") | ||
| except (KeyError, ValueError): | ||
| raise ParsingError( | ||
| "Couldn't find the distortion tag. Sensor might not be supported" | ||
| "Couldn't parse the distortion parameters. Sensor might not be supported" | ||
| ) | ||
|
|
||
| def location(self) -> WorldCoords: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding more detailed documentation on the expected format of DJI distortion data (e.g., semicolon-separated header and comma-separated numeric values) to aid future maintainers.