From 93810c8ba05fb707a771ae6f958bca8734138254 Mon Sep 17 00:00:00 2001 From: Marek Ruszczyk Date: Thu, 19 Jan 2023 20:07:24 +0100 Subject: [PATCH] SRT Data, Hide sensitive OSD info, small fixes (#11) * SRT file support (experimental) * Fix progresss bar * Implement hardware encoding for ffmpeg (experimental) * Optionally hide sensitive OSD elements * Improved performance for PNG generation (up to 10times faster) --- .vscode/settings.json | 4 + README.md | 55 +++++-- font.ttf | Bin 0 -> 63592 bytes makefile | 3 +- osd_gui.py | 153 ++++++++++++------- processor.py | 343 ++++++++++++++++++++++++++++++++++-------- requirements.txt | Bin 542 -> 566 bytes settings.py | 21 ++- 8 files changed, 440 insertions(+), 139 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 font.ttf diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..33fe63f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.linting.flake8Enabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/README.md b/README.md index 936026e..00ead63 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,45 @@ ![image](https://user-images.githubusercontent.com/1878027/210377476-0ca2a14e-71d7-40d8-add5-3d5d6f00a006.png) +# Tool for generating OSD for Walksnail DVR -# Ack -Software is provided as is and it is open sourced so contributions are welcome! +That's easy, drag and drop files into UI, click Generate. +Then import generated sequence into and video editing software and adjust framerate to 60fps if needed. -Feel free to create a ticket in case something is not working. +# How to run -# Release +### Windows +Nothing to do, Go to [Release page](https://github.com/kirek007/ws-osd-py/releases) -Go to [Release page](https://github.com/kirek007/ws-osd-py/releases) +### Linux / MacOS -# Requirements +Disclamer: I've tried my best to make it work on all systems, but it's only tested on Windows. So it might not work as expected +on other systems. -If you want to render osd into file directly in app you need ffmpeg to be installed in you system. +**Only works with Python 3.10 due to issues with wxPython and python-opencv libs** -For Windows: Nothing to do, incuded in release. -For Linux: `sudo apt install ffmpeg ` -For MacOs `brew install ffmpeg` +Install ffmpeg: -# Tool for generating OSD for Walksnail DVR +For Linux: +```bash +sudo apt install ffmpeg +``` -That's easy, drag and drop files into UI, click Generate. -Then import generated sequence into and video editing software and adjust framerate to 60fps if needed. +For MacOs: +```bash +brew install ffmpeg +``` + +Then clone respotiory and run app + +```bash +git clone https://github.com/kirek007/ws-osd-py.git +cd ws-osd-py +make run +``` + +# Usage tutorial + +https://www.youtube.com/watch?v=we3F4rIXTqU # Fonts You can get fancy fonts from [Sneaky_FPV](https://sites.google.com/view/sneaky-fpv/home?pli=1), or get [default walksnails fonts](https://drive.google.com/file/d/1c3CRgXYQaM3Tt4ukLSIvoogScQZs9w49/view) @@ -32,3 +50,14 @@ Here are some results: https://www.youtube.com/watch?v=fHHXh9k-SGg https://www.youtube.com/watch?v=2u7wiJBIdCg + +# Ack +Software is provided as is and it is open sourced so contributions are welcome! + +Feel free to create a ticket in case something is not working. + + +## Coffee needed +If you like tool, you can buy me a coffee so keep working more overnights :) + +Buy Me A Coffee \ No newline at end of file diff --git a/font.ttf b/font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d345388bd60810393dd4b8a653a9a22294e91447 GIT binary patch literal 63592 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 (3,9) and sys.version_info < (3,11)); sys.exit(0) if valid else sys.exit(1)' || (echo "Python 3.10 is required"; exit 1) $(VENV)/python osd_gui.py diff --git a/osd_gui.py b/osd_gui.py index ebc498f..b42dd1a 100644 --- a/osd_gui.py +++ b/osd_gui.py @@ -7,33 +7,33 @@ from settings import appState from pubsub import pub + class PubSubEvents(str, Enum): FileSelected = "FileDrop" ConfigUpdate = "ConfigUpdate" ApplicationConfigured = "ApplicationConfigured" PreviewUpdate = "PreviewUpdate" - class FilesDropTarget(wx.FileDropTarget): """""" - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- def __init__(self, window): """Constructor""" wx.FileDropTarget.__init__(self) self.window = window - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- def OnDropFiles(self, x, y, filenames): filename = filenames[0] logging.debug("File drop: %s", filename) appState.getOptionsByPath(filename) pub.sendMessage(PubSubEvents.ConfigUpdate) - return True + class FileInputPanel(wx.Panel): def __init__(self, parent): @@ -60,17 +60,17 @@ def __init__(self, parent): self.font_default = wx.Font(18, wx.DEFAULT, wx.NORMAL, wx.BOLD) self.font_bold = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.BOLD) self.font_warning = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.BOLD) - self.lbl_output_info.SetForegroundColour((255,0,0)) + self.lbl_output_info.SetForegroundColour((255, 0, 0)) lbl_info.SetFont(self.font_default) - self.lbl_osd_sel.SetFont(self.font_bold) - self.lbl_osd_info.SetFont(self.font_bold) - self.lbl_video_sel.SetFont(self.font_bold) - self.lbl_video_info.SetFont(self.font_bold) - self.lbl_font_sel.SetFont(self.font_bold) - self.lbl_font_info.SetFont(self.font_bold) - self.lbl_output_sel.SetFont(self.font_bold) - self.lbl_output_info.SetFont(self.font_bold) + # self.lbl_osd_sel.SetFont(self.font_bold) + # self.lbl_osd_info.SetFont(self.font_bold) + # self.lbl_video_sel.SetFont(self.font_bold) + # self.lbl_video_info.SetFont(self.font_bold) + # self.lbl_font_sel.SetFont(self.font_bold) + # self.lbl_font_info.SetFont(self.font_bold) + # self.lbl_output_sel.SetFont(self.font_bold) + # self.lbl_output_info.SetFont(self.font_bold) box = wx.StaticBox(self, -1, "Import files") bsizer = wx.StaticBoxSizer(box, wx.VERTICAL) @@ -90,7 +90,6 @@ def __init__(self, parent): bsizer.Add(self.lbl_output_sel, 0, wx.ALL, 5) bsizer.Add(self.lbl_output_info, 0, wx.ALL, 5) - bsizer.Add(hsizer, 0, wx.LEFT) main_sizer = wx.BoxSizer() main_sizer.Add(bsizer, 1, wx.EXPAND | wx.ALL, 10) @@ -99,6 +98,7 @@ def __init__(self, parent): pub.subscribe(self.eventConfigUpdate, PubSubEvents.ConfigUpdate) pass + def eventConfigUpdate(self): self.updateSettings() @@ -106,39 +106,43 @@ def updateSettings(self): """ Write text to the text control """ - self.lbl_video_sel.SetLabel(appState._video_path) - self.lbl_osd_sel.SetLabel(appState._osd_path) - self.lbl_font_sel.SetLabel(appState._font_path) - self.lbl_output_sel.SetLabel(appState._output_path) + self.lbl_video_sel.SetLabel(appState._video_path) + self.lbl_osd_sel.SetLabel(appState._osd_path) + self.lbl_font_sel.SetLabel(appState._font_path) + self.lbl_output_sel.SetLabel(appState._output_path) self.updateInfo() def updateInfo(self): if appState._osd_path: soft_name = OSDFile(appState._osd_path, None).get_software_name() - self.lbl_osd_info.SetLabel("Recognized '%s' software." % soft_name) + self.lbl_osd_info.SetLabel("Recognized '%s' software." % soft_name) if appState._font_path: font = OsdFont(appState._font_path) font_size_text = ("HD" if font.is_hd() else "SD") - self.lbl_font_info.SetLabel("Recognized '%s' font." % font_size_text) + self.lbl_font_info.SetLabel( + "Recognized '%s' font." % font_size_text) if appState._video_path: video = VideoFile(appState._video_path) video_size_text = ("HD" if video.is_hd() else "SD") - self.lbl_video_info.SetLabel("Recognized '%s' video." % video_size_text) + self.lbl_video_info.SetLabel( + "Recognized '%s' video." % video_size_text) if appState.is_output_exists(): - self.lbl_output_info.SetLabel("Output directory already exists, remove it to regenerate PNG files") + self.lbl_output_info.SetLabel( + "Output directory already exists, remove it") else: - self.lbl_output_info.SetLabel("") + self.lbl_output_info.SetLabel("") if appState._font_path and appState._video_path: font = OsdFont(appState._font_path) video = VideoFile(appState._video_path) if video.is_hd() != font.is_hd(): - self.lbl_font_info.SetLabel("Font doesn't match video resolution, please select '%s' font " % video_size_text ) - + self.lbl_font_info.SetLabel( + "Font doesn't match video resolution, please select '%s' font " % video_size_text) + class ButtonsPanel(wx.Panel): @@ -188,11 +192,23 @@ def chekboxClick(self, event): def eventConfigUpdate(self): configured = appState.is_configured() self.btnStartPng.Enable(configured) - self.btnStartVideo.Enable(appState.is_output_exists()) - + self.btnStartVideo.Enable(configured) + def btnStartVideoClick(self, event): + done = self._render_png() + if done: + self._render_video() + mes = wx.MessageBox("Render done.", "OK") + + def btnStartPngClick(self, event): + if self._render_png(): + mes = wx.MessageBox( + "OSD overlay files are in '%s' directory" % appState._output_path, "OK") + + def _render_video(self): status = appState.osd_init() - pd = wx.ProgressDialog("Rendering video", "Check console log for status", 1, self, style=wx.PD_APP_MODAL) + pd = wx.ProgressDialog( + "Rendering video", "Check console log for status", 1, self, style=wx.PD_APP_MODAL) pd.Show() appState.osd_render_video() _osd_gen = appState._osd_gen @@ -200,14 +216,14 @@ def btnStartVideoClick(self, event): wx.MilliSleep(200) pd.Update(0) pd.Update(1) - mes = wx.MessageBox("Render done.", "OK") pd.Destroy() pub.sendMessage(PubSubEvents.ConfigUpdate) - - def btnStartPngClick(self, event): + def _render_png(self): + canceled = False status = appState.osd_init() - pd = wx.ProgressDialog("Generating OSD", "Processing frames...", status.total_frames + 1, self, style=wx.PD_CAN_ABORT | wx.PD_APP_MODAL | wx.PD_REMAINING_TIME | wx.PD_ELAPSED_TIME | wx.PD_SMOOTH) + pd = wx.ProgressDialog("Generating OSD", "Processing frames...", status.total_frames + 1, self, + style=wx.PD_CAN_ABORT | wx.PD_APP_MODAL | wx.PD_REMAINING_TIME | wx.PD_ELAPSED_TIME | wx.PD_SMOOTH) pd.Show() appState.osd_start_process() keepGoing = True @@ -215,16 +231,15 @@ def btnStartPngClick(self, event): wx.MilliSleep(200) keepGoing, skip = pd.Update(status.current_frame) if not keepGoing: + canceled = True appState.osd_cancel_process() - - if status.is_complete(): - mes = wx.MessageBox("OSD overlay files are in '%s' directory" % appState._output_path, "OK") - else: - mes = wx.MessageBox("Process canceled.", "CANCELED") - + + pd.Update(status.total_frames) pd.Destroy() pub.sendMessage(PubSubEvents.ConfigUpdate) + return not canceled + class OsdSettingsPanel(wx.Panel): @@ -239,7 +254,8 @@ def __init__(self, parent): vsizer = wx.BoxSizer(wx.VERTICAL) lbl = wx.StaticText(self, label='Offset left') vsizer.Add(lbl) - self.osdOffsetLeft = wx.Slider(self,name="OSD offset X", minValue=-200, maxValue=600, value=0, style=wx.SL_HORIZONTAL|wx.SL_VALUE_LABEL, size=wx.Size(150, -1)) + self.osdOffsetLeft = wx.Slider(self, name="OSD offset X", minValue=-200, maxValue=600, + value=0, style=wx.SL_HORIZONTAL | wx.SL_VALUE_LABEL, size=wx.Size(150, -1)) self.osdOffsetLeft.Bind(wx.EVT_SCROLL, self.eventSliderUpdated) vsizer.Add(self.osdOffsetLeft) hsizer.Add(vsizer) @@ -247,7 +263,8 @@ def __init__(self, parent): vsizer = wx.BoxSizer(wx.VERTICAL) lbl = wx.StaticText(self, label='Offset top') vsizer.Add(lbl) - self.osdOffsetTop = wx.Slider(self,name="OSD offset Y", minValue=-200, maxValue=600, value=0, style=wx.SL_HORIZONTAL|wx.SL_VALUE_LABEL, size=wx.Size(150, -1)) + self.osdOffsetTop = wx.Slider(self, name="OSD offset Y", minValue=-200, maxValue=600, + value=0, style=wx.SL_HORIZONTAL | wx.SL_VALUE_LABEL, size=wx.Size(150, -1)) self.osdOffsetTop.Bind(wx.EVT_SCROLL, self.eventSliderUpdated) vsizer.Add(self.osdOffsetTop) hsizer.Add(vsizer) @@ -255,36 +272,60 @@ def __init__(self, parent): vsizer = wx.BoxSizer(wx.VERTICAL) lbl = wx.StaticText(self, label='Zoom') vsizer.Add(lbl) - self.osdZoom = wx.Slider(self,name="OSD zoom", minValue=80, maxValue=200, value=100, style=wx.SL_HORIZONTAL|wx.SL_VALUE_LABEL, size=wx.Size(150, -1)) + self.osdZoom = wx.Slider(self, name="OSD zoom", minValue=80, maxValue=200, + value=100, style=wx.SL_HORIZONTAL | wx.SL_VALUE_LABEL, size=wx.Size(150, -1)) self.osdZoom.Bind(wx.EVT_SCROLL, self.eventSliderUpdated) vsizer.Add(self.osdZoom) hsizer.Add(vsizer) vsizer = wx.BoxSizer(wx.VERTICAL) btnReset = wx.Button(self, label='Reset') + self.cbo_srt = wx.CheckBox(self, label="Include SRT data if loaded") + self.cbo_hide_data = wx.CheckBox( + self, label="Hide sensitive OSD values (GPS, Alt, Home dist)") + self.cbo_use_hw = wx.CheckBox( + self, label="Use hardware acceleration for video enconding (experimental)") btnReset.Bind(wx.EVT_BUTTON, self.btnResetClick) - bsizer.Add(hsizer, 0, wx.LEFT) bsizer.Add(btnReset, 0, wx.CENTER) + bsizer.AddSpacer(10) + bsizer.Add(self.cbo_srt, 0, wx.CENTER) + bsizer.AddSpacer(10) + bsizer.Add(self.cbo_hide_data, 0, wx.CENTER) + bsizer.AddSpacer(10) + bsizer.Add(self.cbo_use_hw, 0, wx.CENTER) main_sizer = wx.BoxSizer() main_sizer.Add(bsizer, 1, wx.EXPAND | wx.ALL, 10) bsizer.AddSpacer(10) self.SetSizer(main_sizer) + self.cbo_srt.Bind(wx.EVT_CHECKBOX, self.chekboxClick) + self.cbo_hide_data.Bind(wx.EVT_CHECKBOX, self.chekboxClick) + self.cbo_use_hw.Bind(wx.EVT_CHECKBOX, self.chekboxClick) + + def chekboxClick(self, event): + appState._include_srt = bool(self.cbo_srt.Value) + appState._hide_sensitive_osd = bool(self.cbo_hide_data.Value) + appState._use_hw = bool(self.cbo_use_hw.Value) + pub.sendMessage(PubSubEvents.ConfigUpdate) + def btnResetClick(self, event): self.osdOffsetLeft.SetValue(0) self.osdOffsetTop.SetValue(0) self.osdZoom.SetValue(100) - appState.updateOsdPosition(self.osdOffsetLeft.Value, self.osdOffsetTop.Value, self.osdZoom.Value) + appState.updateOsdPosition( + self.osdOffsetLeft.Value, self.osdOffsetTop.Value, self.osdZoom.Value) pub.sendMessage(PubSubEvents.PreviewUpdate) def eventSliderUpdated(self, event): logging.debug(f"Slider updated.") - appState.updateOsdPosition(self.osdOffsetLeft.Value, self.osdOffsetTop.Value, self.osdZoom.Value) + appState.updateOsdPosition( + self.osdOffsetLeft.Value, self.osdOffsetTop.Value, self.osdZoom.Value) pub.sendMessage(PubSubEvents.PreviewUpdate) + class BottomPanel(wx.Panel): def __init__(self, parent): @@ -298,13 +339,13 @@ def __init__(self, parent): vsizer = wx.BoxSizer(wx.VERTICAL) hyper2 = hl.HyperLinkCtrl(self, -1, "Latest version always here!", - URL="https://github.com/kirek007/ws-osd-pyk") + URL="https://github.com/kirek007/ws-osd-py") vsizer.Add(hyper2) hsizer.Add(vsizer) hsizer.AddSpacer(20) vsizer = wx.BoxSizer(wx.VERTICAL) hyper2 = hl.HyperLinkCtrl(self, -1, "Psst, this is coffee driven application ;)", - URL="https://www.buymeacoffee.com/kirek") + URL="https://www.buymeacoffee.com/kirek") vsizer.Add(hyper2) hsizer.Add(vsizer) bsizer.Add(hsizer, 0, wx.LEFT) @@ -313,37 +354,39 @@ def __init__(self, parent): bsizer.AddSpacer(20) self.SetSizer(main_sizer) + class PrewievPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent=parent) - + box = wx.StaticBox(self, -1, "OSD Preview") bsizer = wx.StaticBoxSizer(box, wx.VERTICAL) - self.imageCtrl = wx.StaticBitmap(self, wx.ID_ANY, + self.imageCtrl = wx.StaticBitmap(self, wx.ID_ANY, wx.Bitmap(wx.Image(640, 360, True))) - bsizer.Add(self.imageCtrl, 20, wx.EXPAND | wx.ALL, 20) + bsizer.Add(self.imageCtrl, 20, wx.EXPAND | wx.ALL, 20) main_sizer = wx.BoxSizer() main_sizer.Add(bsizer, 0, wx.EXPAND | wx.ALL, 20) self.SetSizer(main_sizer) pub.subscribe(self.eventConfigUpdate, PubSubEvents.ConfigUpdate) pub.subscribe(self.eventConfigUpdate, PubSubEvents.PreviewUpdate) - + def eventConfigUpdate(self): if not appState.is_configured(): return - logging.debug(f"Preview update requested.") + logging.debug("Preview update requested.") self.onView() - def onView(self): prev = OsdPreview(appState.get_osd_config()) - image = prev.generate_preview((appState.offsetLeft, appState.offsetTop ),appState.osdZoom) + image = prev.generate_preview( + (appState.offsetLeft, appState.offsetTop), appState.osdZoom) self.imageCtrl.SetBitmap(wx.Bitmap.FromBuffer(640, 360, image)) self.imageCtrl.Refresh() self.Refresh() + class MainWindow(wx.Frame): def __init__(self): @@ -374,6 +417,6 @@ def __init__(self): app = wx.App(False) frame = MainWindow() - frame.Size = (1300, 785) - frame.MinSize = wx.Size(1300, 785) + frame.Size = (1300, 790) + frame.MinSize = wx.Size(1300, 790) app.MainLoop() diff --git a/processor.py b/processor.py index f18aa23..0aaf0aa 100644 --- a/processor.py +++ b/processor.py @@ -1,20 +1,24 @@ -import argparse import cProfile +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field import io import logging +import multiprocessing import os from datetime import datetime -from pathlib import Path +import platform from pstats import SortKey import pstats +import queue from struct import unpack -from subprocess import TimeoutExpired +import subprocess from threading import Thread -import time - import cv2 import numpy as np import ffmpeg +import srt +from PIL import ImageFont, ImageDraw, Image + class CountsPerSec: """ @@ -38,6 +42,7 @@ def countsPerSec(self): elapsed_time = (datetime.now() - self._start_time).total_seconds() return self._num_occurrences / elapsed_time + class OsdFont: GLYPH_HD_H = 18 * 3 @@ -62,6 +67,11 @@ def is_hd(self): font_w = self.font.shape[1] return font_w == self.GLYPH_HD_W + def get_srt_font_size(self): + if self.is_hd(): + return 32 + else: + return 24 class OSDFile: @@ -84,13 +94,13 @@ def peek_frame(self, frame_no): return frame def read_frame(self): - + rawData = self.osdFile.read(self.READ_SIZE) if len(rawData) < self.READ_SIZE: return False return Frame(rawData, self.font) - + def get_software_name(self): match self.fcType: case "BTFL": @@ -99,11 +109,10 @@ def get_software_name(self): return "Ardupilot" case "INAV": return "INav" - case _ : + case _: return "Unknown" - class Frame: frame_w = 53 frame_h = 20 @@ -114,17 +123,50 @@ def __init__(self, data, font: OsdFont): self.rawData = data[4:] self.font = font - def __convert_to_glyphs(self): + self.glyph_hide_start = [ + 3, # gps + 4, # gps + 16, # home distance + 345, # alt symb + + ] + self.glyph_hide_len = [ + 6, + 6, + 3, + 3, + ] + self.mask_glyph_no = ord("*") + self.curent_mask_index = -1 + self.curent_mask_counter = 0 + + def __convert_to_glyphs(self, hide): glyphs_arr = [] for x in range(0, len(self.rawData), 2): index, page = unpack(" -1: + if self.curent_mask_counter < self.glyph_hide_len[self.curent_mask_index]: + + if self.curent_mask_counter > 0: + glyph_index = self.mask_glyph_no + self.curent_mask_counter += 1 + else: + self.curent_mask_counter = 0 + self.curent_mask_index = -1 + + glyph = self.font.get_glyph(glyph_index) glyphs_arr.append(glyph) return glyphs_arr - def get_osd_frame_glyphs(self): - glyphs = self.__convert_to_glyphs() + def get_osd_frame_glyphs(self, hide): + glyphs = self.__convert_to_glyphs(hide) osd_frame = [] gi = 0 @@ -157,7 +199,8 @@ def is_hd(self): def get_size(self): width = self.videoFile.get(cv2.CAP_PROP_FRAME_WIDTH) # float `width` - height = self.videoFile.get(cv2.CAP_PROP_FRAME_HEIGHT) # float `height` + height = self.videoFile.get( + cv2.CAP_PROP_FRAME_HEIGHT) # float `height` return int(height), int(width) def get_total_frames(self): @@ -171,7 +214,7 @@ def read_frame(self): ret, frame = self.videoFile.read() if not ret: return None - + if len(frame) == 0: return None @@ -179,15 +222,20 @@ def read_frame(self): class OsdGenConfig: - def __init__(self, video_path, osd_path, font_path, output_path, offset_left, offset_top, osd_zoom, render_upscale) -> None: + def __init__(self, video_path, osd_path, font_path, srt_path, output_path, offset_left, offset_top, osd_zoom, render_upscale, include_srt, hide_sensitive_osd, use_hw) -> None: self.video_path = video_path self.osd_path = osd_path self.font_path = font_path + self.srt_path = srt_path self.output_path = output_path self.offset_left = offset_left self.offset_top = offset_top self.osd_zoom = osd_zoom self.render_upscale = render_upscale + self.include_srt = include_srt + self.hide_sensitive_osd = hide_sensitive_osd + self.use_hw = use_hw + class OsdGenStatus: def __init__(self) -> None: @@ -203,15 +251,17 @@ def update(self, current, total, fps) -> None: def is_complete(self) -> bool: return self.current_frame >= self.total_frames + class Utils: - + @staticmethod def merge_images(img, overlay, x, y, zoom): - scale_percent = zoom # percent of original size + scale_percent = zoom # percent of original size width = int(overlay.shape[1] * scale_percent / 100) height = int(overlay.shape[0] * scale_percent / 100) dim = (width, height) - img_overlay_res = cv2.resize(overlay, dim, interpolation = cv2.INTER_CUBIC) + img_overlay_res = cv2.resize( + overlay, dim, interpolation=cv2.INTER_CUBIC) # img_crop = img_overlay_res[y:img.shape[0],x:img.shape[1]] # Image ranges @@ -228,15 +278,15 @@ def merge_images(img, overlay, x, y, zoom): img_crop[:] = img_overlay_crop + img_crop img = img_crop # img[y:y+img_crop.shape[0], x:x+img_crop.shape[1]] = img_crop - @staticmethod def overlay_image_alpha(img, img_overlay, x, y, zoom): - scale_percent = zoom # percent of original size + scale_percent = zoom # percent of original size width = int(img_overlay.shape[1] * scale_percent / 100) height = int(img_overlay.shape[0] * scale_percent / 100) dim = (width, height) - img_overlay_res = cv2.resize(img_overlay, dim, interpolation = cv2.INTER_CUBIC) + img_overlay_res = cv2.resize( + img_overlay, dim, interpolation=cv2.INTER_CUBIC) # Mask alpha_mask = img_overlay_res[:, :, 3] / 255.0 @@ -262,36 +312,141 @@ def overlay_image_alpha(img, img_overlay, x, y, zoom): img_crop[:] = alpha * img_overlay_crop + alpha_inv * img_crop + @staticmethod + def overlay_srt_line(img, line, font_size, left_offset): + pos_calc = (left_offset, img.shape[0] - 15) + pil_im = Image.fromarray(img) + draw = ImageDraw.Draw(pil_im, 'RGBA') + font = ImageFont.truetype("font.ttf", font_size) + + # left, top, right, bottom = draw.textbbox(pos_calc, line, font=font, anchor="lb") + # draw.rectangle((left-5, top-5, right+5, bottom+5), fill=(0, 0, 0, 125)) + draw.text(pos_calc, line, font=font, fill=( + 255, 255, 255, 255), anchor="lb") + return Utils.to_numpy(pil_im) + + # pos_calc = (20, img.shape[0] - 30) + # cv2.putText(img, line, pos_calc, cv2.FONT_ITALIC, 1/10 * font_size, (255, 255, 255, 255), 1) + + # return img + + @staticmethod + def to_numpy(im): + im.load() + # unpack data + e = Image._getencoder(im.mode, 'raw', im.mode) + e.setimage(im.im) + + # NumPy buffer for the result + shape, typestr = Image._conv_type_shape(im) + data = np.empty(shape, dtype=np.dtype(typestr)) + mem = data.data.cast('B', (data.data.nbytes,)) + + bufsize, s, offset = 65536, 0, 0 + while not s: + l, s, d = e.encode(bufsize) + mem[offset:offset + len(d)] = d + offset += len(d) + if s < 0: + raise RuntimeError("encoder error %d in tobytes" % s) + return data + + class OsdPreview: def __init__(self, config: OsdGenConfig): self.stopped = False - - + self.font = OsdFont(config.font_path) self.osd = OSDFile(config.osd_path, self.font) self.video = VideoFile(config.video_path) + if config.srt_path: + self.srt = SrtFile(config.srt_path) + else: + self.srt = None self.output = config.output_path self.config = config + def str_line_to_glyphs(self, line): + filler = self.font.get_glyph(32) + rssi = self.font.get_glyph(1) + glyphs = [filler, rssi] + for char in line: + gi = ord(char) + g = self.font.get_glyph(gi) + glyphs.append(g) + for x in range(len(glyphs), 53): + glyphs.append(filler) + + return glyphs[:53] def generate_preview(self, osd_pos, osd_zomm): video_frame = self.video.read_frame().data - for skipme in range(100): + for skipme in range(20): self.osd.read_frame() + if self.srt: + srt_data = self.srt.next_data() + + osd_frame_glyphs = self.osd.read_frame().get_osd_frame_glyphs( + hide=self.config.hide_sensitive_osd) + + osd_frame = cv2.vconcat([cv2.hconcat(im_list_h) + for im_list_h in osd_frame_glyphs]) + if self.srt and self.config.include_srt: + srt_line = srt_data["line"] + video_frame = Utils.overlay_srt_line( + video_frame, srt_line, self.font.get_srt_font_size(), (150 if self.font.is_hd() else 100)) + Utils.overlay_image_alpha( + video_frame, osd_frame, osd_pos[0], osd_pos[1], osd_zomm) + result = cv2.resize(video_frame, (640, 360), + interpolation=cv2.INTER_AREA) + result = cv2.cvtColor(result, cv2.COLOR_BGR2RGB) - osd_frame_glyphs = self.osd.read_frame().get_osd_frame_glyphs() - osd_frame = cv2.vconcat([cv2.hconcat(im_list_h) for im_list_h in osd_frame_glyphs]) + return result + # return result - Utils.overlay_image_alpha(video_frame, osd_frame, osd_pos[0], osd_pos[1], osd_zomm) - result = cv2.resize(video_frame, (640, 360), interpolation = cv2.INTER_CUBIC) - result = cv2.cvtColor(result, cv2.COLOR_BGR2RGB) +class SrtFile(): + def __init__(self, path): + self.index = 0 + with open(path, "r") as f: + self.subs = list(srt.parse(f, True)) + + def next_data(self) -> dict: + if self.index >= len(self.subs): + self.index = len(self.subs) - 1 + sub = self.subs[self.index] + data = dict(x.split(":") for x in sub.content.split(" ")) + d = dict() + d["startTime"] = sub.start.seconds / 1000 * sub.start.microseconds + d["data"] = data # sub.start.seconds / 1000 * sub.start.microseconds + d["line"] = "Time: %3s Signal:%1s Delay:%5s Bitrate:%7s Disatnce:%5s" % ( + data["FlightTime"], data["Signal"], data["Delay"], data["Bitrate"], data["Distance"]) + self.index += 1 + return d + + +class ThreadPoolExecutorWithQueueSizeLimit(ThreadPoolExecutor): + def __init__(self, maxsize=50, *args, **kwargs): + super(ThreadPoolExecutorWithQueueSizeLimit, + self).__init__(*args, **kwargs) + self._work_queue = queue.Queue(maxsize=maxsize) - return result + +@dataclass() +class CodecItem: + supported_os: list + name: str + +@dataclass +class CodecsList: + codecs: list[CodecItem] = field(default_factory=list) + + def getbyOS(self, os_name: str) -> list[CodecItem]: + return list(filter(lambda codec: os_name in codec.supported_os, self.codecs)) class OsdGenerator: @@ -305,14 +460,53 @@ def __init__(self, config: OsdGenConfig): self.output = config.output_path self.config = config self.osdGenStatus = OsdGenStatus() + self.render_done = False + self.use_hw = config.use_hw + self.codecs = CodecsList(self.load_codecs()) + + if config.srt_path: + self.srt = SrtFile(config.srt_path) + else: + self.srt = None self.osdGenStatus.update(0, self.video.get_total_frames(), 0) try: os.mkdir(self.output) except: pass - - + def load_codecs(self): + + macos = "darwin" + windows = "windows" + linux = "linux" + codecs = [] + if self.use_hw: + codecs.append(CodecItem(name="hevc_videotoolbox", supported_os=[macos])) + codecs.append(CodecItem(name="hevc_nvenc", supported_os=[windows, linux])) + codecs.append(CodecItem(name="hevc_amf", supported_os=[windows])) + codecs.append(CodecItem(name="hevc_vaapi", supported_os=[linux])) + codecs.append(CodecItem(name="hevc_qsv", supported_os=[linux, windows])) + codecs.append(CodecItem(name="hevc_mf", supported_os=[windows])) + codecs.append(CodecItem(name="hevc_v4l2m2m", supported_os=[linux])) + + codecs.append(CodecItem(name="libx265", supported_os=[macos, windows, linux])) + + return codecs + + def get_working_encoder(self): + available_codecs = self.codecs.getbyOS(platform.system().lower()) + run_line = "ffmpeg -y -hwaccel auto -f lavfi -i nullsrc -c:v %s -frames:v 1 -f null -" + for codec in available_codecs: + runme = (run_line % codec.name).split(" ") + ret = subprocess.run(runme, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + if ret.returncode == 0: + logging.info("Found a working codec (%s)" % codec.name) + return codec.name + + raise Exception("There is no valid codedc. It should not happen") + def start_video(self, upscale: bool): Thread(target=self.render, args=()).start() return self @@ -326,7 +520,8 @@ def stop(self): @staticmethod def __render_osd_frame(osd_frame_glyphs): - render = cv2.vconcat([cv2.hconcat(im_list_h) for im_list_h in osd_frame_glyphs]) + render = cv2.vconcat([cv2.hconcat(im_list_h) + for im_list_h in osd_frame_glyphs]) return render def __overlay_osd(self, video_frame, osd_frame): @@ -349,92 +544,114 @@ def render(self): else: ff_size = {"w": video_size[1], "h": video_size[0]} - out_path = os.path.join(self.output, "ws_%09d.png") osd_frame = ( ffmpeg .input(out_path, framerate=60) .filter("scale", **ff_size, force_original_aspect_ratio=0) + ) + input_args = { + "hwaccel": "auto", + } + video = ( ffmpeg - .input(self.config.video_path) - .filter("scale", **ff_size, force_original_aspect_ratio=1) + .input(self.config.video_path, **input_args) + .filter("scale", **ff_size, force_original_aspect_ratio=1, ) ) - + encoder_name = self.get_working_encoder() + output_args = { + "c:v": encoder_name, + "preset": "fast", + "crf": 0, + "b:v": "40M", + "acodec": "copy" + } self.render_done = False process = ( video .filter("pad", **ff_size, x=-1, y=-1, color="black") .overlay(osd_frame, x=0, y=0) - .output("%s_osd.mp4" % (self.output), video_bitrate="40M") - .overwrite_output() + .output("%s_osd.mp4" % (self.output), **output_args) + .overwrite_output() .run() ) - self.render_done = True - def render_example(self): - frame = [] - return frame - - def main(self): + def main(self): cps = CountsPerSec().start() pr = cProfile.Profile() pr.enable() - osd_time = -1 osd_frame = [] current_frame = 1 + srt_time = -1 video_fps = self.video.get_fps() total_frames = self.video.get_total_frames() video_size = self.video.get_size() img_height, img_width = video_size[0], video_size[1] n_channels = 4 - transparent_img = np.zeros((img_height, img_width, n_channels), dtype=np.uint8) + transparent_img = np.zeros( + (img_height, img_width, n_channels), dtype=np.uint8) frame = transparent_img.copy() + executor = ThreadPoolExecutorWithQueueSizeLimit( + max_workers=multiprocessing.cpu_count()-1, maxsize=2000) + while True: if self.stopped: print("Process canceled.") break - - frames_per_ms = 1 / video_fps * 1000 calc_video_time = int((current_frame - 1) * frames_per_ms) if current_frame >= total_frames: break + if self.srt and self.config.include_srt: + if srt_time < calc_video_time: + srt_data = self.srt.next_data() + srt_time = srt_data["startTime"] + if osd_time < calc_video_time: raw_osd_frame = self.osd.read_frame() if not raw_osd_frame: break frame = transparent_img.copy() - osd_frame = self.__render_osd_frame(raw_osd_frame.get_osd_frame_glyphs()) - Utils.merge_images(frame, osd_frame, self.config.offset_left, self.config.offset_top, self.config.osd_zoom) + osd_frame = self.__render_osd_frame( + raw_osd_frame.get_osd_frame_glyphs(hide=self.config.hide_sensitive_osd)) osd_time = raw_osd_frame.startTime - + Utils.merge_images(frame, osd_frame, self.config.offset_left, + self.config.offset_top, self.config.osd_zoom) + + if self.srt and self.config.include_srt: + result = Utils.overlay_srt_line(frame, srt_data["line"], self.font.get_srt_font_size( + ), (150 if self.font.is_hd() else 100)) + else: + result = frame + out_path = os.path.join(self.output, "ws_%09d.png" % (current_frame)) - cv2.imwrite(out_path, frame) - + executor.submit(cv2.imwrite, out_path, result) - current_frame+=1 + current_frame += 1 cps.increment() fps = int(cps.countsPerSec()) - self.osdGenStatus.update(current_frame, total_frames, fps) + self.osdGenStatus.update(current_frame - 1, total_frames, fps) if current_frame % 200 == 0: - - print("Current: %s/%s (fps: %d)" % (current_frame, total_frames, fps)) - + logging.debug("Current: %s/%s (fps: %d)" % + (current_frame, total_frames, fps)) + + logging.info("Waiting for jobs to complete") + executor.shutdown(cancel_futures=False, wait=True) + logging.info("Save complete") + self.osdGenStatus.update(total_frames, total_frames, fps) pr.disable() s = io.StringIO() sortby = SortKey.CUMULATIVE ps = pstats.Stats(pr, stream=s).sort_stats(sortby) ps.print_stats() - print(s.getvalue()) - print("Done.") - + logging.debug(s.getvalue()) diff --git a/requirements.txt b/requirements.txt index 1bc848fb0fe0d7c93529cc9db8026aa0cd27ab78..b726f7aed62953317ec123225a1790d666f34fe6 100644 GIT binary patch delta 32 kcmbQovW;bf9Fqhu0~bRvLlHv>gDnsmGw3mx0(^b delta 7 OcmdnSGLL1091{QvjRGwI diff --git a/settings.py b/settings.py index 1b93639..239ba38 100644 --- a/settings.py +++ b/settings.py @@ -11,7 +11,11 @@ def __init__(self) -> None: self._osd_path = "" self._font_path = "" self._output_path = "" + self._srt_path = "" self._osd_gen = None + self._include_srt = False + self._hide_sensitive_osd = False + self._use_hw = False self.offsetLeft = 0 self.offsetTop = 0 @@ -28,15 +32,14 @@ def getOptionsByPath(self, path: str): file_ext = pathlib.Path(path).suffix match file_ext: - case ".osd": - self._osd_path = path + case ".osd"|".mp4"|".srt": video = os.fspath(pathlib.Path(path).with_suffix('.mp4')) + srt = os.fspath(pathlib.Path(path).with_suffix('.srt')) + osd = os.fspath(pathlib.Path(path).with_suffix('.osd')) if os.path.exists(video): self._video_path = video - self.update_output_path(path) - case ".mp4": - self._video_path = path - osd = os.fspath(pathlib.Path(path).with_suffix('.osd')) + if os.path.exists(srt): + self._srt_path = srt if os.path.exists(osd): self._osd_path = osd self.update_output_path(path) @@ -70,11 +73,15 @@ def get_osd_config(self) -> OsdGenConfig: self._video_path, self._osd_path, self._font_path, + self._srt_path, self._output_path, self.offsetLeft, self.offsetTop, self.osdZoom, - self.render_upscale + self.render_upscale, + self._include_srt, + self._hide_sensitive_osd, + self._use_hw ) def osd_init(self) -> OsdGenStatus: