Omar L
Add Flora inference examples
6a7a629 verified
Raw History Blame
5.14 kB
from dataclasses import dataclass
from fractions import Fraction
from pathlib import Path
import hashlib
import av
from .config import MAX_DURATION_S, MAX_DIMENSION, MAX_FRAME_COUNT
class InputError(ValueError):
pass
@dataclass(frozen=True)
class Video:
path: Path
sha256: str
rate: Fraction
pts_us: tuple
duration_us: int
size: tuple
cfr: bool
@property
def duration_s(self):
return self.duration_us / 1000000
def probe(path):
with av.open(str(path), options={'err_detect': 'explode'}) as container:
if len(container.streams.video) != 1:
raise InputError('Use a file containing one video stream.')
stream = container.streams.video[0]
stream.codec_context.thread_count = 1
rate = stream.average_rate
if not rate or rate < 4:
raise InputError('The input needs at least 4 source frames per second.')
size = (stream.codec_context.width, stream.codec_context.height)
if min(size) <= 0 or max(size) > MAX_DIMENSION:
raise InputError('Video dimensions must be at most 4096 pixels per side.')
times, zero, last_length = ([], None, None)
for frame in container.decode(stream):
if frame.pts is None:
raise InputError('Every source frame needs a presentation timestamp.')
zero = frame.pts if zero is None else zero
t = Fraction(frame.pts - zero) * stream.time_base * 1000000
if times and t <= times[-1]:
raise InputError('Source timestamps must be strictly increasing.')
if t >= MAX_DURATION_S * 1000000 or len(times) >= MAX_FRAME_COUNT:
raise InputError(f'Video exceeds {MAX_DURATION_S:g} seconds or {MAX_FRAME_COUNT} frames.')
times.append(t)
last_length = Fraction(frame.duration) * stream.time_base * 1000000 if frame.duration else None
if len(times) < 2:
raise InputError('The video contains too few source frames.')
tail = last_length or Fraction(1000000, rate)
duration = round(times[-1] + tail)
if duration > MAX_DURATION_S * 1000000:
raise InputError(f'Video exceeds {MAX_DURATION_S:g} seconds.')
cfr = all((abs(t - Fraction(i * 1000000, rate)) <= 1 for i, t in enumerate(times)))
with path.open('rb') as f:
digest = hashlib.file_digest(f, 'sha256').hexdigest()
return Video(path, digest, rate, tuple(times), duration, size, cfr)
def check_synchronized(videos):
main = videos['main']
if len(videos) == 3:
for role, video in videos.items():
if video.rate != main.rate or video.pts_us != main.pts_us or video.duration_us != main.duration_us:
raise InputError('Three camera files must have identical relative frame timestamps, frame counts and duration.')
if not main.cfr:
raise InputError('The three-view profile requires constant-rate synchronized source videos.')
def decode_selected(video, wanted, canonical=False):
wanted = set(wanted)
result = {}
if not wanted:
return result
if min(wanted) < 0 or max(wanted) >= len(video.pts_us):
raise InputError('A requested source frame is outside the video.')
with av.open(str(video.path), options={'err_detect': 'explode'}) as container:
stream = container.streams.video[0]
stream.codec_context.thread_count = 1
zero = None
for i, frame in enumerate(container.decode(stream)):
zero = frame.pts if zero is None else zero
if i in wanted:
if Fraction(frame.pts - zero) * stream.time_base * 1000000 != video.pts_us[i]:
raise InputError('Source frame timestamps changed while decoding.')
use_canonical = canonical and frame.format.name == 'yuv420p' and (frame.width % 16 == 0) and (frame.height % 2 == 0) and (int(frame.colorspace) in (2, 5, 6)) and (int(frame.color_range) in (0, 1))
if use_canonical:
from canonical_rgb import canonical_rgb
result[i] = canonical_rgb(frame)
else:
result[i] = frame.to_image().convert('RGB')
if i >= max(wanted):
break
if set(result) != wanted:
raise InputError('A requested source frame could not be decoded.')
return result
def frames_for(videos, wanted, mosaic='YAM'):
frames = {role: decode_selected(video, wanted, canonical=len(videos) == 3) for role, video in videos.items()}
if len(videos) == 1:
return frames['main']
from composers import compose_top_dominant_mosaic, compose_frame
if mosaic == 'YAM':
return {i: compose_top_dominant_mosaic(frames['main'][i], frames['left_wrist'][i], frames['right_wrist'][i]) for i in wanted}
if mosaic == 'Dex':
return {i: compose_frame(dict(ego=frames['main'][i], left_wrist=frames['left_wrist'][i], right_wrist=frames['right_wrist'][i]), ('ego', 'left_wrist', 'right_wrist')) for i in wanted}
raise InputError('Choose a supported camera layout.')