Skip to content

Commit 6d79fbf

Browse files
committed
relax rotation angle extraction process
1 parent a43db28 commit 6d79fbf

File tree

2 files changed

+116
-20
lines changed

2 files changed

+116
-20
lines changed

src/imars3d/backend/dataio/data.py

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ class load_data(param.ParameterizedFunction):
125125
126126
Currently, we are using a forgiving reader to load the image where a corrupted file
127127
will not block reading other data.
128+
129+
The rotation angles are extracted from the filenames if possible, otherwise from the
130+
metadata embedded in the tiff files. If both failed, the angle will be set to None.
128131
"""
129132

130133
#
@@ -544,7 +547,7 @@ def _get_filelist_by_dir(
544547
def _extract_rotation_angles(
545548
filelist: List[str],
546549
metadata_idx: int = 65039,
547-
) -> np.ndarray:
550+
) -> Optional[np.ndarray]:
548551
"""
549552
Extract rotation angles in degrees from filename or metadata.
550553
@@ -558,40 +561,106 @@ def _extract_rotation_angles(
558561
Returns
559562
-------
560563
rotation_angles
564+
Array of rotation angles if successfully extracted, None otherwise.
561565
"""
562566
# sanity check
563-
if filelist == []:
567+
if not filelist:
564568
logger.error("filelist is [].")
565569
raise ValueError("filelist cannot be empty list.")
566570

567-
# extract rotation angles from file names
571+
# process one file at a time
572+
rotation_angles = []
573+
for filename in filelist:
574+
file_ext = Path(filename).suffix.lower()
575+
angle = None
576+
if file_ext == ".tiff":
577+
# first, let's try to extract the angle from the filename
578+
angle = extract_rotation_angle_from_filename(filename)
579+
if angle is None:
580+
# if failed, try to extract from metadata
581+
angle = extract_rotation_angle_from_tiff_metadata(filename, metadata_idx)
582+
if angle is None:
583+
# if failed, log a warning and move on
584+
logger.warning(f"Failed to extract rotation angle from {filename}.")
585+
elif file_ext in (".tif", ".fits"):
586+
# for tif and fits, we can only extract from filename as the metadata is not reliable
587+
angle = extract_rotation_angle_from_filename(filename)
588+
if angle is None:
589+
# if failed, log a warning and move on
590+
logger.warning(f"Failed to extract rotation angle from {filename}.")
591+
else:
592+
# if the file type is not supported, raise value error
593+
logger.error(f"Unsupported file type: {file_ext}")
594+
raise ValueError("Unsupported file type.")
595+
596+
rotation_angles.append(angle)
597+
598+
# this means we have a list of None
599+
if all(angle is None for angle in rotation_angles):
600+
logger.warning("Failed to extract any rotation angles.")
601+
return None
602+
603+
# warn users if some angles are missing
604+
if any(angle is None for angle in rotation_angles):
605+
logger.warning("Some rotation angles are missing. You will see nan in the rotation angles array.")
606+
607+
return np.array(rotation_angles, dtype=float)
608+
609+
610+
def extract_rotation_angle_from_filename(filename: str) -> Optional[float]:
611+
"""
612+
Extract rotation angle in degrees from filename.
613+
614+
Parameters
615+
----------
616+
filename:
617+
Filename to extract rotation angle from.
618+
619+
Returns
620+
-------
621+
rotation_angle
622+
Rotation angle in degrees if successfully extracted, None otherwise.
623+
"""
624+
# extract rotation angle from file names
568625
# Note
569626
# ----
570627
# For the following file
571628
# 20191030_ironman_small_0070_300_440_0520.tif(f)
629+
# 20191030_ironman_small_0070_300_440_0520.fits
572630
# the rotation angle is 300.44 degrees
573-
# If all given filenames follows the pattern, we will use the angles from
574-
# filenames. Otherwise, we will use the angles from metadata.
575-
regex = r"\d{8}_\S*_\d{4}_(?P<deg>\d{3})_(?P<dec>\d{3})_\d*\.tif{1,2}"
576-
matches = [re.match(regex, Path(f).name) for f in filelist]
577-
if all(matches):
578-
logger.info("Using rotation angles from filenames.")
579-
rotation_angles = np.array([float(".".join(m.groups())) for m in matches])
631+
regex = r"\d{8}_\S*_\d{4}_(?P<deg>\d{3})_(?P<dec>\d{3})_\d*\.(?:tiff?|fits)"
632+
match = re.match(regex, Path(filename).name)
633+
if match:
634+
rotation_angle = float(".".join(match.groups()))
580635
else:
581-
# extract rotation angles from metadata
582-
file_exts = set(Path(f).suffix.lower() for f in filelist)
583-
if not file_exts.issubset({".tiff", ".tif"}):
584-
logger.error("Only .tiff and .tif files are supported.")
585-
raise ValueError("Rotation angle from metadata is only supported for .tiff and .tif files.")
636+
rotation_angle = None
637+
return rotation_angle
638+
639+
640+
def extract_rotation_angle_from_tiff_metadata(filename: str, metadata_idx: int = 65039) -> Optional[float]:
641+
"""
642+
Extract rotation angle in degrees from metadata of a tiff file.
643+
644+
Parameters
645+
----------
646+
filename:
647+
Filename to extract rotation angle from.
648+
metadata_idx:
649+
Index of metadata to extract rotation angle from, default is 65039.
650+
651+
Returns
652+
-------
653+
rotation_angle
654+
Rotation angle in degrees if successfully extracted, None otherwise.
655+
"""
656+
try:
586657
# -- read metadata
587658
# img = tifffile.TiffFile("test_with_metadata_0.tiff")
588659
# img.pages[0].tags[65039].value
589660
# >> 'RotationActual:0.579840'
590-
rotation_angles = np.array(
591-
[float(tifffile.TiffFile(f).pages[0].tags[metadata_idx].value.split(":")[-1]) for f in filelist],
592-
dtype="float",
593-
)
594-
return rotation_angles
661+
return float(tifffile.TiffFile(filename).pages[0].tags[metadata_idx].value.split(":")[-1])
662+
except Exception:
663+
return None
595664

596665

597666
def _save_data(filename: Path, data: np.ndarray, rot_angles: np.ndarray = None) -> None:

tests/unit/backend/dataio/test_data.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
load_data,
1616
save_checkpoint,
1717
save_data,
18+
extract_rotation_angle_from_filename,
19+
extract_rotation_angle_from_tiff_metadata,
1820
)
1921

2022

@@ -181,6 +183,31 @@ def test_extract_rotation_angles(data_fixture):
181183
rst = _extract_rotation_angles([metadata_tiff] * 3)
182184
ref = np.array([0.1, 0.1, 0.1])
183185
np.testing.assert_array_almost_equal(rst, ref)
186+
# case_2: mixed file types
187+
rst = _extract_rotation_angles([good_tiff, metadata_tiff, generic_tiff, generic_fits])
188+
ref = np.array([10.02, 0.1, np.nan, np.nan])
189+
np.testing.assert_array_equal(rst, ref)
190+
# case_3: all files without extractable angles
191+
rst = _extract_rotation_angles([generic_tiff, generic_fits])
192+
assert rst is None
193+
194+
195+
def test_extract_rotation_angle_from_filename():
196+
# Test cases for extract_rotation_angle_from_filename
197+
assert extract_rotation_angle_from_filename("20191030_sample_0070_300_440_0520.tiff") == 300.44
198+
assert extract_rotation_angle_from_filename("20191030_sample_0071_301_441_0521.tif") == 301.441
199+
assert extract_rotation_angle_from_filename("20191030_sample_0072_302_442_0522.fits") == 302.442
200+
assert extract_rotation_angle_from_filename("generic_file.tiff") is None
201+
202+
203+
def test_extract_rotation_angle_from_tiff_metadata(tmpdir):
204+
# Create a TIFF file with rotation angle in metadata
205+
data = np.ones((3, 3))
206+
filename = str(tmpdir / "metadata.tiff")
207+
tifffile.imwrite(filename, data, extratags=[(65039, "s", 0, "RotationActual:0.5", True)])
208+
209+
assert extract_rotation_angle_from_tiff_metadata(filename) == 0.5
210+
assert extract_rotation_angle_from_tiff_metadata("non_existent_file.tiff") is None
184211

185212

186213
@pytest.fixture(scope="module")

0 commit comments

Comments
 (0)