Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions opensfm/exif.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import datetime
import logging
from codecs import decode, encode
import math
from typing import Any, BinaryIO, Callable, Dict, List, Optional, Tuple, Union

import exifread
Expand Down Expand Up @@ -60,22 +61,52 @@ def get_tag_as_float(tags: Dict[str, Any], key: str, index: int = 0) -> Optional
return None


def focal35_to_focal_ratio(
focal35_or_ratio: float, width: int, height: int, inverse=False
) -> float:
"""
Convert focal length in 35mm film equivalent to focal ratio (and vice versa).
We follow https://en.wikipedia.org/wiki/35_mm_equivalent_focal_length
"""
image_ratio = float(max(width, height)) / min(width, height)
is_32 = math.fabs(image_ratio - 3.0 / 2.0)
is_43 = math.fabs(image_ratio - 4.0 / 3.0)
if is_32 < is_43:
Comment on lines +71 to +74
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wikipedia article mentions a definition that works for any aspect ratio in the "Calculation" section

"Converted focal length into 35 mm camera" = (Diagonal distance of image area in the 35 mm camera (43.27 mm) / Diagonal distance of image area on the image sensor of the DSC) × focal length of the lens of the DSC.

Should we use that instead so that we support any aspect ratio rather than picking between these two?
I guess the question is which definition do camera manufacturers use to fill the EXIF tag.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but in the end, everywhere else in the code, we use a ratio of focal / width (and not focal / diagonal), so we'll have to specify this width anyway.

Copy link
Copy Markdown
Contributor

@JakeSmarter JakeSmarter May 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have made a valid observation. Thank you for bringing this up. However, imho the focal length conversion should be based on diagonal sensor size in order to correctly and transparently support all aspect ratios and sensor dimensions. I would suggest for your PR to focus on propagating this aspect though the rest of the code rather than trying to revert it or add separate code paths for each aspect ratio.

I guess the question is which definition do camera manufacturers use to fill the EXIF tag.

Although it is not explicitly specified in the Exif spec, afaik they assume diagonal equivalency, exactly due to different sensor aspect ratios. I have tried to address this in #599.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CIPA DCG-001-Translation- 2005-12-

(2 – c) “Conversion of focal length of DSC lens into 35mm camera equivalent”

The focal length of a DSC lens converted into that of 35mm camera lens which has the same input field angle as the DSC lens.
Rules for notation

  • a) “Converted into 35mm camera” or similar wording should be noted.
  • b) The value is to be calculated using the following equation;
    “Converted focal length into 35mm camera” = Diagonal distance of image area in the 35mm camera (43.27mm) ÷ Diagonal distance of image area on the image sensor of the DSC × focal length of the lens of the DSC
  • c) All of the numerical values can be rounded off to two decimal places.

Copy link
Copy Markdown
Contributor

@JakeSmarter JakeSmarter May 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also do not see much reason for the is_32 and is_43 variables, unless something eludes me. If you are trying to determine the image orientation then test for the Orientation or PixelXDimension/PixelYDimension Exif tags. If these are not present then it is safe to assume that width ≥ height by standard definition. Again, unless an image has been deliberately cropped into an unusual aspect ratio than the original image or the sensor has non-square pixels. But, these are obscure cases.

# 3:2 aspect ratio : use 36mm for 35mm film
film_width = 36.0
if inverse:
return focal35_or_ratio * film_width
else:
return focal35_or_ratio / film_width
else:
# 4:3 aspect ratio : use 34mm for 35mm film
film_width = 34
if inverse:
return focal35_or_ratio * film_width
else:
return focal35_or_ratio / film_width


def compute_focal(
pixel_width: int,
pixel_height: int,
focal_35: Optional[float],
focal: Optional[float],
sensor_width: Optional[float],
sensor_string: Optional[str],
) -> Tuple[float, float]:
if focal_35 is not None and focal_35 > 0:
focal_ratio = focal_35 / 36.0 # 35mm film produces 36x24mm pictures.
focal_ratio = focal35_to_focal_ratio(focal_35, pixel_width, pixel_height)
else:
if not sensor_width:
sensor_width = (
sensor_data().get(sensor_string, None) if sensor_string else None
)
if sensor_width and focal:
focal_ratio = focal / sensor_width
focal_35 = 36.0 * focal_ratio
focal_35 = focal35_to_focal_ratio(
focal, pixel_width, pixel_height, inverse=True
)
else:
focal_35 = 0.0
focal_ratio = 0.0
Expand Down Expand Up @@ -248,7 +279,10 @@ def extract_projection_type(self) -> str:

def extract_focal(self) -> Tuple[float, float]:
make, model = self.extract_make(), self.extract_model()
width, height = self.extract_image_size()
focal_35, focal_ratio = compute_focal(
width,
height,
get_tag_as_float(self.tags, "EXIF FocalLengthIn35mmFilm"),
get_tag_as_float(self.tags, "EXIF FocalLength"),
self.extract_sensor_width(),
Expand Down Expand Up @@ -636,7 +670,13 @@ def extract_exif(self) -> Dict[str, Any]:

def hard_coded_calibration(exif: Dict[str, Any]) -> Optional[Dict[str, Any]]:
focal = exif["focal_ratio"]
fmm35 = int(round(focal * 36.0))
fmm35 = int(
round(
focal35_to_focal_ratio(
focal, int(exif["width"]), int(exif["height"]), inverse=True
)
)
)
make = exif["make"].strip().lower()
model = exif["model"].strip().lower()
raw_calibrations = camera_calibration()[0]
Expand Down
Loading