mirror of
https://github.com/iperov/DeepFaceLive
synced 2025-08-14 02:37:01 -07:00
code release
This commit is contained in:
parent
b941ba41a3
commit
a902f11f74
354 changed files with 826570 additions and 1 deletions
275
xlib/player/FramePlayer.py
Normal file
275
xlib/player/FramePlayer.py
Normal file
|
@ -0,0 +1,275 @@
|
|||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
from xlib.image import ImageProcessor
|
||||
from xlib.python import Disposable
|
||||
|
||||
|
||||
class FramePlayer(Disposable):
|
||||
"""
|
||||
Base class for players based on fixed number of frames
|
||||
"""
|
||||
|
||||
class Frame:
|
||||
__slots__ = ['image','timestamp','fps','frame_num','frame_count','name','error']
|
||||
|
||||
image : np.ndarray # if none - error during loading frame
|
||||
timestamp : float
|
||||
fps : float
|
||||
frame_num : int
|
||||
frame_count : int
|
||||
name : str
|
||||
error : str
|
||||
|
||||
def __init__(self):
|
||||
self.image = None
|
||||
self.timestamp = None
|
||||
self.fps = None
|
||||
self.frame_num = None
|
||||
self.frame_count = None
|
||||
self.name = None
|
||||
self.error = None
|
||||
|
||||
def __init__(self, default_fps, frame_count):
|
||||
if frame_count == 0:
|
||||
raise Exception('Frames count are 0.')
|
||||
|
||||
self._default_fps = default_fps
|
||||
self._frame_count = frame_count
|
||||
|
||||
self._is_realtime = True
|
||||
self._is_autorewind = False
|
||||
|
||||
self._fps = 0
|
||||
|
||||
self._target_width = 0
|
||||
|
||||
self._is_playing = False
|
||||
self._frame_idx = 0
|
||||
self._frame_timestamp = None
|
||||
self._req_is_playing = None
|
||||
self._req_frame_seek_idx = None
|
||||
|
||||
self._cached_frames = {}
|
||||
self._cached_frames_idxs = []
|
||||
|
||||
def is_playing(self): return self._is_playing
|
||||
def get_frame_count(self): return self._frame_count
|
||||
def get_frame_idx(self): return self._frame_idx
|
||||
|
||||
def get_is_autorewind(self): return self._is_autorewind
|
||||
def set_is_autorewind(self, is_autorewind):
|
||||
if not isinstance(is_autorewind, bool):
|
||||
raise ValueError('is_autorewind must be an instance of bool')
|
||||
self._is_autorewind = is_autorewind
|
||||
return self._is_autorewind
|
||||
|
||||
def get_is_realtime(self): return self._is_realtime
|
||||
def set_is_realtime(self, is_realtime):
|
||||
if not isinstance(is_realtime, bool):
|
||||
raise ValueError('is_realtime must be an instance of bool')
|
||||
self._is_realtime = is_realtime
|
||||
return self._is_realtime
|
||||
|
||||
def get_fps(self): return self._fps
|
||||
def set_fps(self, fps):
|
||||
"""
|
||||
set new FPS.
|
||||
Returns adjusted FPS.
|
||||
"""
|
||||
if not isinstance(fps, (int, float) ):
|
||||
raise ValueError('fps must be an instance of int/float')
|
||||
self._fps = float(np.clip(fps, 0, 240))
|
||||
self._on_fps_changed()
|
||||
return self._fps
|
||||
|
||||
def get_target_width(self): return self._target_width
|
||||
def set_target_width(self, target_width):
|
||||
"""
|
||||
0 - auto
|
||||
4..4096
|
||||
|
||||
returns adjusted target_width
|
||||
"""
|
||||
if not isinstance(target_width, (int,float) ):
|
||||
raise ValueError('target_width must be an instance of int/float')
|
||||
|
||||
target_width = int(target_width)
|
||||
target_width = (target_width // 4) * 4
|
||||
target_width = int(np.clip(target_width, 0, 4096))
|
||||
self._target_width = target_width
|
||||
self._on_target_width_changed()
|
||||
return self._target_width
|
||||
|
||||
def _on_play_start(self):
|
||||
"""@overridable"""
|
||||
def _on_play_stop(self):
|
||||
"""@overridable"""
|
||||
def _on_fps_changed(self):
|
||||
"""@overridable"""
|
||||
def _on_target_width_changed(self):
|
||||
"""@overridable"""
|
||||
|
||||
def _on_get_frame(self, idx) -> Tuple[np.ndarray, str]:
|
||||
"""
|
||||
@overridable
|
||||
|
||||
return (image_of_frame, name_or_error)
|
||||
|
||||
if image_of_frame is None, then specify an error to 'name_or_error'
|
||||
otherwise 'name_or_error' is a name of frame
|
||||
"""
|
||||
return None
|
||||
|
||||
def req_frame_seek(self, idx, mode):
|
||||
"""
|
||||
Request to seek to specified frame idx
|
||||
"""
|
||||
self._req_frame_seek_idx = (idx, mode)
|
||||
|
||||
|
||||
def req_play_start(self):
|
||||
"""
|
||||
Request to start playing.
|
||||
"""
|
||||
self._req_is_playing = True
|
||||
|
||||
def req_play_stop(self):
|
||||
"""
|
||||
Request to stop playing.
|
||||
"""
|
||||
self._req_is_playing = False
|
||||
|
||||
class ProcessResult:
|
||||
__slots__ = ['new_is_playing','new_frame_idx','new_frame']
|
||||
|
||||
def __init__(self):
|
||||
self.new_is_playing = None
|
||||
self.new_frame_idx = None
|
||||
self.new_frame = None
|
||||
|
||||
def process(self) -> 'FramePlayer.ProcessResult':
|
||||
"""
|
||||
processes inner logic
|
||||
|
||||
returns FramePlayer.ProcessResult()
|
||||
"""
|
||||
# Process player logic.
|
||||
new_is_playing = None
|
||||
new_frame_idx = None
|
||||
new_frame_timestamp = None
|
||||
update_frame = False
|
||||
|
||||
fps = self._fps
|
||||
if fps == 0:
|
||||
fps = self._default_fps
|
||||
|
||||
result = FramePlayer.ProcessResult()
|
||||
|
||||
if self._is_playing:
|
||||
if self._is_realtime:
|
||||
diff_frames = int( (datetime.now().timestamp() - self._frame_timestamp) / (1.0/fps) )
|
||||
if diff_frames != 0:
|
||||
new_frame_idx = self._frame_idx + diff_frames
|
||||
new_frame_timestamp = self._frame_timestamp + diff_frames * (1.0/fps)
|
||||
else:
|
||||
new_frame_idx = self._frame_idx + 1
|
||||
new_frame_timestamp = datetime.now().timestamp()
|
||||
|
||||
if self._req_frame_seek_idx is not None:
|
||||
# User frame seek overrides new frame idx
|
||||
seek_idx, seek_mode = self._req_frame_seek_idx
|
||||
if seek_mode == 0:
|
||||
new_frame_idx = seek_idx
|
||||
elif seek_mode == 1:
|
||||
new_frame_idx = self._frame_idx + seek_idx
|
||||
elif seek_mode == 2:
|
||||
new_frame_idx = self._frame_count - seek_idx -1
|
||||
|
||||
new_frame_timestamp = datetime.now().timestamp()
|
||||
|
||||
if new_frame_idx is not None:
|
||||
# new_frame_idx mean the frame should be updated, even if idx is not changed
|
||||
update_frame = True
|
||||
|
||||
if new_frame_idx < 0 or new_frame_idx >= self._frame_count:
|
||||
# End of frames reached
|
||||
if self._is_autorewind:
|
||||
# AutoRewind
|
||||
new_frame_idx %= self._frame_count
|
||||
else:
|
||||
# No AutoRewind. Stop at last frame, but don't update frame
|
||||
if new_frame_idx < 0:
|
||||
new_frame_idx = 0
|
||||
else:
|
||||
new_frame_idx = self._frame_count-1
|
||||
update_frame = False
|
||||
new_is_playing = False
|
||||
|
||||
if self._frame_idx != new_frame_idx:
|
||||
self._frame_idx = result.new_frame_idx = new_frame_idx
|
||||
|
||||
if new_frame_timestamp is not None:
|
||||
self._frame_timestamp = new_frame_timestamp
|
||||
|
||||
if new_is_playing is None:
|
||||
# new_is_playing is not changed by system, now can handle user request
|
||||
new_is_playing = self._req_is_playing
|
||||
|
||||
if new_is_playing is not None and new_is_playing and not self._is_playing:
|
||||
# Start playing
|
||||
result.new_is_playing = self._is_playing = True
|
||||
self._frame_timestamp = datetime.now().timestamp()
|
||||
self._on_play_start()
|
||||
update_frame = True
|
||||
|
||||
if update_frame:
|
||||
|
||||
# Frame changed, construct Frame() with current values
|
||||
_frame_idx = self._frame_idx
|
||||
_cached_frames = self._cached_frames
|
||||
_cached_frames_idxs = self._cached_frames_idxs
|
||||
|
||||
p_frame = result.new_frame = FramePlayer.Frame()
|
||||
p_frame.fps = fps
|
||||
p_frame.timestamp = self._frame_timestamp
|
||||
|
||||
p_frame.frame_num = _frame_idx
|
||||
p_frame.frame_count = self._frame_count
|
||||
|
||||
frame_tuple = _cached_frames.get(_frame_idx, None)
|
||||
if frame_tuple is None:
|
||||
frame_tuple = self._on_get_frame(_frame_idx)
|
||||
_cached_frames[_frame_idx] = frame_tuple
|
||||
_cached_frames_idxs.insert(0, _frame_idx)
|
||||
if len(_cached_frames_idxs) > 5:
|
||||
_cached_frames.pop(_cached_frames_idxs.pop(-1))
|
||||
|
||||
frame_image, name_or_err = frame_tuple
|
||||
|
||||
if frame_image is None:
|
||||
# frame is not provided, stop playing, but return p_frame without an image
|
||||
new_is_playing = False
|
||||
p_frame.error = name_or_err
|
||||
else:
|
||||
# frame is provided.
|
||||
ip = ImageProcessor(frame_image)
|
||||
if self._target_width != 0:
|
||||
ip.fit_in(TW=self._target_width)
|
||||
frame_image = ip.ch(3).get_image('HWC')
|
||||
|
||||
p_frame.image = frame_image
|
||||
p_frame.name = name_or_err
|
||||
|
||||
|
||||
if new_is_playing is not None and self._is_playing and not new_is_playing:
|
||||
# Stop playing
|
||||
result.new_is_playing = self._is_playing = False
|
||||
self._on_play_stop()
|
||||
|
||||
if self._req_is_playing is not None or self._req_frame_seek_idx is not None:
|
||||
self._req_is_playing = None
|
||||
self._req_frame_seek_idx = None
|
||||
|
||||
return result
|
69
xlib/player/ImageSequencePlayer.py
Normal file
69
xlib/player/ImageSequencePlayer.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
from xlib import cv as lib_cv
|
||||
from xlib import path as lib_path
|
||||
|
||||
from .FramePlayer import FramePlayer
|
||||
|
||||
|
||||
class ImageSequencePlayer(FramePlayer):
|
||||
"""
|
||||
Play image sequence folder.
|
||||
|
||||
arguments
|
||||
|
||||
dir_path path to directory
|
||||
|
||||
is_realtime(True) bool False - process every frame as fast as possible
|
||||
fps parameter will be ignored
|
||||
True - process in real time with desired fps
|
||||
|
||||
is_autorewind(True) bool
|
||||
|
||||
fps float specify fps
|
||||
|
||||
target_width(None) int if None : resolution will be not modified
|
||||
|
||||
raises
|
||||
|
||||
Exception path does not exists
|
||||
path has no image files
|
||||
"""
|
||||
SUPPORTED_IMAGE_SEQUENCE_SUFFIXES = ['.jpg','.png']
|
||||
|
||||
def __init__(self, dir_path,
|
||||
on_error_func=None,
|
||||
on_player_state_func=None,
|
||||
on_frame_update_func=None):
|
||||
|
||||
dir_path = Path(dir_path)
|
||||
if not dir_path.exists():
|
||||
raise Exception(f'{dir_path} does not exist.')
|
||||
|
||||
if not dir_path.is_dir():
|
||||
raise Exception(f'{dir_path} is not a directory.')
|
||||
|
||||
images_paths = lib_path.get_files_paths(dir_path, ImageSequencePlayer.SUPPORTED_IMAGE_SEQUENCE_SUFFIXES)
|
||||
if len(images_paths) == 0:
|
||||
raise Exception(f'Images with extensions {ImageSequencePlayer.SUPPORTED_IMAGE_SEQUENCE_SUFFIXES} are not found in directory: /{dir_path.name}/')
|
||||
|
||||
#
|
||||
super().__init__(default_fps=30, frame_count=len(images_paths) )
|
||||
|
||||
self._images_paths = images_paths
|
||||
self._dir_path = dir_path
|
||||
|
||||
|
||||
def _on_get_frame(self, idx) -> Tuple[np.ndarray, str]:
|
||||
filepath = self._images_paths[idx]
|
||||
|
||||
try:
|
||||
img = lib_cv.imread(filepath)
|
||||
return img, filepath.name
|
||||
except Exception as e:
|
||||
return None, 'cv2.imread error: '+str(e)
|
||||
|
||||
|
||||
|
178
xlib/player/VideoFilePlayer.py
Normal file
178
xlib/player/VideoFilePlayer.py
Normal file
|
@ -0,0 +1,178 @@
|
|||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
from xlib import ffmpeg as lib_ffmpeg
|
||||
|
||||
from .FramePlayer import FramePlayer
|
||||
|
||||
|
||||
class VideoFilePlayer(FramePlayer):
|
||||
"""
|
||||
Play video track from the video file using subprocess ffmpeg.
|
||||
|
||||
arguments
|
||||
|
||||
filepath str/Path path to video file
|
||||
|
||||
is_realtime(True) bool False - process every frame as fast as possible
|
||||
fps parameter will be ignored
|
||||
True - process in real time with desired fps
|
||||
|
||||
is_autorewind(True) bool
|
||||
|
||||
fps(None) float specify fps.
|
||||
None - video fps will be used
|
||||
|
||||
target_width(None) int if None : resolution will be not modified
|
||||
|
||||
raises
|
||||
|
||||
Exception path does not exists
|
||||
ffprobe failed
|
||||
file has no video tracks
|
||||
"""
|
||||
SUPPORTED_VIDEO_FILE_SUFFIXES = ['.avi','.mkv','.mp4']
|
||||
|
||||
def __init__(self, filepath):
|
||||
self._ffmpeg_proc = None
|
||||
|
||||
|
||||
self._filepath = filepath = Path(filepath)
|
||||
if not filepath.exists():
|
||||
raise Exception(f'{filepath} does not exist.')
|
||||
|
||||
if not filepath.is_file():
|
||||
raise Exception(f'{filepath} is not a file.')
|
||||
|
||||
if not filepath.suffix in VideoFilePlayer.SUPPORTED_VIDEO_FILE_SUFFIXES:
|
||||
raise Exception(f'Supported video files: {VideoFilePlayer.SUPPORTED_VIDEO_FILE_SUFFIXES}')
|
||||
|
||||
probe_info = lib_ffmpeg.probe (str(filepath))
|
||||
# Analize probe_info
|
||||
stream_idx = None
|
||||
stream_fps = None
|
||||
stream_width = None
|
||||
stream_height = None
|
||||
for stream in probe_info['streams']:
|
||||
if stream_idx is None and stream['codec_type'] == 'video':
|
||||
#print(stream)
|
||||
stream_idx = stream.get('index',None)
|
||||
if stream_idx is not None:
|
||||
stream_idx = int(stream_idx)
|
||||
stream_width = stream.get('width', None)
|
||||
if stream_width is not None:
|
||||
stream_width = int(stream_width)
|
||||
stream_height = stream.get('height', None)
|
||||
if stream_height is not None:
|
||||
stream_height = int(stream_height)
|
||||
stream_start_time = stream.get('start_time', None)
|
||||
stream_duration = stream.get('duration', None)
|
||||
stream_fps = stream.get('avg_frame_rate', None)
|
||||
if stream_fps is None:
|
||||
stream_fps = stream.get('r_frame_rate', None)
|
||||
if stream_fps is not None:
|
||||
stream_fps = eval(stream_fps)
|
||||
break
|
||||
|
||||
if any( x is None for x in [stream_idx, stream_width, stream_height, stream_start_time, stream_duration, stream_fps] ):
|
||||
raise Exception(f'Incorrect video file.')
|
||||
|
||||
stream_frame_count = round( ( float(stream_duration)-float(stream_start_time) ) / (1.0/stream_fps) )
|
||||
|
||||
self._stream_idx = stream_idx
|
||||
self._stream_width = stream_width
|
||||
self._stream_height = stream_height
|
||||
self._stream_fps = stream_fps
|
||||
self._ffmpeg_need_restart = False
|
||||
self._ffmpeg_frame_idx = -1
|
||||
|
||||
super().__init__(default_fps=stream_fps, frame_count=stream_frame_count)
|
||||
|
||||
def _on_dispose(self):
|
||||
self._ffmpeg_stop()
|
||||
super()._on_dispose()
|
||||
|
||||
def _ffmpeg_stop(self):
|
||||
if self._ffmpeg_proc is not None:
|
||||
self._ffmpeg_proc.kill()
|
||||
self._ffmpeg_proc = None
|
||||
|
||||
def _ffmpeg_restart(self, start_frame_number=0):
|
||||
#print('_ffmpeg_restart')
|
||||
self._ffmpeg_stop()
|
||||
|
||||
_target_width = self._target_width
|
||||
if _target_width == 0:
|
||||
_width = self._ffmpeg_width = self._stream_width
|
||||
_height = self._ffmpeg_height = self._stream_height
|
||||
else:
|
||||
_height = self._ffmpeg_height = int( _target_width / (self._stream_width / self._stream_height) )
|
||||
_width = self._ffmpeg_width = _target_width
|
||||
|
||||
args = []
|
||||
if start_frame_number != 0:
|
||||
# -ss before -i to fast and accurate seek
|
||||
# using time instead of frame, because '-vf select' does not work correctly with some videos
|
||||
args += ['-ss', str(start_frame_number*(1.0 / self._stream_fps)) ]
|
||||
|
||||
args += ['-i', str(self._filepath),
|
||||
'-s', f'{_width}:{_height}'
|
||||
]
|
||||
|
||||
# Set exact FPS for constant framerate
|
||||
args += ['-r', str(self._stream_fps)]
|
||||
|
||||
args += ['-f', 'rawvideo',
|
||||
'-pix_fmt', 'bgr24',
|
||||
'-map', f'0:v:{self._stream_idx}',
|
||||
'pipe:']
|
||||
|
||||
self._ffmpeg_proc = lib_ffmpeg.run (args, pipe_stdout=True, quiet_std_err=True)
|
||||
return self._ffmpeg_proc is not None
|
||||
|
||||
def _ffmpeg_next_frame(self, frames_idx_offset=1):
|
||||
frame_buffer = None
|
||||
|
||||
while frames_idx_offset != 0:
|
||||
frame_buffer = self._ffmpeg_proc.stdout.read(self._ffmpeg_height*self._ffmpeg_width*3)
|
||||
if len(frame_buffer) == 0:
|
||||
# End reached
|
||||
self._ffmpeg_stop()
|
||||
return None
|
||||
frames_idx_offset -= 1
|
||||
|
||||
if frame_buffer is not None:
|
||||
frame_image = np.ndarray( (self._ffmpeg_height, self._ffmpeg_width, 3), dtype=np.uint8, buffer=frame_buffer).copy()
|
||||
return frame_image
|
||||
return None
|
||||
|
||||
def _on_target_width_changed(self):
|
||||
self._ffmpeg_need_restart = True
|
||||
|
||||
def _on_get_frame(self, idx) -> Tuple[np.ndarray, str]:
|
||||
|
||||
frame_diff = idx - self._ffmpeg_frame_idx
|
||||
self._ffmpeg_frame_idx = idx
|
||||
|
||||
if self._ffmpeg_proc is None or \
|
||||
frame_diff <= 0 or frame_diff >= 100:
|
||||
self._ffmpeg_need_restart = True
|
||||
|
||||
if self._ffmpeg_need_restart:
|
||||
self._ffmpeg_need_restart = False
|
||||
if not self._ffmpeg_restart(idx):
|
||||
return (None, 'ffmpeg error')
|
||||
frame_diff = 1
|
||||
else:
|
||||
frame_diff = max(1, frame_diff)
|
||||
|
||||
#frame_diff += 1
|
||||
image = self._ffmpeg_next_frame(frame_diff)
|
||||
if image is None:
|
||||
return (None, 'Unpredicted end of stream.')
|
||||
|
||||
return (image, f'{self._filepath.name}_{idx:06}')
|
||||
|
||||
|
||||
|
3
xlib/player/__init__.py
Normal file
3
xlib/player/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .FramePlayer import FramePlayer
|
||||
from .ImageSequencePlayer import ImageSequencePlayer
|
||||
from .VideoFilePlayer import VideoFilePlayer
|
Loading…
Add table
Add a link
Reference in a new issue