Add get_image_info_metadata function for image metadata retrieval.

- Added a new function to retrieve metadata from image files.
- The function first tries OpenImageIO and then falls back to FFprobe if needed.
This commit is contained in:
Jakub Jezek 2024-06-06 16:36:43 +02:00
parent 5678b2f842
commit cdd0aa7795
No known key found for this signature in database
GPG key ID: 06DBD609ADF27FD9
6 changed files with 157 additions and 23 deletions

View file

@ -122,6 +122,7 @@ from .transcoding import (
convert_ffprobe_fps_value,
convert_ffprobe_fps_to_float,
get_rescaled_command_arguments,
get_image_info_metadata,
)
from .plugin_tools import (
@ -159,15 +160,11 @@ __all__ = [
"get_local_site_id",
"get_ayon_username",
"get_openpype_username",
"initialize_ayon_connection",
"CacheItem",
"NestedCacheItem",
"emit_event",
"register_event_callback",
"get_ayon_launcher_args",
"get_openpype_execute_args",
"get_linux_launcher_args",
@ -178,10 +175,8 @@ __all__ = [
"run_openpype_process",
"path_to_subprocess_arg",
"CREATE_NO_WINDOW",
"env_value_to_bool",
"get_paths_from_environ",
"ToolNotFoundError",
"find_executable",
"get_oiio_tools_path",
@ -189,13 +184,10 @@ __all__ = [
"get_ffmpeg_tool_path",
"get_ffmpeg_tool_args",
"is_oiio_supported",
"AbstractAttrDef",
"UIDef",
"UISeparatorDef",
"UILabelDef",
"UnknownDef",
"NumberDef",
"TextDef",
@ -203,14 +195,12 @@ __all__ = [
"BoolDef",
"FileDef",
"FileDefItem",
"import_filepath",
"modules_from_path",
"recursive_bases_from_class",
"classes_from_module",
"import_module_from_dirpath",
"is_func_signature_supported",
"get_transcode_temp_directory",
"should_convert_for_ffmpeg",
"convert_for_ffmpeg",
@ -222,33 +212,26 @@ __all__ = [
"convert_ffprobe_fps_value",
"convert_ffprobe_fps_to_float",
"get_rescaled_command_arguments",
"get_image_info_metadata",
"compile_list_of_regexes",
"filter_profiles",
"prepare_template_data",
"source_hash",
"format_file_size",
"collect_frames",
"create_hard_link",
"version_up",
"get_version_from_path",
"get_last_version_from_path",
"TemplateUnsolved",
"StringTemplate",
"FormatObject",
"terminal",
"get_datetime_data",
"get_timestamp",
"get_formatted_current_time",
"Logger",
"is_in_ayon_launcher_process",
"is_running_from_build",
"is_using_ayon_console",

View file

@ -834,6 +834,90 @@ def get_ffprobe_streams(path_to_file, logger=None):
return get_ffprobe_data(path_to_file, logger)["streams"]
def get_image_info_metadata(
path_to_file,
keys=None,
logger=None
):
"""Get metadata from image file.
At first it will try to detect if the image input is supported by
OpenImageIO. If it is then it gets the metadata from the image using
OpenImageIO. If it is not supported by OpenImageIO then it will try to
get the metadata using FFprobe.
Args:
path_to_file (str): Path to image file.
keys (list[str]): List of keys that should be returned. If None then
all keys are returned.
logger (logging.Logger): Logger used for logging.
"""
if logger is None:
logger = logging.getLogger(__name__)
def _ffprobe_metadata_conversion(metadata):
"""Convert ffprobe metadata unified format."""
output = {}
for key, val in metadata.items():
if key in ("tags", "disposition"):
output.update(val)
else:
output[key] = val
return output
metadata_stream = None
ext = os.path.splitext(path_to_file)[-1].lower()
if ext not in IMAGE_EXTENSIONS:
logger.info((
"File extension \"{}\" is not supported by OpenImageIO."
" Trying to get metadata using FFprobe."
).format(ext))
ffprobe_stream = get_ffprobe_data(path_to_file, logger)
if "streams" in ffprobe_stream and len(ffprobe_stream["streams"]) > 0:
metadata_stream = _ffprobe_metadata_conversion(
ffprobe_stream["streams"][0])
if not metadata_stream and is_oiio_supported():
oiio_stream = get_oiio_info_for_input(path_to_file, logger=logger)
if "attribs" in (oiio_stream or {}):
metadata_stream = {}
for key, val in oiio_stream["attribs"].items():
if "smpte:" in key.lower():
key = key.replace("smpte:", "")
metadata_stream[key.lower()] = val
for key, val in oiio_stream.items():
if key == "attribs":
continue
metadata_stream[key] = val
else:
logger.info((
"OpenImageIO is not supported on this system."
" Trying to get metadata using FFprobe."
))
ffprobe_stream = get_ffprobe_data(path_to_file, logger)
if "streams" in ffprobe_stream and len(ffprobe_stream["streams"]) > 0:
metadata_stream = _ffprobe_metadata_conversion(
ffprobe_stream["streams"][0])
if not metadata_stream:
logger.warning("Failed to get metadata from image file.")
return {}
if keys is None:
return metadata_stream
output = {}
for key in keys:
for k, v in metadata_stream.items():
if key == k:
output[key] = v
break
if isinstance(v, dict) and key in v:
output[key] = v[key]
break
return output
def get_ffmpeg_format_args(ffprobe_data, source_ffmpeg_cmd=None):
"""Copy format from input metadata for output.

View file

@ -1,4 +1,3 @@
import os
from pathlib import Path
from collections import defaultdict
@ -6,6 +5,7 @@ from qtpy import QtWidgets, QtCore, QtGui
from ayon_api import get_representations
from ayon_core.pipeline import load, Anatomy
from ayon_core.lib import get_image_info_metadata
from ayon_core import resources, style
from ayon_core.pipeline.load import get_representation_path_with_anatomy
from ayon_core.tools.utils import show_message_dialog
@ -44,6 +44,7 @@ class ExportOTIOOptionsDialog(QtWidgets.QDialog):
# Not all hosts have OpenTimelineIO available.
import opentimelineio as OTIO
self.OTIO = OTIO
self.log = log
super(ExportOTIOOptionsDialog, self).__init__(parent=parent)
@ -247,12 +248,23 @@ class ExportOTIOOptionsDialog(QtWidgets.QDialog):
# Get path to representation with correct frame number
repre_path = get_representation_path_with_anatomy(
representation, anatomy)
timecode_start_frame = 0
if file_metadata := get_image_info_metadata(
repre_path, ["timecode"], self.log
):
# use otio to convert timecode into frame number
timecode_start_frame = self.OTIO.opentime.from_timecode(
file_metadata["timecode"], framerate)
repre_path = Path(repre_path)
first_frame = representation["context"].get("frame")
if first_frame is None:
range = self.OTIO.opentime.TimeRange(
start_time=self.OTIO.opentime.RationalTime(0, framerate),
start_time=self.OTIO.opentime.RationalTime(
timecode_start_frame, framerate
),
duration=self.OTIO.opentime.RationalTime(frames, framerate),
)
# Use 'repre_path' as single file
@ -282,9 +294,11 @@ class ExportOTIOOptionsDialog(QtWidgets.QDialog):
frame_padding = len(frame_str)
range = self.OTIO.opentime.TimeRange(
start_time=self.OTIO.opentime.RationalTime(0, framerate),
start_time=self.OTIO.opentime.RationalTime(
timecode_start_frame.to_frames(), float(framerate)
),
duration=self.OTIO.opentime.RationalTime(
len(repre_files), framerate)
len(repre_files), framerate),
)
media_reference = self.OTIO.schema.ImageSequenceReference(

View file

@ -0,0 +1,53 @@
import pytest
import logging
from pathlib import Path
from ayon_core.lib.transcoding import get_image_info_metadata
logger = logging.getLogger('test_transcoding')
logger.setLevel(logging.DEBUG)
@pytest.mark.parametrize(
"resources_path_factory, metadata, expected, test_id",
[
(
Path(__file__).parent.parent
/ "resources"
/ "lib"
/ "transcoding"
/ "a01vfxd_sh010_plateP01_v002.1013.exr",
["timecode"],
{"timecode": "01:00:06:03"},
"test_01",
),
(
Path(__file__).parent.parent
/ "resources"
/ "lib"
/ "transcoding"
/ "a01vfxd_sh010_plateP01_v002.1013.exr",
["timecode", "width", "height", "duration"],
{"timecode": "01:00:06:03", "width": 1920, "height": 1080},
"test_02",
),
(
Path(__file__).parent.parent
/ "resources"
/ "lib"
/ "transcoding"
/ "a01vfxd_sh010_plateP01_v002.mov",
["width", "height", "duration"],
{"width": 1920, "height": 1080, "duration": "0.041708"},
"test_03",
),
],
)
def test_get_image_info_metadata_happy_path(
resources_path_factory, metadata, expected, test_id
):
path_to_file = resources_path_factory.as_posix()
returned_data = get_image_info_metadata(path_to_file, metadata, logger)
logger.debug(f"Returned data: {returned_data}")
assert returned_data == expected