Omar L
Add Flora inference examples (#1)
a4ba82a
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.')