code release

This commit is contained in:
iperov 2021-07-23 17:34:49 +04:00
commit a902f11f74
354 changed files with 826570 additions and 1 deletions

275
xlib/player/FramePlayer.py Normal file
View 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

View 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)

View 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
View file

@ -0,0 +1,3 @@
from .FramePlayer import FramePlayer
from .ImageSequencePlayer import ImageSequencePlayer
from .VideoFilePlayer import VideoFilePlayer