From d4e88ec3cbbdd0330ec8b940e28358393a877eba Mon Sep 17 00:00:00 2001 From: Brenno Date: Tue, 14 Jul 2020 11:52:22 -0300 Subject: [PATCH 01/18] added preprocessing dialog window --- src/windows/process_effect.py | 115 ++++++++++++++++++++++++++ src/windows/views/timeline_webview.py | 24 ++++-- 2 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 src/windows/process_effect.py diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py new file mode 100644 index 0000000000..502a7a605b --- /dev/null +++ b/src/windows/process_effect.py @@ -0,0 +1,115 @@ +""" + @file + @brief This file loads the Initialize Effects / Pre-process effects dialog + @author Jonathan Thomas + + @section LICENSE + + Copyright (c) 2008-2018 OpenShot Studios, LLC + (http://www.openshotstudios.com). This file is part of + OpenShot Video Editor (http://www.openshot.org), an open-source project + dedicated to delivering high quality video editing and animation solutions + to the world. + + OpenShot Video Editor is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenShot Video Editor is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenShot Library. If not, see . + """ + +import os +import sys +import time + +from PyQt5.QtCore import * +from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem +from PyQt5.QtWidgets import * +from PyQt5 import uic +import openshot # Python module for libopenshot (required video editing module installed separately) + +from classes import info, ui_util, settings, qt_types, updates +from classes.app import get_app +from classes.logger import log +from classes.metrics import * + + +class ProcessEffect(QDialog): + """ Choose Profile Dialog """ + progress = pyqtSignal(int) + + # Path to ui file + ui_path = os.path.join(info.PATH, 'windows', 'ui', 'process-effect.ui') + + def __init__(self, clip_id, effect_name): + + # Create dialog class + QDialog.__init__(self) + + # Load UI from designer & init + ui_util.load_ui(self, self.ui_path) + ui_util.init_ui(self) + + # get translations + _ = get_app()._tr + + # Pause playback (to prevent crash since we are fixing to change the timeline's max size) + get_app().window.actionPlay_trigger(None, force="pause") + + # Track metrics + track_metric_screen("process-effect-screen") + + # Init form + self.progressBar.setValue(0) + self.txtAdvanced.setText("{}") + self.setWindowTitle(_("%s: Initialize Effect") % effect_name) + self.clip_id = clip_id + self.effect_name = effect_name + + # Add combo entries + self.cboOptions.addItem("Option 1", 1) + self.cboOptions.addItem("Option 2", 2) + self.cboOptions.addItem("Option 3", 3) + + # Add buttons + self.cancel_button = QPushButton(_('Cancel')) + self.process_button = QPushButton(_('Process Effect')) + self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) + self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) + + def accept(self): + """ Start processing effect """ + # Disable UI + self.cboOptions.setEnabled(False) + self.txtAdvanced.setEnabled(False) + self.process_button.setEnabled(False) + + # DO WORK HERE, and periodically set progressBar value + # Access C++ timeline and find the Clip instance which this effect should be applied to + timeline_instance = get_app().window.timeline_sync.timeline + for clip_instance in timeline_instance.Clips(): + if clip_instance.Id() == self.clip_id: + print("Apply effect: %s to clip: %s" % (self.effect_name, clip_instance.Id())) + + # EXAMPLE progress updates + for value in range(1, 100, 4): + self.progressBar.setValue(value) + time.sleep(0.25) + + # Process any queued events + QCoreApplication.processEvents() + + # Accept dialog + super(ProcessEffect, self).accept() + + def reject(self): + # Cancel dialog + self.exporting = False + super(ProcessEffect, self).reject() diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index 782a604ea7..627dc12c81 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -38,7 +38,7 @@ from PyQt5.QtCore import QFileInfo, pyqtSlot, QUrl, Qt, QCoreApplication, QTimer from PyQt5.QtGui import QCursor, QKeySequence from PyQt5.QtWebKitWidgets import QWebView -from PyQt5.QtWidgets import QMenu +from PyQt5.QtWidgets import QMenu, QDialog from classes import info, updates from classes import settings @@ -2939,6 +2939,9 @@ def addTransition(self, file_ids, position): # Add Effect def addEffect(self, effect_names, position): log.info("addEffect: %s at %s" % (effect_names, position)) + # Translation object + _ = get_app()._tr + # Get name of effect name = effect_names[0] @@ -2951,13 +2954,23 @@ def addEffect(self, effect_names, position): # Loop through clips on the closest layer possible_clips = Clip.filter(layer=closest_layer) for clip in possible_clips: - if js_position == 0 or ( - clip.data["position"] <= js_position <= clip.data["position"] - + (clip.data["end"] - clip.data["start"]) - ): + if js_position == 0 or (clip.data["position"] <= js_position <= clip.data["position"] + (clip.data["end"] - clip.data["start"])): log.info("Applying effect to clip") log.info(clip) + # Handle custom effect dialogs + if name in ["Bars", "Stabilize", "Tracker"]: + + from windows.process_effect import ProcessEffect + win = ProcessEffect(clip.id, name) + # Run the dialog event loop - blocking interaction on this window during this time + result = win.exec_() + if result == QDialog.Accepted: + log.info('Start processing') + else: + log.info('Cancel processing') + return + # Create Effect effect = openshot.EffectInfo().CreateEffect(name) @@ -2970,6 +2983,7 @@ def addEffect(self, effect_names, position): # Update clip data for project self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True) + break # Without defining this method, the 'copy' action doesn't show with cursor def dragMoveEvent(self, event): From 83f6e4116ce9c07802830efd582ac51cfff0ec83 Mon Sep 17 00:00:00 2001 From: Brenno Date: Wed, 15 Jul 2020 10:42:30 -0300 Subject: [PATCH 02/18] Added better integration with stabilizer effect --- src/effects/icons/stabilizer.png | Bin 0 -> 3924 bytes src/effects/icons/tracker.png | Bin 0 -> 14430 bytes src/example-effect-init.patch | 298 ++++++++++++++++++++++++++ src/windows/process_effect.py | 16 +- src/windows/views/timeline_webview.py | 12 +- 5 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 src/effects/icons/stabilizer.png create mode 100644 src/effects/icons/tracker.png create mode 100644 src/example-effect-init.patch diff --git a/src/effects/icons/stabilizer.png b/src/effects/icons/stabilizer.png new file mode 100644 index 0000000000000000000000000000000000000000..c70d20cd64cb4ceb11cda8991dc812bef037c25f GIT binary patch literal 3924 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 SO8iUknx>kbYUShSA^!*P%rXN2 literal 0 HcmV?d00001 diff --git a/src/example-effect-init.patch b/src/example-effect-init.patch new file mode 100644 index 0000000000..4926ad7255 --- /dev/null +++ b/src/example-effect-init.patch @@ -0,0 +1,298 @@ +Index: src/windows/process_effect.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- src/windows/process_effect.py (date 1594417794000) ++++ src/windows/process_effect.py (date 1594417794000) +@@ -0,0 +1,115 @@ ++""" ++ @file ++ @brief This file loads the Initialize Effects / Pre-process effects dialog ++ @author Jonathan Thomas ++ ++ @section LICENSE ++ ++ Copyright (c) 2008-2018 OpenShot Studios, LLC ++ (http://www.openshotstudios.com). This file is part of ++ OpenShot Video Editor (http://www.openshot.org), an open-source project ++ dedicated to delivering high quality video editing and animation solutions ++ to the world. ++ ++ OpenShot Video Editor is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ OpenShot Video Editor is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with OpenShot Library. If not, see . ++ """ ++ ++import os ++import sys ++import time ++ ++from PyQt5.QtCore import * ++from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem ++from PyQt5.QtWidgets import * ++from PyQt5 import uic ++import openshot # Python module for libopenshot (required video editing module installed separately) ++ ++from classes import info, ui_util, settings, qt_types, updates ++from classes.app import get_app ++from classes.logger import log ++from classes.metrics import * ++ ++ ++class ProcessEffect(QDialog): ++ """ Choose Profile Dialog """ ++ progress = pyqtSignal(int) ++ ++ # Path to ui file ++ ui_path = os.path.join(info.PATH, 'windows', 'ui', 'process-effect.ui') ++ ++ def __init__(self, clip_id, effect_name): ++ ++ # Create dialog class ++ QDialog.__init__(self) ++ ++ # Load UI from designer & init ++ ui_util.load_ui(self, self.ui_path) ++ ui_util.init_ui(self) ++ ++ # get translations ++ _ = get_app()._tr ++ ++ # Pause playback (to prevent crash since we are fixing to change the timeline's max size) ++ get_app().window.actionPlay_trigger(None, force="pause") ++ ++ # Track metrics ++ track_metric_screen("process-effect-screen") ++ ++ # Init form ++ self.progressBar.setValue(0) ++ self.txtAdvanced.setText("{}") ++ self.setWindowTitle(_("%s: Initialize Effect") % effect_name) ++ self.clip_id = clip_id ++ self.effect_name = effect_name ++ ++ # Add combo entries ++ self.cboOptions.addItem("Option 1", 1) ++ self.cboOptions.addItem("Option 2", 2) ++ self.cboOptions.addItem("Option 3", 3) ++ ++ # Add buttons ++ self.cancel_button = QPushButton(_('Cancel')) ++ self.process_button = QPushButton(_('Process Effect')) ++ self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) ++ self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) ++ ++ def accept(self): ++ """ Start processing effect """ ++ # Disable UI ++ self.cboOptions.setEnabled(False) ++ self.txtAdvanced.setEnabled(False) ++ self.process_button.setEnabled(False) ++ ++ # DO WORK HERE, and periodically set progressBar value ++ # Access C++ timeline and find the Clip instance which this effect should be applied to ++ timeline_instance = get_app().window.timeline_sync.timeline ++ for clip_instance in timeline_instance.Clips(): ++ if clip_instance.Id() == self.clip_id: ++ print("Apply effect: %s to clip: %s" % (self.effect_name, clip_instance.Id())) ++ ++ # EXAMPLE progress updates ++ for value in range(1, 100, 4): ++ self.progressBar.setValue(value) ++ time.sleep(0.25) ++ ++ # Process any queued events ++ QCoreApplication.processEvents() ++ ++ # Accept dialog ++ super(ProcessEffect, self).accept() ++ ++ def reject(self): ++ # Cancel dialog ++ self.exporting = False ++ super(ProcessEffect, self).reject() +Index: src/windows/ui/process-effect.ui +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- src/windows/ui/process-effect.ui (date 1594417794000) ++++ src/windows/ui/process-effect.ui (date 1594417794000) +@@ -0,0 +1,105 @@ ++ ++ ++ Dialog ++ ++ ++ ++ 0 ++ 0 ++ 410 ++ 193 ++ ++ ++ ++ %s: Initialize Effect ++ ++ ++ ++ ++ ++ Progress: ++ ++ ++ ++ ++ ++ ++ 24 ++ ++ ++ ++ ++ ++ ++ Option: ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Advanced: ++ ++ ++ ++ ++ ++ ++ ++ 0 ++ 75 ++ ++ ++ ++ ++ ++ ++ ++ Qt::Horizontal ++ ++ ++ QDialogButtonBox::NoButton ++ ++ ++ ++ ++ ++ ++ ++ ++ buttonBox ++ accepted() ++ Dialog ++ accept() ++ ++ ++ 248 ++ 254 ++ ++ ++ 157 ++ 274 ++ ++ ++ ++ ++ buttonBox ++ rejected() ++ Dialog ++ reject() ++ ++ ++ 316 ++ 260 ++ ++ ++ 286 ++ 274 ++ ++ ++ ++ ++ +Index: src/windows/views/timeline_webview.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- src/windows/views/timeline_webview.py (date 1592265000000) ++++ src/windows/views/timeline_webview.py (date 1594417794000) +@@ -38,7 +38,7 @@ + from PyQt5.QtCore import QFileInfo, pyqtSlot, QUrl, Qt, QCoreApplication, QTimer + from PyQt5.QtGui import QCursor, QKeySequence + from PyQt5.QtWebKitWidgets import QWebView +-from PyQt5.QtWidgets import QMenu ++from PyQt5.QtWidgets import QMenu, QDialog + + from classes import info, updates + from classes import settings +@@ -2939,6 +2939,9 @@ + # Add Effect + def addEffect(self, effect_names, position): + log.info("addEffect: %s at %s" % (effect_names, position)) ++ # Translation object ++ _ = get_app()._tr ++ + # Get name of effect + name = effect_names[0] + +@@ -2951,13 +2954,23 @@ + # Loop through clips on the closest layer + possible_clips = Clip.filter(layer=closest_layer) + for clip in possible_clips: +- if js_position == 0 or ( +- clip.data["position"] <= js_position <= clip.data["position"] +- + (clip.data["end"] - clip.data["start"]) +- ): ++ if js_position == 0 or (clip.data["position"] <= js_position <= clip.data["position"] + (clip.data["end"] - clip.data["start"])): + log.info("Applying effect to clip") + log.info(clip) + ++ # Handle custom effect dialogs ++ if name in ["Bars", "Stabilize", "Tracker"]: ++ ++ from windows.process_effect import ProcessEffect ++ win = ProcessEffect(clip.id, name) ++ # Run the dialog event loop - blocking interaction on this window during this time ++ result = win.exec_() ++ if result == QDialog.Accepted: ++ log.info('Start processing') ++ else: ++ log.info('Cancel processing') ++ return ++ + # Create Effect + effect = openshot.EffectInfo().CreateEffect(name) + +@@ -2970,6 +2983,7 @@ + + # Update clip data for project + self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True) ++ break + + # Without defining this method, the 'copy' action doesn't show with cursor + def dragMoveEvent(self, event): diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index 502a7a605b..e1cfc885d8 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -96,15 +96,21 @@ def accept(self): timeline_instance = get_app().window.timeline_sync.timeline for clip_instance in timeline_instance.Clips(): if clip_instance.Id() == self.clip_id: + self.protobufPath = openshot.ClipProcessingJobs(self.effect_name, clip_instance).stabilizeVideo(clip_instance) + self.effect = openshot.EffectInfo().CreateEffect(self.effect_name, "/media/brenno/Data/projects/openshot/stabilization.data") + # self.effect.SetJson('{"Stabilizer":{"protobuf_data_path": "/home/gustavostahl/LabVisao/VideoEditor/openshot-qt/stabilization.data"}}') + # clip_instance.AddEffect(self.effect) + # return self.effect print("Apply effect: %s to clip: %s" % (self.effect_name, clip_instance.Id())) + # EXAMPLE progress updates - for value in range(1, 100, 4): - self.progressBar.setValue(value) - time.sleep(0.25) + # for value in range(1, 100, 4): + # self.progressBar.setValue(value) + # time.sleep(0.25) - # Process any queued events - QCoreApplication.processEvents() + # # Process any queued events + # QCoreApplication.processEvents() # Accept dialog super(ProcessEffect, self).accept() diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index 627dc12c81..bfc2403b60 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -2959,20 +2959,25 @@ def addEffect(self, effect_names, position): log.info(clip) # Handle custom effect dialogs - if name in ["Bars", "Stabilize", "Tracker"]: + if name in ["Bars", "Stabilizer", "Tracker"]: from windows.process_effect import ProcessEffect - win = ProcessEffect(clip.id, name) + win = ProcessEffect(clip.id, "Stabilizer") # Run the dialog event loop - blocking interaction on this window during this time result = win.exec_() + if result == QDialog.Accepted: log.info('Start processing') else: log.info('Cancel processing') return + effect = win.effect + + # Create Effect - effect = openshot.EffectInfo().CreateEffect(name) + else: + effect = openshot.EffectInfo().CreateEffect(name) # Get Effect JSON effect.Id(get_app().project.generate_id()) @@ -2980,6 +2985,7 @@ def addEffect(self, effect_names, position): # Append effect JSON to clip clip.data["effects"].append(effect_json) + print(clip.data["effects"]) # Update clip data for project self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True) From 6f5855a1f827c3f4673d023f31a25d9dc528d993 Mon Sep 17 00:00:00 2001 From: Brenno Date: Fri, 17 Jul 2020 17:00:00 -0300 Subject: [PATCH 03/18] added comunication with CV processing effects --- src/windows/process_effect.py | 145 +++++++++++++++++++++++--- src/windows/views/timeline_webview.py | 10 +- 2 files changed, 134 insertions(+), 21 deletions(-) diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index e1cfc885d8..c7b00af5c6 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -30,7 +30,7 @@ import time from PyQt5.QtCore import * -from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem +from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem, QPixmap from PyQt5.QtWidgets import * from PyQt5 import uic import openshot # Python module for libopenshot (required video editing module installed separately) @@ -40,6 +40,53 @@ from classes.logger import log from classes.metrics import * +class BbWindow(QDialog): + def __init__(self, frame): + super().__init__() + self.setWindowModality(Qt.ApplicationModal) + self.title = "Bounding Box Selector" + self.top = 200 + self.left = 500 + self.width = 300 + self.height = 200 + self.windowIconName = os.path.join(info.PATH, 'xdg', 'openshot-arrow.png') + self.rubberBand = QRubberBand(QRubberBand.Rectangle, self) + self.InitWindow(frame) + self.origin = QPoint() + + def InitWindow(self,frame): + self.setWindowIcon(QIcon(self.windowIconName)) + self.setWindowTitle(self.title) + self.setGeometry(self.left, self.top, self.width, self.height) + vbox = QVBoxLayout() + labelImage = QLabel(self) + pixmap = QPixmap(frame) + labelImage.setPixmap(pixmap) + vbox.addWidget(labelImage) + self.setLayout(vbox) + self.show() + + def closeEvent(self, event): + event.accept() + + # Set top left rectangle coordinate + def mousePressEvent(self, event): + + if event.button() == Qt.LeftButton: + self.origin = QPoint(event.pos()) + self.rubberBand.setGeometry(QRect(self.origin, QSize())) + self.rubberBand.show() + + # Change rectangle selection while the mouse moves + def mouseMoveEvent(self, event): + + if not self.origin.isNull(): + self.end = event.pos() + self.rubberBand.setGeometry(QRect(self.origin, self.end).normalized()) + + # Return bounding box selection coordinates + def getBB(self): + return self.origin.x(), self.origin.y(), self.end.x() - self.origin.x(), self.end.y() - self.origin.y() class ProcessEffect(QDialog): """ Choose Profile Dialog """ @@ -84,6 +131,10 @@ def __init__(self, clip_id, effect_name): self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) + # flag to close the clip processing thread + self.cancel_clip_processing = False + self.effect = None + def accept(self): """ Start processing effect """ # Disable UI @@ -96,21 +147,49 @@ def accept(self): timeline_instance = get_app().window.timeline_sync.timeline for clip_instance in timeline_instance.Clips(): if clip_instance.Id() == self.clip_id: - self.protobufPath = openshot.ClipProcessingJobs(self.effect_name, clip_instance).stabilizeVideo(clip_instance) - self.effect = openshot.EffectInfo().CreateEffect(self.effect_name, "/media/brenno/Data/projects/openshot/stabilization.data") - # self.effect.SetJson('{"Stabilizer":{"protobuf_data_path": "/home/gustavostahl/LabVisao/VideoEditor/openshot-qt/stabilization.data"}}') - # clip_instance.AddEffect(self.effect) - # return self.effect - print("Apply effect: %s to clip: %s" % (self.effect_name, clip_instance.Id())) - - - # EXAMPLE progress updates - # for value in range(1, 100, 4): - # self.progressBar.setValue(value) - # time.sleep(0.25) - - # # Process any queued events - # QCoreApplication.processEvents() + self.clip_instance = clip_instance + break + + # Create effect Id and protobuf data path + ID = get_app().project.generate_id() + + protobufFolderPath = os.path.join(info.PATH, '..', 'protobuf_data') + # Check if protobuf data folder exists, otherwise it will create one + if not os.path.exists(protobufFolderPath): + os.mkdir(protobufFolderPath) + + # Create protobuf data path + protobufPath = os.path.join(protobufFolderPath, ID + '.data') + + # Load into JSON string info abou protobuf data path + jsonString = self.generateJson(protobufPath) + + # Generate processed data + processing = openshot.ClipProcessingJobs(self.effect_name, jsonString) + processing.processClip(self.clip_instance) + + # get processing status + while(not processing.IsDone() ): + # update progressbar + progressionStatus = processing.GetProgress() + self.progressBar.setValue(progressionStatus) + time.sleep(0.01) + + # Process any queued events + QCoreApplication.processEvents() + + # if the cancel button was pressed, close the processing thread + if(self.cancel_clip_processing): + processing.CancelProcessing() + + if(not self.cancel_clip_processing): + + # Load processed data into effect + self.effect = openshot.EffectInfo().CreateEffect(self.effect_name) + self.effect.SetJson( '{"protobuf_data_path": "%s"}' % protobufPath ) + self.effect.Id(ID) + + print("Applied effect: %s to clip: %s" % (self.effect_name, self.clip_instance.Id())) # Accept dialog super(ProcessEffect, self).accept() @@ -118,4 +197,38 @@ def accept(self): def reject(self): # Cancel dialog self.exporting = False + self.cancel_clip_processing = True super(ProcessEffect, self).reject() + + def generateJson(self, protobufPath): + # Start JSON string with the protobuf data path, necessary for all pre-processing effects + jsonString = '{"protobuf_data_path": "%s"' % protobufPath + + if self.effect_name == "Stabilizer": + pass + + # Special case where more info is needed for the JSON string + if self.effect_name == "Tracker": + + # Create temporary imagem to be shown in PyQt Window + temp_img_path = protobufPath.split(".data")[0] + '.png' + self.clip_instance.GetFrame(0).Save(temp_img_path, 1) + + # Show bounding box selection window + bbWindow = BbWindow(temp_img_path) + bbWindow.exec_() + + # Remove temporary image + os.remove(temp_img_path) + + # Get bounding box selection coordinates + bb = bbWindow.getBB() + + # Set tracker info in JSON string + trackerType = "KCF" + jsonString += ',"tracker_type": "%s"'%trackerType + jsonString += ',"bbox": {"x": %d, "y": %d, "w": %d, "h": %d}'%(bb[0],bb[1],bb[2],bb[3]) + + # Finish JSON string + jsonString+='}' + return jsonString diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index bfc2403b60..17910b8cb7 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -2962,7 +2962,7 @@ def addEffect(self, effect_names, position): if name in ["Bars", "Stabilizer", "Tracker"]: from windows.process_effect import ProcessEffect - win = ProcessEffect(clip.id, "Stabilizer") + win = ProcessEffect(clip.id, name) # Run the dialog event loop - blocking interaction on this window during this time result = win.exec_() @@ -2972,15 +2972,15 @@ def addEffect(self, effect_names, position): log.info('Cancel processing') return - effect = win.effect - - # Create Effect + effect = win.effect # effect.Id already set + if effect is None: + break else: effect = openshot.EffectInfo().CreateEffect(name) + effect.Id(get_app().project.generate_id()) # Get Effect JSON - effect.Id(get_app().project.generate_id()) effect_json = json.loads(effect.Json()) # Append effect JSON to clip From 2b71b6133c9f407f85cdc1ad7ccd727aac844eaf Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 18 Jul 2020 17:07:14 -0300 Subject: [PATCH 04/18] Merged with dynamic effects UI dialog --- .gitignore | 2 + src/classes/effect_init.py | 153 +++++++++++++ src/windows/about.py | 16 +- src/windows/main_window.py | 1 + src/windows/process_effect.py | 301 +++++++++++++++++++------- src/windows/region.py | 250 +++++++++++++++++++++ src/windows/ui/process-effect.ui | 104 +++++++++ src/windows/ui/region.ui | 136 ++++++++++++ src/windows/video_widget.py | 117 +++++++++- src/windows/views/timeline_webview.py | 11 +- 10 files changed, 998 insertions(+), 93 deletions(-) create mode 100644 src/classes/effect_init.py create mode 100644 src/windows/region.py create mode 100644 src/windows/ui/process-effect.ui create mode 100644 src/windows/ui/region.ui diff --git a/.gitignore b/.gitignore index a8a5ba423d..b1f2fd0604 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ doc/_build build dist openshot_qt.egg-info + +protobuf_data/ diff --git a/src/classes/effect_init.py b/src/classes/effect_init.py new file mode 100644 index 0000000000..882203cf03 --- /dev/null +++ b/src/classes/effect_init.py @@ -0,0 +1,153 @@ +""" + @file + @brief This file contains some Effect metadata related to pre-processing of Effects + @author Jonathan Thomas + @author Frank Dana + + @section LICENSE + + Copyright (c) 2008-2018 OpenShot Studios, LLC + (http://www.openshotstudios.com). This file is part of + OpenShot Video Editor (http://www.openshot.org), an open-source project + dedicated to delivering high quality video editing and animation solutions + to the world. + + OpenShot Video Editor is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenShot Video Editor is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenShot Library. If not, see . + """ + +# Not all Effects support pre-processing, so for now, this is a hard-coded +# solution to providing the pre-processing params needed for these special effects. + +effect_options = { + # TODO: Remove Bars example options + "Bars": [ + { + "title": "Region", + "setting": "region", + "x": 0.05, + "y": 0.05, + "width": 0.25, + "height": 0.25, + "type": "rect" + }, + { + "max": 100, + "title": "Volume", + "min": 0, + "setting": "volume", + "value": 75, + "type": "spinner" + }, + { + "value": "blender", + "title": "Blender Command (path)", + "type": "text", + "setting": "blender_command" + }, + { + "max": 192000, + "title": "Default Audio Sample Rate", + "min": 22050, + "setting": "default-samplerate", + "value": 44100, + "values": [ + { + "value": 22050, + "name": "22050" + }, + { + "value": 44100, + "name": "44100" + }, + { + "value": 48000, + "name": "48000" + }, + { + "value": 96000, + "name": "96000" + }, + { + "value": 192000, + "name": "192000" + } + ], + "type": "dropdown", + } + ], + + "Tracker": [ + { + "title": "Region", + "setting": "region", + "x": 0.05, + "y": 0.05, + "width": 0.25, + "height": 0.25, + "type": "rect" + }, + + { + "title": "Tracker type", + "setting": "tracker-type", + "value": "KCF", + "values": [ + { + "value": "KCF", + "name": "KCF" + }, + { + "value": "MIL", + "name": "MIL" + }, + { + "value": "BOOSTING", + "name": "BOOSTING" + }, + { + "value": "TLD", + "name": "TLD" + }, + { + "value": "MEDIANFLOW", + "name": "MEDIANFLOW" + }, + { + "value": "GOTURN", + "name": "GOTURN" + }, + { + "value": "MOSSE", + "name": "MOSSE" + }, + { + "value": "CSRT", + "name": "CSRT" + } + ], + "type": "dropdown", + } + ], + + "Stabilizer": [ + { + "max": 100, + "title": "Smoothing window", + "min": 1, + "setting": "smoothing-window", + "value": 30, + "type": "spinner" + } + ] +} diff --git a/src/windows/about.py b/src/windows/about.py index 920bebc46b..5c8a4b7d86 100644 --- a/src/windows/about.py +++ b/src/windows/about.py @@ -68,7 +68,7 @@ def __init__(self): changelog_path = os.path.join(info.PATH, 'settings', '%s.log' % project) if os.path.exists(changelog_path): # Attempt to open changelog with utf-8, and then utf-16-le (for unix / windows support) - for encoding_name in ('utf-8', 'utf_16_le'): + for encoding_name in ('utf_8', 'utf_16'): try: with codecs.open(changelog_path, 'r', encoding=encoding_name) as changelog_file: if changelog_file.read(): @@ -183,7 +183,7 @@ def __init__(self): # Get list of developers developer_list = [] - with codecs.open(os.path.join(info.RESOURCES_PATH, 'contributors.json'), 'r', 'utf-8') as contributors_file: + with codecs.open(os.path.join(info.RESOURCES_PATH, 'contributors.json'), 'r', 'utf_8') as contributors_file: developer_string = contributors_file.read() developer_list = json.loads(developer_string) @@ -214,7 +214,7 @@ def __init__(self): # Get list of supporters supporter_list = [] - with codecs.open(os.path.join(info.RESOURCES_PATH, 'supporters.json'), 'r', 'utf-8') as supporter_file: + with codecs.open(os.path.join(info.RESOURCES_PATH, 'supporters.json'), 'r', 'utf_8') as supporter_file: supporter_string = supporter_file.read() supporter_list = json.loads(supporter_string) @@ -259,7 +259,7 @@ def __init__(self): changelog_path = os.path.join(info.PATH, 'settings', 'openshot-qt.log') if os.path.exists(changelog_path): # Attempt to open changelog with utf-8, and then utf-16-le (for unix / windows support) - for encoding_name in ('utf-8', 'utf_16_le'): + for encoding_name in ('utf_8', 'utf_16'): try: with codecs.open(changelog_path, 'r', encoding=encoding_name) as changelog_file: for line in changelog_file: @@ -279,7 +279,7 @@ def __init__(self): changelog_path = os.path.join(info.PATH, 'settings', 'libopenshot.log') if os.path.exists(changelog_path): # Attempt to open changelog with utf-8, and then utf-16-le (for unix / windows support) - for encoding_name in ('utf-8', 'utf_16_le'): + for encoding_name in ('utf_8', 'utf_16'): try: with codecs.open(changelog_path, 'r', encoding=encoding_name) as changelog_file: for line in changelog_file: @@ -298,8 +298,10 @@ def __init__(self): changelog_list = [] changelog_path = os.path.join(info.PATH, 'settings', 'libopenshot-audio.log') if os.path.exists(changelog_path): - # Attempt to open changelog with utf-8, and then utf-16-le (for unix / windows support) - for encoding_name in ('utf-8', 'utf_16_le'): + # Attempt to support Linux- and Windows-encoded files by opening + # changelog with utf-8, then utf-16 (endianness via the BOM, which + # gets filtered out automatically by the decoder) + for encoding_name in ('utf_8', 'utf_16'): try: with codecs.open(changelog_path, 'r', encoding=encoding_name) as changelog_file: for line in changelog_file: diff --git a/src/windows/main_window.py b/src/windows/main_window.py index 0c0382cbd8..fd87e5a25f 100644 --- a/src/windows/main_window.py +++ b/src/windows/main_window.py @@ -94,6 +94,7 @@ class MainWindow(QMainWindow, updates.UpdateWatcher): FoundVersionSignal = pyqtSignal(str) WaveformReady = pyqtSignal(str, list) TransformSignal = pyqtSignal(str) + SelectRegionSignal = pyqtSignal(str) ExportStarted = pyqtSignal(str, int, int) ExportFrame = pyqtSignal(str, int, int, int) ExportEnded = pyqtSignal(str) diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index c7b00af5c6..e3bf4b2dc8 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -28,9 +28,11 @@ import os import sys import time +import operator +import functools from PyQt5.QtCore import * -from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem, QPixmap +from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem, QBrush, QPalette from PyQt5.QtWidgets import * from PyQt5 import uic import openshot # Python module for libopenshot (required video editing module installed separately) @@ -40,53 +42,6 @@ from classes.logger import log from classes.metrics import * -class BbWindow(QDialog): - def __init__(self, frame): - super().__init__() - self.setWindowModality(Qt.ApplicationModal) - self.title = "Bounding Box Selector" - self.top = 200 - self.left = 500 - self.width = 300 - self.height = 200 - self.windowIconName = os.path.join(info.PATH, 'xdg', 'openshot-arrow.png') - self.rubberBand = QRubberBand(QRubberBand.Rectangle, self) - self.InitWindow(frame) - self.origin = QPoint() - - def InitWindow(self,frame): - self.setWindowIcon(QIcon(self.windowIconName)) - self.setWindowTitle(self.title) - self.setGeometry(self.left, self.top, self.width, self.height) - vbox = QVBoxLayout() - labelImage = QLabel(self) - pixmap = QPixmap(frame) - labelImage.setPixmap(pixmap) - vbox.addWidget(labelImage) - self.setLayout(vbox) - self.show() - - def closeEvent(self, event): - event.accept() - - # Set top left rectangle coordinate - def mousePressEvent(self, event): - - if event.button() == Qt.LeftButton: - self.origin = QPoint(event.pos()) - self.rubberBand.setGeometry(QRect(self.origin, QSize())) - self.rubberBand.show() - - # Change rectangle selection while the mouse moves - def mouseMoveEvent(self, event): - - if not self.origin.isNull(): - self.end = event.pos() - self.rubberBand.setGeometry(QRect(self.origin, self.end).normalized()) - - # Return bounding box selection coordinates - def getBB(self): - return self.origin.x(), self.origin.y(), self.end.x() - self.origin.x(), self.end.y() - self.origin.y() class ProcessEffect(QDialog): """ Choose Profile Dialog """ @@ -95,15 +50,22 @@ class ProcessEffect(QDialog): # Path to ui file ui_path = os.path.join(info.PATH, 'windows', 'ui', 'process-effect.ui') - def __init__(self, clip_id, effect_name): + def __init__(self, clip_id, effect_name, effect_params): # Create dialog class QDialog.__init__(self) + # Track effect details + self.clip_id = clip_id + self.effect_name = effect_name + self.context = {} # Load UI from designer & init ui_util.load_ui(self, self.ui_path) ui_util.init_ui(self) + # Update window title + self.setWindowTitle(self.windowTitle() % self.effect_name) + # get translations _ = get_app()._tr @@ -113,17 +75,110 @@ def __init__(self, clip_id, effect_name): # Track metrics track_metric_screen("process-effect-screen") - # Init form - self.progressBar.setValue(0) - self.txtAdvanced.setText("{}") - self.setWindowTitle(_("%s: Initialize Effect") % effect_name) - self.clip_id = clip_id - self.effect_name = effect_name - - # Add combo entries - self.cboOptions.addItem("Option 1", 1) - self.cboOptions.addItem("Option 2", 2) - self.cboOptions.addItem("Option 3", 3) + # Loop through options and create widgets + row_count = 0 + for param in effect_params: + + # Create Label + widget = None + label = QLabel() + label.setText(_(param["title"])) + label.setToolTip(_(param["title"])) + + if param["type"] == "spinner": + # create QDoubleSpinBox + widget = QDoubleSpinBox() + widget.setMinimum(float(param["min"])) + widget.setMaximum(float(param["max"])) + widget.setValue(float(param["value"])) + widget.setSingleStep(1.0) + widget.setToolTip(param["title"]) + widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = float(param["value"]) + + if param["type"] == "rect": + # create QPushButton which opens up a display of the clip, with ability to select Rectangle + widget = QPushButton(_("Click to Select")) + widget.setMinimumHeight(80) + widget.setToolTip(param["title"]) + widget.clicked.connect(functools.partial(self.rect_select_clicked, widget, param)) + + # Set initial context + self.context[param["setting"]] = {"button-clicked": False, "x": 0, "y": 0, "width": 0, "height": 0} + + if param["type"] == "spinner-int": + # create QDoubleSpinBox + widget = QSpinBox() + widget.setMinimum(int(param["min"])) + widget.setMaximum(int(param["max"])) + widget.setValue(int(param["value"])) + widget.setSingleStep(1.0) + widget.setToolTip(param["title"]) + widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = int(param["value"]) + + elif param["type"] == "text": + # create QLineEdit + widget = QLineEdit() + widget.setText(_(param["value"])) + widget.textChanged.connect(functools.partial(self.text_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = param["value"] + + elif param["type"] == "bool": + # create spinner + widget = QCheckBox() + if param["value"] == True: + widget.setCheckState(Qt.Checked) + self.context[param["setting"]] = True + else: + widget.setCheckState(Qt.Unchecked) + self.context[param["setting"]] = False + widget.stateChanged.connect(functools.partial(self.bool_value_changed, widget, param)) + + elif param["type"] == "dropdown": + + # create spinner + widget = QComboBox() + + # Get values + value_list = param["values"] + + # Add normal values + box_index = 0 + for value_item in value_list: + k = value_item["name"] + v = value_item["value"] + i = value_item.get("icon", None) + + # add dropdown item + widget.addItem(_(k), v) + + # select dropdown (if default) + if v == param["value"]: + widget.setCurrentIndex(box_index) + + # Set initial context + self.context[param["setting"]] = param["value"] + box_index = box_index + 1 + + widget.currentIndexChanged.connect(functools.partial(self.dropdown_index_changed, widget, param)) + + # Add Label and Widget to the form + if (widget and label): + # Add minimum size + label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Create HBoxLayout for each field + self.scrollAreaWidgetContents.layout().insertRow(row_count - 1, label, widget) + + row_count += 1 # Add buttons self.cancel_button = QPushButton(_('Cancel')) @@ -134,13 +189,100 @@ def __init__(self, clip_id, effect_name): # flag to close the clip processing thread self.cancel_clip_processing = False self.effect = None + + def spinner_value_changed(self, widget, param, value): + """Spinner value change callback""" + self.context[param["setting"]] = value + log.info(self.context) + + def bool_value_changed(self, widget, param, state): + """Boolean value change callback""" + if state == Qt.Checked: + self.context[param["setting"]] = True + else: + self.context[param["setting"]] = False + log.info(self.context) + + def dropdown_index_changed(self, widget, param, index): + """Dropdown value change callback""" + value = widget.itemData(index) + self.context[param["setting"]] = value + log.info(self.context) + + def text_value_changed(self, widget, param, value=None): + """Textbox value change callback""" + try: + # Attempt to load value from QTextEdit (i.e. multi-line) + if not value: + value = widget.toPlainText() + except: + log.debug('Failed to get plain text from widget') + + self.context[param["setting"]] = value + log.info(self.context) + + def rect_select_clicked(self, widget, param): + """Rect select button clicked""" + self.context[param["setting"]].update({"button-clicked": True}) + + # show dialog + from windows.region import SelectRegion + from classes.query import File, Clip + + c = Clip.get(id=self.clip_id) + reader_path = c.data.get('reader', {}).get('path','') + f = File.get(path=reader_path) + if f: + win = SelectRegion(f, c) + # Run the dialog event loop - blocking interaction on this window during that time + result = win.exec_() + if result == QDialog.Accepted: + # Region selected (get coordinates if any) + topLeft = win.videoPreview.regionTopLeftHandle + bottomRight = win.videoPreview.regionBottomRightHandle + viewPortSize = win.viewport_rect + + # Get QImage of region + region_qimage = win.videoPreview.region_qimage + + # Resize QImage to match button size + resized_qimage = region_qimage.scaled(widget.size(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) + + # Draw Qimage onto QPushButton (to display region selection to user) + palette = widget.palette() + palette.setBrush(widget.backgroundRole(), QBrush(resized_qimage)) + widget.setFlat(True) + widget.setAutoFillBackground(True) + widget.setPalette(palette) + + # Remove button text (so region QImage is more visible) + widget.setText("") + + # If data found, add to context + if topLeft and bottomRight: + self.context[param["setting"]].update({"x": topLeft.x(), "y": topLeft.y(), + "width": bottomRight.x() - topLeft.x(), + "height": bottomRight.y() - topLeft.y(), + "scaled_x": topLeft.x() / viewPortSize.width(), "scaled_y": topLeft.y() / viewPortSize.height(), + "scaled_width": (bottomRight.x() - topLeft.x()) / viewPortSize.width(), + "scaled_height": (bottomRight.y() - topLeft.y()) / viewPortSize.height(), + }) + log.info(self.context) + + else: + log.error('No file found with path: %s' % reader_path) def accept(self): """ Start processing effect """ # Disable UI - self.cboOptions.setEnabled(False) - self.txtAdvanced.setEnabled(False) - self.process_button.setEnabled(False) + for child_widget in self.scrollAreaWidgetContents.children(): + child_widget.setEnabled(False) + + # Enable ProgressBar + self.progressBar.setEnabled(True) + + # Print effect settings + log.info(self.context) # DO WORK HERE, and periodically set progressBar value # Access C++ timeline and find the Clip instance which this effect should be applied to @@ -204,31 +346,26 @@ def generateJson(self, protobufPath): # Start JSON string with the protobuf data path, necessary for all pre-processing effects jsonString = '{"protobuf_data_path": "%s"' % protobufPath + # Obs: Following redundant variables were created for better code visualization (e.g. smoothingWindow) + + # Special case where more info is needed for the JSON string if self.effect_name == "Stabilizer": - pass + smoothingWindow = self.context["smoothing-window"] + jsonString += ', "smoothing_window": %d' % (smoothingWindow) # Special case where more info is needed for the JSON string if self.effect_name == "Tracker": - # Create temporary imagem to be shown in PyQt Window - temp_img_path = protobufPath.split(".data")[0] + '.png' - self.clip_instance.GetFrame(0).Save(temp_img_path, 1) - - # Show bounding box selection window - bbWindow = BbWindow(temp_img_path) - bbWindow.exec_() - - # Remove temporary image - os.remove(temp_img_path) - - # Get bounding box selection coordinates - bb = bbWindow.getBB() - # Set tracker info in JSON string - trackerType = "KCF" - jsonString += ',"tracker_type": "%s"'%trackerType - jsonString += ',"bbox": {"x": %d, "y": %d, "w": %d, "h": %d}'%(bb[0],bb[1],bb[2],bb[3]) + # Get selected tracker [KCF, MIL, TLD, BOOSTING, MEDIANFLOW, GOTURN, MOOSE, CSRT] + trackerType = self.context["tracker-type"] + jsonString += ',"tracker_type": "%s"' % trackerType + + # Get bounding box coordinates + tracker_dict = self.context["region"] + bbox = (tracker_dict["x"],tracker_dict["y"],tracker_dict["width"],tracker_dict["height"]) + jsonString += ',"bbox": {"x": %d, "y": %d, "w": %d, "h": %d}' % (bbox) # Finish JSON string jsonString+='}' - return jsonString + return jsonString \ No newline at end of file diff --git a/src/windows/region.py b/src/windows/region.py new file mode 100644 index 0000000000..3f16dfd4d9 --- /dev/null +++ b/src/windows/region.py @@ -0,0 +1,250 @@ +""" + @file + @brief This file loads the UI for selecting a region of a video (rectangle used for effect processing) + @author Jonathan Thomas + + @section LICENSE + + Copyright (c) 2008-2018 OpenShot Studios, LLC + (http://www.openshotstudios.com). This file is part of + OpenShot Video Editor (http://www.openshot.org), an open-source project + dedicated to delivering high quality video editing and animation solutions + to the world. + + OpenShot Video Editor is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenShot Video Editor is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenShot Library. If not, see . + """ + +import os +import sys +import functools +import math + +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * +import openshot # Python module for libopenshot (required video editing module installed separately) + +from classes import info, ui_util, time_parts, settings, qt_types, updates +from classes.app import get_app +from classes.logger import log +from classes.metrics import * +from windows.preview_thread import PreviewParent +from windows.video_widget import VideoWidget + +import json + +class SelectRegion(QDialog): + """ SelectRegion Dialog """ + + # Path to ui file + ui_path = os.path.join(info.PATH, 'windows', 'ui', 'region.ui') + + # Signals for preview thread + previewFrameSignal = pyqtSignal(int) + refreshFrameSignal = pyqtSignal() + LoadFileSignal = pyqtSignal(str) + PlaySignal = pyqtSignal(int) + PauseSignal = pyqtSignal() + SeekSignal = pyqtSignal(int) + SpeedSignal = pyqtSignal(float) + StopSignal = pyqtSignal() + + def __init__(self, file=None, clip=None): + _ = get_app()._tr + + # Create dialog class + QDialog.__init__(self) + + # Load UI from designer + ui_util.load_ui(self, self.ui_path) + + # Init UI + ui_util.init_ui(self) + + # Track metrics + track_metric_screen("cutting-screen") + + self.start_frame = 1 + self.start_image = None + self.end_frame = 1 + self.end_image = None + + # Keep track of file object + self.file = file + self.file_path = file.absolute_path() + self.video_length = int(file.data['video_length']) + self.fps_num = int(file.data['fps']['num']) + self.fps_den = int(file.data['fps']['den']) + self.fps = float(self.fps_num) / float(self.fps_den) + self.width = int(file.data['width']) + self.height = int(file.data['height']) + self.sample_rate = int(file.data['sample_rate']) + self.channels = int(file.data['channels']) + self.channel_layout = int(file.data['channel_layout']) + + # Open video file with Reader + log.info(self.file_path) + + # Add Video Widget + self.videoPreview = VideoWidget() + self.videoPreview.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + self.verticalLayout.insertWidget(0, self.videoPreview) + + # Set max size of video preview (for speed) + self.viewport_rect = self.videoPreview.centeredViewport(self.videoPreview.width(), self.videoPreview.height()) + + # Create an instance of a libopenshot Timeline object + self.r = openshot.Timeline(self.viewport_rect.width(), self.viewport_rect.height(), openshot.Fraction(self.fps_num, self.fps_den), self.sample_rate, self.channels, self.channel_layout) + self.r.info.channel_layout = self.channel_layout + self.r.SetMaxSize(self.viewport_rect.width(), self.viewport_rect.height()) + + try: + # Add clip for current preview file + self.clip = openshot.Clip(self.file_path) + + # Show waveform for audio files + if not self.clip.Reader().info.has_video and self.clip.Reader().info.has_audio: + self.clip.Waveform(True) + + # Set has_audio property + self.r.info.has_audio = self.clip.Reader().info.has_audio + + # Update video_length property of the Timeline object + self.r.info.video_length = self.video_length + + self.r.AddClip(self.clip) + except: + log.error('Failed to load media file into region select player: %s' % self.file_path) + return + + # Open reader + self.r.Open() + + # Start the preview thread + self.initialized = False + self.transforming_clip = False + self.preview_parent = PreviewParent() + self.preview_parent.Init(self, self.r, self.videoPreview) + self.preview_thread = self.preview_parent.worker + + # Set slider constraints + self.sliderIgnoreSignal = False + self.sliderVideo.setMinimum(1) + self.sliderVideo.setMaximum(self.video_length) + self.sliderVideo.setSingleStep(1) + self.sliderVideo.setSingleStep(1) + self.sliderVideo.setPageStep(24) + + # Determine if a start or end attribute is in this file + start_frame = 1 + if 'start' in self.file.data.keys(): + start_frame = (float(self.file.data['start']) * self.fps) + 1 + + # Display start frame (and then the previous frame) + QTimer.singleShot(500, functools.partial(self.sliderVideo.setValue, start_frame + 1)) + QTimer.singleShot(600, functools.partial(self.sliderVideo.setValue, start_frame)) + + # Add buttons + self.cancel_button = QPushButton(_('Cancel')) + self.process_button = QPushButton(_('Select Region')) + self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) + self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) + + # Connect signals + self.actionPlay.triggered.connect(self.actionPlay_Triggered) + self.btnPlay.clicked.connect(self.btnPlay_clicked) + self.sliderVideo.valueChanged.connect(self.sliderVideo_valueChanged) + self.initialized = True + + get_app().window.SelectRegionSignal.emit(clip.id) + + def actionPlay_Triggered(self): + # Trigger play button (This action is invoked from the preview thread, so it must exist here) + self.btnPlay.click() + + def movePlayhead(self, frame_number): + """Update the playhead position""" + + # Move slider to correct frame position + self.sliderIgnoreSignal = True + self.sliderVideo.setValue(frame_number) + self.sliderIgnoreSignal = False + + # Convert frame to seconds + seconds = (frame_number-1) / self.fps + + # Convert seconds to time stamp + time_text = time_parts.secondsToTime(seconds, self.fps_num, self.fps_den) + timestamp = "%s:%s:%s:%s" % (time_text["hour"], time_text["min"], time_text["sec"], time_text["frame"]) + + # Update label + self.lblVideoTime.setText(timestamp) + + def btnPlay_clicked(self, force=None): + log.info("btnPlay_clicked") + + if force == "pause": + self.btnPlay.setChecked(False) + elif force == "play": + self.btnPlay.setChecked(True) + + if self.btnPlay.isChecked(): + log.info('play (icon to pause)') + ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-pause") + self.preview_thread.Play(self.video_length) + else: + log.info('pause (icon to play)') + ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-start") # to default + self.preview_thread.Pause() + + # Send focus back to toolbar + self.sliderVideo.setFocus() + + def sliderVideo_valueChanged(self, new_frame): + if self.preview_thread and not self.sliderIgnoreSignal: + log.info('sliderVideo_valueChanged') + + # Pause video + self.btnPlay_clicked(force="pause") + + # Seek to new frame + self.preview_thread.previewFrame(new_frame) + + def accept(self): + """ Ok button clicked """ + self.shutdownPlayer() + super(SelectRegion, self).accept() + + def shutdownPlayer(self): + log.info('shutdownPlayer') + + # Stop playback + self.preview_parent.worker.Stop() + + # Stop preview thread (and wait for it to end) + self.preview_parent.worker.kill() + self.preview_parent.background.exit() + self.preview_parent.background.wait(5000) + + # Close readers + self.r.Close() + self.clip.Close() + self.r.ClearAllCache() + + def reject(self): + # Cancel dialog + self.shutdownPlayer() + super(SelectRegion, self).reject() + + + diff --git a/src/windows/ui/process-effect.ui b/src/windows/ui/process-effect.ui new file mode 100644 index 0000000000..1f778d491e --- /dev/null +++ b/src/windows/ui/process-effect.ui @@ -0,0 +1,104 @@ + + + Dialog + + + + 0 + 0 + 410 + 217 + + + + + 410 + 100 + + + + %s: Initialize Effect + + + + + + true + + + + + 0 + 0 + 390 + 160 + + + + + 0 + 0 + + + + + + + + + + false + + + 0 + + + + + + + Qt::Horizontal + + + QDialogButtonBox::NoButton + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/windows/ui/region.ui b/src/windows/ui/region.ui new file mode 100644 index 0000000000..1b61d0e2fe --- /dev/null +++ b/src/windows/ui/region.ui @@ -0,0 +1,136 @@ + + + Dialog + + + + 0 + 0 + 584 + 434 + + + + Select Region + + + + :/openshot.svg:/openshot.svg + + + false + + + + + + true + + + Qt::Horizontal + + + QDialogButtonBox::NoButton + + + + + + + + + + + Draw a rectangle to select a region of the video frame. + + + + + + + + + + + + + .. + + + true + + + + + + + true + + + Qt::Horizontal + + + + + + + 00:00:00:1 + + + + + + + + + + + + + + .. + + + Play + + + Play + + + + + + + buttonBox + rejected() + Dialog + reject() + + + 184 + 234 + + + 286 + 274 + + + + + buttonBox + accepted() + Dialog + accept() + + + 184 + 228 + + + 157 + 274 + + + + + diff --git a/src/windows/video_widget.py b/src/windows/video_widget.py index 2f595e6403..c8705027b0 100644 --- a/src/windows/video_widget.py +++ b/src/windows/video_widget.py @@ -25,7 +25,7 @@ along with OpenShot Library. If not, see . """ -from PyQt5.QtCore import QSize, Qt, QCoreApplication, QPointF, QRect, QRectF, QMutex, QTimer +from PyQt5.QtCore import QSize, Qt, QCoreApplication, QPointF, QPoint, QRect, QRectF, QMutex, QTimer from PyQt5.QtGui import * from PyQt5.QtWidgets import QSizePolicy, QWidget, QPushButton import openshot # Python module for libopenshot (required video editing module installed separately) @@ -269,6 +269,45 @@ def paintEvent(self, event, *args): # Remove transform painter.resetTransform() + if self.region_enabled: + # Paint region selector onto video preview + self.region_transform = QTransform() + + # Init X/Y + x = viewport_rect.x() + y = viewport_rect.y() + + # Apply translate/move + if x or y: + self.region_transform.translate(x, y) + + # Apply scale (if any) + if self.zoom: + self.region_transform.scale(self.zoom, self.zoom) + + # Apply transform + self.region_transform_inverted = self.region_transform.inverted()[0] + painter.setTransform(self.region_transform) + + # Draw transform corners and center origin circle + # Corner size + cs = 14.0 + + if self.regionTopLeftHandle and self.regionBottomRightHandle: + # Draw 2 corners and bounding box + pen = QPen(QBrush(QColor("#53a0ed")), 1.5) + pen.setCosmetic(True) + painter.setPen(pen) + painter.drawRect(self.regionTopLeftHandle.x() - (cs/2.0/self.zoom), self.regionTopLeftHandle.y() - (cs/2.0/self.zoom), self.regionTopLeftHandle.width() / self.zoom, self.regionTopLeftHandle.height() / self.zoom) + painter.drawRect(self.regionBottomRightHandle.x() - (cs/2.0/self.zoom), self.regionBottomRightHandle.y() - (cs/2.0/self.zoom), self.regionBottomRightHandle.width() / self.zoom, self.regionBottomRightHandle.height() / self.zoom) + region_rect = QRectF(self.regionTopLeftHandle.x(), self.regionTopLeftHandle.y(), + self.regionBottomRightHandle.x() - self.regionTopLeftHandle.x(), + self.regionBottomRightHandle.y() - self.regionTopLeftHandle.y()) + painter.drawRect(region_rect) + + # Remove transform + painter.resetTransform() + # End painter painter.end() @@ -323,6 +362,27 @@ def mouseReleaseEvent(self, event): self.mouse_pressed = False self.mouse_dragging = False self.transform_mode = None + self.region_mode = None + + # Save region image data (as QImage) + # This can be used other widgets to display the selected region + if self.region_enabled: + # Find centered viewport + viewport_rect = self.centeredViewport(self.width(), self.height()) + + # Get region coordinates + region_rect = QRectF(self.regionTopLeftHandle.x(), self.regionTopLeftHandle.y(), + self.regionBottomRightHandle.x() - self.regionTopLeftHandle.x(), + self.regionBottomRightHandle.y() - self.regionTopLeftHandle.y()).normalized() + + # Map region (due to zooming) + mapped_region_rect = self.region_transform.mapToPolygon(region_rect.toRect()).boundingRect() + + # Create QImage from videoPreview widget + self.region_qimage = QImage(mapped_region_rect.width(), mapped_region_rect.height(), QImage.Format_ARGB32) + region_painter = QPainter(self.region_qimage) + self.render(region_painter, QPoint(0,0), QRegion(mapped_region_rect, QRegion.Rectangle)) + region_painter.end() # Inform UpdateManager to accept updates, and only store our final update get_app().updates.ignore_history = False @@ -348,6 +408,7 @@ def mouseMoveEvent(self, event): self.mouse_dragging = True if self.transforming_clip: + # Modify clip transform properties (x, y, height, width, rotation, shear) # Get framerate fps = get_app().project.get("fps") fps_float = float(fps["num"]) / float(fps["den"]) @@ -630,6 +691,48 @@ def mouseMoveEvent(self, event): # Force re-paint self.update() + if self.region_enabled: + # Modify region selection (x, y, width, height) + # Corner size + cs = 14.0 + + # Adjust existing region coordinates (if any) + if not self.mouse_dragging and self.resize_button.isVisible() and self.resize_button.rect().contains(event.pos()): + # Mouse over resize button (and not currently dragging) + self.setCursor(Qt.ArrowCursor) + elif self.region_transform and self.regionTopLeftHandle and self.region_transform.mapToPolygon(self.regionTopLeftHandle.toRect()).containsPoint(event.pos(), Qt.OddEvenFill): + if not self.region_mode or self.region_mode == 'scale_top_left': + self.setCursor(self.rotateCursor(self.cursors.get('resize_fdiag'), 0, 0, 0)) + # Set the region mode + if self.mouse_dragging and not self.region_mode: + self.region_mode = 'scale_top_left' + elif self.region_transform and self.regionBottomRightHandle and self.region_transform.mapToPolygon(self.regionBottomRightHandle.toRect()).containsPoint(event.pos(), Qt.OddEvenFill): + if not self.region_mode or self.region_mode == 'scale_bottom_right': + self.setCursor(self.rotateCursor(self.cursors.get('resize_fdiag'), 0, 0, 0)) + # Set the region mode + if self.mouse_dragging and not self.region_mode: + self.region_mode = 'scale_bottom_right' + else: + self.setCursor(Qt.ArrowCursor) + + # Initialize new region coordinates at current event.pos() + if self.mouse_dragging and not self.region_mode: + self.region_mode = 'scale_bottom_right' + self.regionTopLeftHandle = QRectF(self.region_transform_inverted.map(event.pos()).x(), self.region_transform_inverted.map(event.pos()).y(), cs, cs) + self.regionBottomRightHandle = QRectF(self.region_transform_inverted.map(event.pos()).x(), self.region_transform_inverted.map(event.pos()).y(), cs, cs) + + # Move existing region coordinates + if self.mouse_dragging: + diff_x = self.region_transform_inverted.map(event.pos()).x() - self.region_transform_inverted.map(self.mouse_position).x() + diff_y = self.region_transform_inverted.map(event.pos()).y() - self.region_transform_inverted.map(self.mouse_position).y() + if self.region_mode == 'scale_top_left': + self.regionTopLeftHandle.adjust(diff_x, diff_y, diff_x, diff_y) + elif self.region_mode == 'scale_bottom_right': + self.regionBottomRightHandle.adjust(diff_x, diff_y, diff_x, diff_y) + + # Repaint widget on zoom + self.update() + # Update mouse position self.mouse_position = event.pos() @@ -707,6 +810,11 @@ def transformTriggered(self, clip_id): get_app().window.refreshFrameSignal.emit() get_app().window.propertyTableView.select_frame(get_app().window.preview_thread.player.Position()) + def regionTriggered(self, clip_id): + """Handle the 'select region' signal when it's emitted""" + self.region_enabled = True + get_app().window.refreshFrameSignal.emit() + def resizeEvent(self, event): """Widget resize event""" self.delayed_size = self.size() @@ -795,6 +903,12 @@ def __init__(self, *args): self.transform_mode = None self.gravity_point = None self.original_clip_data = None + self.region_qimage = None + self.region_transform = None + self.region_enabled = False + self.region_mode = None + self.regionTopLeftHandle = None + self.regionBottomRightHandle = None self.zoom = 1.0 # Zoom of widget (does not affect video, only workspace) self.resize_button = QPushButton(_('Reset Zoom'), self) self.resize_button.hide() @@ -843,4 +957,5 @@ def __init__(self, *args): # Connect to signals self.win.TransformSignal.connect(self.transformTriggered) + self.win.SelectRegionSignal.connect(self.regionTriggered) self.win.refreshFrameSignal.connect(self.refreshTriggered) diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index 17910b8cb7..7ff3067067 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -47,6 +47,7 @@ from classes.query import File, Clip, Transition, Track from classes.waveform import get_audio_data from classes.conversion import zoomToSeconds, secondsToZoom +from classes.effect_init import effect_options import json @@ -2959,10 +2960,14 @@ def addEffect(self, effect_names, position): log.info(clip) # Handle custom effect dialogs - if name in ["Bars", "Stabilizer", "Tracker"]: + if name in effect_options: + # Get effect options + effect_params = effect_options.get(name) + + # Show effect pre-processing window from windows.process_effect import ProcessEffect - win = ProcessEffect(clip.id, name) + win = ProcessEffect(clip.id, name, effect_params) # Run the dialog event loop - blocking interaction on this window during this time result = win.exec_() @@ -2972,7 +2977,7 @@ def addEffect(self, effect_names, position): log.info('Cancel processing') return - # Create Effect + # Create Effect effect = win.effect # effect.Id already set if effect is None: break From e3001da271552f9802ca654c0e28ee64b1d9c499 Mon Sep 17 00:00:00 2001 From: Brenno Date: Thu, 23 Jul 2020 20:07:24 -0300 Subject: [PATCH 05/18] Added interval to apply OpenCV effects --- src/classes/effect_init.py | 1 + src/windows/process_effect.py | 19 ++++++++---- src/windows/region.py | 54 ++++++++++++++++++++++++----------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/classes/effect_init.py b/src/classes/effect_init.py index 882203cf03..8c563f1906 100644 --- a/src/classes/effect_init.py +++ b/src/classes/effect_init.py @@ -95,6 +95,7 @@ "y": 0.05, "width": 0.25, "height": 0.25, + "first-frame": 1, "type": "rect" }, diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index e3bf4b2dc8..dcc21d4e0c 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -59,6 +59,12 @@ def __init__(self, clip_id, effect_name, effect_params): self.effect_name = effect_name self.context = {} + timeline_instance = get_app().window.timeline_sync.timeline + for clip_instance in timeline_instance.Clips(): + if clip_instance.Id() == self.clip_id: + self.clip_instance = clip_instance + break + # Load UI from designer & init ui_util.load_ui(self, self.ui_path) ui_util.init_ui(self) @@ -233,10 +239,11 @@ def rect_select_clicked(self, widget, param): reader_path = c.data.get('reader', {}).get('path','') f = File.get(path=reader_path) if f: - win = SelectRegion(f, c) + win = SelectRegion(f, self.clip_instance) # Run the dialog event loop - blocking interaction on this window during that time result = win.exec_() if result == QDialog.Accepted: + # self.first_frame = win.current_frame # Region selected (get coordinates if any) topLeft = win.videoPreview.regionTopLeftHandle bottomRight = win.videoPreview.regionBottomRightHandle @@ -266,6 +273,7 @@ def rect_select_clicked(self, widget, param): "scaled_x": topLeft.x() / viewPortSize.width(), "scaled_y": topLeft.y() / viewPortSize.height(), "scaled_width": (bottomRight.x() - topLeft.x()) / viewPortSize.width(), "scaled_height": (bottomRight.y() - topLeft.y()) / viewPortSize.height(), + "first-frame": win.current_frame }) log.info(self.context) @@ -286,11 +294,6 @@ def accept(self): # DO WORK HERE, and periodically set progressBar value # Access C++ timeline and find the Clip instance which this effect should be applied to - timeline_instance = get_app().window.timeline_sync.timeline - for clip_instance in timeline_instance.Clips(): - if clip_instance.Id() == self.clip_id: - self.clip_instance = clip_instance - break # Create effect Id and protobuf data path ID = get_app().project.generate_id() @@ -365,6 +368,10 @@ def generateJson(self, protobufPath): tracker_dict = self.context["region"] bbox = (tracker_dict["x"],tracker_dict["y"],tracker_dict["width"],tracker_dict["height"]) jsonString += ',"bbox": {"x": %d, "y": %d, "w": %d, "h": %d}' % (bbox) + + # Get processing start frame + print(self.context["region"]) + jsonString +=',"first_frame": %d' % (self.context["region"]["first-frame"]) # Finish JSON string jsonString+='}' diff --git a/src/windows/region.py b/src/windows/region.py index 3f16dfd4d9..3e8f68f30d 100644 --- a/src/windows/region.py +++ b/src/windows/region.py @@ -78,22 +78,30 @@ def __init__(self, file=None, clip=None): self.start_image = None self.end_frame = 1 self.end_image = None + self.current_frame = 1 + + self.clip = clip + self.clip.Open() + self.clip_position = self.clip.Position() + self.clip.Position(0) # Keep track of file object - self.file = file - self.file_path = file.absolute_path() - self.video_length = int(file.data['video_length']) - self.fps_num = int(file.data['fps']['num']) - self.fps_den = int(file.data['fps']['den']) - self.fps = float(self.fps_num) / float(self.fps_den) - self.width = int(file.data['width']) - self.height = int(file.data['height']) - self.sample_rate = int(file.data['sample_rate']) - self.channels = int(file.data['channels']) - self.channel_layout = int(file.data['channel_layout']) + # self.file = file + # self.file_path = file.absolute_path() + + c_info = clip.Reader().info + self.fps = c_info.fps.ToInt() #float(self.fps_num) / float(self.fps_den) + self.fps_num = self.fps #int(file.data['fps']['num']) + self.fps_den = 1 #int(file.data['fps']['den']) + self.width = c_info.width #int(file.data['width']) + self.height = c_info.height #int(file.data['height']) + self.sample_rate = c_info.sample_rate #int(file.data['sample_rate']) + self.channels = c_info.channels #int(file.data['channels']) + self.channel_layout = c_info.channel_layout #int(file.data['channel_layout']) + self.video_length = int(self.clip.Duration() * self.fps) + 1 #int(file.data['video_length']) # Open video file with Reader - log.info(self.file_path) + log.info(self.clip.Reader()) # Add Video Widget self.videoPreview = VideoWidget() @@ -110,7 +118,7 @@ def __init__(self, file=None, clip=None): try: # Add clip for current preview file - self.clip = openshot.Clip(self.file_path) + # self.clip = openshot.Clip(self.file_path) # Show waveform for audio files if not self.clip.Reader().info.has_video and self.clip.Reader().info.has_audio: @@ -123,6 +131,7 @@ def __init__(self, file=None, clip=None): self.r.info.video_length = self.video_length self.r.AddClip(self.clip) + except: log.error('Failed to load media file into region select player: %s' % self.file_path) return @@ -147,8 +156,8 @@ def __init__(self, file=None, clip=None): # Determine if a start or end attribute is in this file start_frame = 1 - if 'start' in self.file.data.keys(): - start_frame = (float(self.file.data['start']) * self.fps) + 1 + # if 'start' in self.file.data.keys(): + # start_frame = (float(self.file.data['start']) * self.fps) + 1 # Display start frame (and then the previous frame) QTimer.singleShot(500, functools.partial(self.sliderVideo.setValue, start_frame + 1)) @@ -166,7 +175,7 @@ def __init__(self, file=None, clip=None): self.sliderVideo.valueChanged.connect(self.sliderVideo_valueChanged) self.initialized = True - get_app().window.SelectRegionSignal.emit(clip.id) + get_app().window.SelectRegionSignal.emit(clip.Id()) def actionPlay_Triggered(self): # Trigger play button (This action is invoked from the preview thread, so it must exist here) @@ -175,6 +184,7 @@ def actionPlay_Triggered(self): def movePlayhead(self, frame_number): """Update the playhead position""" + self.current_frame = frame_number # Move slider to correct frame position self.sliderIgnoreSignal = True self.sliderVideo.setValue(frame_number) @@ -222,10 +232,16 @@ def sliderVideo_valueChanged(self, new_frame): def accept(self): """ Ok button clicked """ + + self.clip.Position(self.clip_position) + self.shutdownPlayer() super(SelectRegion, self).accept() def shutdownPlayer(self): + + self.clip.Position(self.clip_position) + log.info('shutdownPlayer') # Stop playback @@ -237,11 +253,15 @@ def shutdownPlayer(self): self.preview_parent.background.wait(5000) # Close readers + self.r.RemoveClip(self.clip) self.r.Close() - self.clip.Close() + # self.clip.Close() self.r.ClearAllCache() def reject(self): + + self.clip.Position(self.clip_position) + # Cancel dialog self.shutdownPlayer() super(SelectRegion, self).reject() From 55f61039a6a875aca9a0f8718a3c8a914b088069 Mon Sep 17 00:00:00 2001 From: Brenno Date: Sun, 26 Jul 2020 16:25:18 -0300 Subject: [PATCH 06/18] Added integration with Object Detector effect --- src/classes/effect_init.py | 37 ++++++++++++++++++++++++++ src/effects/icons/objectdetector.png | Bin 0 -> 3916 bytes src/windows/process_effect.py | 16 ++++++++++- src/windows/views/timeline_webview.py | 5 ++-- 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/effects/icons/objectdetector.png diff --git a/src/classes/effect_init.py b/src/classes/effect_init.py index 8c563f1906..d12b880cfa 100644 --- a/src/classes/effect_init.py +++ b/src/classes/effect_init.py @@ -150,5 +150,42 @@ "value": 30, "type": "spinner" } + ], + + "Object Detector": [ + { + "value": "../yolo/yolov3.weights", + "title": "Model Weights", + "type": "text", + "setting": "model-weights" + }, + { + "value": "../yolo/yolov3.cfg", + "title": "Model Config", + "type": "text", + "setting": "model-config" + }, + { + "value": "../yolo/obj.names", + "title": "Class names", + "type": "text", + "setting": "class-names" + }, + { + "title": "Processing Device", + "setting": "processing-device", + "value": "GPU", + "values": [ + { + "value": "GPU", + "name": "GPU" + }, + { + "value": "CPU", + "name": "CPU" + } + ], + "type": "dropdown", + } ] } diff --git a/src/effects/icons/objectdetector.png b/src/effects/icons/objectdetector.png new file mode 100644 index 0000000000000000000000000000000000000000..7ce5c37b41f2abb4ccea6eda31d4a427058462b5 GIT binary patch literal 3916 zcmV-S53}%zP)<$i2OGHHu83@HE-O_Hc*Yl1X=-f4MO)H3JT4hkIa^6CUfS0h&3cD7F5CmJTPI$N_X}FpEC0RDJzzGdSdM90=Wo47(7JZ-ux(l_qc~>$#2a02 zz8}!{4x3IUav&(Cx%CGTV!vDHr-#itLyI>|xBlQ(-ZAvorwPbGvFj86^BVizL2JL+ zb&C-Tw~gguGhhK|p+7xll95B%UijC$f!1r+sgfbf$VoANXw_@xy2WxAud#m}H?;7j znH4H_{la*m-Rf!6Vg(B~C&d_{^-qu1Sqa5nXz972qyLKN-+`Z=I4h+vGo~0<@OtB) zbZE8mn{Ba@3J(N(HnkbVmv;s^68) z>`_mMQ9=`#EzV%kw?7lw$e?Q&Gxu~WuXw8AFbyn8-PT3~jWIjE7JK9M^XK>Pzuj2n zHZ<3G)XkAVYX~zpt&?y3zjmWV?BS|{VA~S`b=lCBYuU`+{{H-3U0#V)uY)!eU_WS2 zie~xH!2HbXC+_N%-e;?;Kx5lG+-BL(zhyJqjf;Av|Eqomb}RO`ddbjFOkgnG)d$KY zFj;c*Vx>%I_aE8)YfiQ+gI`tAvX@d`Kci#N&_;Gz$!1;_LtiC;LBWkptu!T|dNh;S zUUr*VuX>S9-DovSg08F>z@`VVzht+yx1cx6fqntjmH8^<>v!m-KtBO`HCWEFUsVI- z>|&(^=;=)5sxy9oPMfx|!B`L)X`E9PSfd8JtswNZOfgjU>jX0Z@V-gCAT%t{D0Ed} zK+hF{7TGe$?6WjO&#vrUh_PM(S_K4}&k;>qm6hyw&WildfB>?3i+U6`ovnh|+j2vT zPqKSb*-o~6y1y8J%;khuXR^0Rydmi3T3{@CqdOloAbdxAwadAC)_% zb;<2h@eE@3X|!AtG_tHPzOUB+r+<3PbdpxG&su#dMubl}q*I`gWyR~i^=A6neg+OZ zJXlFqw8uayI7@*B_8eZBt)!pTf9$6*grHS+W%uX)T24Tn zs^}5YEdC||gjRtKX*>+$XWdT*X=dEpY^MHr1~PS9nj>5st+yXm!SBpmZePl{FA}C?GNPPVDr{Y5`W$tJu5zxkGvu z7zm+w#uZhu<2~h2B~uC51D+JlChb7LK;Vtkz8)Jp{=%CDy@E9~-=2ULPYg_=Wa!6W zRkQAJXt5h?Xk|U21z|HzBMH670Gs;UaA3Rqd7J`aS3w*JA#Z;=hsD%{cQ3Fwc}BmagN3@vV=4IPMd7g~VR zq^C9{pwVw-_V$6$YSm^Gk?ulQ9r(Gn46VV>8SO#P?k}*1#;k1m*LR{V<_$#|x`GNd z5j3IZ_o`A2!5by_{pw<)9PtX%O5 zBb7P1mv$3+N!g~SpsN(YR9aHSHHQXm$k5Xi(K1>|XmN+~v?E)qNc9HOr36iCE50QO zT~jFaHe_?u=N1uqNrGPFq6)LoB z=^}n|LxKjirLJuKU4m|MDs)4JzD*LY$Y32nFVb3zFG} z&^);n*+9RC3>~hCQ}l^C)Yl?tSX)Z1#6euc7Zw?MNrEPRv!}0x(CSP**GwBi|A!16 zF60q3Rl1yZ2rcedIUej7v&l1FH2HK^V(QYgC4|-#Vx^XBfnT9R&q>ftC&%35?WcEB zhms}hdGHMHIEmeOXCF_UF_p7TUkSKMmC6Mf8m-i{c*iriAwla@Xzr=TOrIe~uFU4J z$Jxt5>-de!c0m0YwRWfFK|dl7f7uLB!(>YdVx1x_)cGl&`JroMX8KW91R5OuR?9bx zM;lS4RRo$f78!j-WBcT|ap-Yh2lokI#s&yIhbrG$+t_Wgbg+5ZeHMdW3f)J;iK-wp zKgj6a!{^D?d>=EgMHyX_Ykp`p$8r=pv~LX4yTdjConu8^$OFBXer}>hl+FhPZIuko z#~~hNL$8bjdO?#-|HTI)eahqXDX*a=24dkUui)Z>ZQ?s;($zAcy~Rb#v5ZTeXBhN+ zw1E!$MQKak5IL4{oeBDeX;y-U_8$Ci2`$(WMWB_BJk2G?)>MLq%#VR88|&Vd?BPT1 zXiwQ=Q!ZHvXh_hIttQZfW4qsi3{6%7_W3%ZN>;a+1P!&675O2F+i_2IA|2`KR;bug zdx)&qb~M@Ow4%6T>z{~^*o4qEbcEf)_ZBcA@W_NPRm4~$0Ao$p4i+|RA!AKFGS-lX zqHGqM0?cBKpnyJ_BIDJP4aOIeKr^FYe z?+q3>*r?9CXK>L>vrvvveWb@1cK3)jT0IlT7gx#i-8c@4lmp#A4#l76KPXa`#&aac z8?CN$X(-ZV9LaUO-6>5S*tH;^4PIZ5H{UCO8rdi z9u_&oCVWL`$IR`NzXufK6HnB#&&B|;^wXIxT#>v7KR0AcVE8QQK!1CWlM2O0C3Ge1 zRQ$N#=W^R|wq6nm*V{hNeoSa8r_}>Z@doN>i9_OS?2`}Nq_1VP!wKCGJI@jf2;MX_ zVs{|EUr&WmyOI3mb_a4=eH^a@p{X++ubj-yWTW?l@*Aoe6hal!hL$+oo)e(d35adEGXR0dAX=quUGzSw~Gp zb9WrGlMb@@flC;9*4KY}rZiXN6m&Pf-4cl$B_+5b!@)$EPT$Fhfho5Wlj&(oxtzY0 zQ)6(i=gBaKZg!!Z?)7|_d;OIA1pW5mfeeX(sMc1ZxAMG!4Brfm(1%qRD;aT@nyLfU zcd93Al<$Wuh<3oRp@#>Y?Sg}I640rS<@4SD>uPcKCBJeB8jjgGs||;)pkYv3Qi&6i z09HHvzl-{paV9kTNFu!fI>Yvcn5Lm&8^~-DT1&=1`_8P3i{7O6dmNG-_OcZC2nU>H z#v`KUCOZ`-m^0=Lg4vpvk9p?zZuqU3&lJyT8v&gUx0iatHs^K=NdzA~hXl)2G{V1JOo z?)lRS{JHeB0!Hf{CU7{HFM)%7s_ax598Hy>7~@ElQ6MSF(|$=w{yCPUB>B_7xV`CL zCUU2L*`bs2npVI>JN=%O676u?G%krgc?+>bJ8fc`F7X6PY1B6(-j^GPyD5z>iGKI{ zK-LORIHzV*G&a z5u{Tz^QKeue!R$jeSMRhxDv=zoLG9XwwxOoRP8$h7(AKK zs&)U-VK zY3#b65!?oK8M|2NRzfZpJ>=`i&4j@{CFMg`uq&*LlW*KOZT469R-v1>!poq_hsG}6 z^6xUXPJI9N!SnkMUu~>%vksX@Cq{Hx7PP)Xa24S;M`16nE&e)}4CDyYG0suffpc`q5Ep{@D^XS}0-WaU*{t zFKA#RY*d<=pat{Y2RTdFp1x%my|WyHkEG0Zld}pYXa^dXS0dE*HGbDMF&lcAor)Mr zk3;v}HXk|&qc`6cp+T0jkoSPDP11de-B;(YXdAk=qfb2S=&wU`08NMLF|*iI=8B)! z3!1{=rl*~;Nh!>qqVAx9i;2>%oiVtF3A%&^UPcbs{X`8aM65)T(7@xUnLHt6eNcXl zp8F&XDh&jdjw5`_L@{I^~tYB ziAgN#xG2NAm-h?W_S|}z+nVa{`yMGpX Date: Mon, 27 Jul 2020 14:36:05 -0500 Subject: [PATCH 07/18] Updating region widget selection, and fixing cancel effect which broke QDragManager --- src/windows/process_effect.py | 40 +++++++++++++++++------------------ src/windows/video_widget.py | 17 ++++++++++----- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index 2e76a87dd2..a2994f78ef 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -195,7 +195,7 @@ def __init__(self, clip_id, effect_name, effect_params): # flag to close the clip processing thread self.cancel_clip_processing = False self.effect = None - + def spinner_value_changed(self, widget, param, value): """Spinner value change callback""" self.context[param["setting"]] = value @@ -250,20 +250,21 @@ def rect_select_clicked(self, widget, param): viewPortSize = win.viewport_rect # Get QImage of region - region_qimage = win.videoPreview.region_qimage + if win.videoPreview.region_qimage: + region_qimage = win.videoPreview.region_qimage - # Resize QImage to match button size - resized_qimage = region_qimage.scaled(widget.size(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) + # Resize QImage to match button size + resized_qimage = region_qimage.scaled(widget.size(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) - # Draw Qimage onto QPushButton (to display region selection to user) - palette = widget.palette() - palette.setBrush(widget.backgroundRole(), QBrush(resized_qimage)) - widget.setFlat(True) - widget.setAutoFillBackground(True) - widget.setPalette(palette) + # Draw Qimage onto QPushButton (to display region selection to user) + palette = widget.palette() + palette.setBrush(widget.backgroundRole(), QBrush(resized_qimage)) + widget.setFlat(True) + widget.setAutoFillBackground(True) + widget.setPalette(palette) - # Remove button text (so region QImage is more visible) - widget.setText("") + # Remove button text (so region QImage is more visible) + widget.setText("") # If data found, add to context if topLeft and bottomRight: @@ -326,18 +327,17 @@ def accept(self): # if the cancel button was pressed, close the processing thread if(self.cancel_clip_processing): processing.CancelProcessing() + break if(not self.cancel_clip_processing): - + # Load processed data into effect self.effect = openshot.EffectInfo().CreateEffect(self.effect_name) self.effect.SetJson( '{"protobuf_data_path": "%s"}' % protobufPath ) self.effect.Id(ID) - - print("Applied effect: %s to clip: %s" % (self.effect_name, self.clip_instance.Id())) - # Accept dialog - super(ProcessEffect, self).accept() + # Accept dialog + super(ProcessEffect, self).accept() def reject(self): # Cancel dialog @@ -364,7 +364,7 @@ def generateJson(self, protobufPath): trackerType = self.context["tracker-type"] jsonString += ',"tracker_type": "%s"' % trackerType - # Get bounding box coordinates + # Get bounding box coordinates tracker_dict = self.context["region"] bbox = (tracker_dict["x"],tracker_dict["y"],tracker_dict["width"],tracker_dict["height"]) jsonString += ',"bbox": {"x": %d, "y": %d, "w": %d, "h": %d}' % (bbox) @@ -380,7 +380,7 @@ def generateJson(self, protobufPath): modelConfigPath = self.context["model-config"] jsonString += ', "model_configuration": "%s"' % modelConfigPath - + classNamesPath = self.context["class-names"] jsonString += ', "classes_file": "%s"' % classNamesPath @@ -389,4 +389,4 @@ def generateJson(self, protobufPath): # Finish JSON string jsonString+='}' - return jsonString \ No newline at end of file + return jsonString diff --git a/src/windows/video_widget.py b/src/windows/video_widget.py index c8705027b0..615941deaf 100644 --- a/src/windows/video_widget.py +++ b/src/windows/video_widget.py @@ -367,9 +367,6 @@ def mouseReleaseEvent(self, event): # Save region image data (as QImage) # This can be used other widgets to display the selected region if self.region_enabled: - # Find centered viewport - viewport_rect = self.centeredViewport(self.width(), self.height()) - # Get region coordinates region_rect = QRectF(self.regionTopLeftHandle.x(), self.regionTopLeftHandle.y(), self.regionBottomRightHandle.x() - self.regionTopLeftHandle.x(), @@ -378,9 +375,19 @@ def mouseReleaseEvent(self, event): # Map region (due to zooming) mapped_region_rect = self.region_transform.mapToPolygon(region_rect.toRect()).boundingRect() - # Create QImage from videoPreview widget - self.region_qimage = QImage(mapped_region_rect.width(), mapped_region_rect.height(), QImage.Format_ARGB32) + # Render a scaled version of the region (as a QImage) + # TODO: Grab higher quality pixmap from the QWidget, as this method seems to be 1/2 resolution + # of the original QWidget video element. + scale = 3.0 + + # Map rect to transform (for scaling video elements) + mapped_region_rect = QRect(mapped_region_rect.x(), mapped_region_rect.y(), mapped_region_rect.width() * scale, mapped_region_rect.height() * scale) + + # Render QWidget onto scaled QImage + self.region_qimage = QImage(mapped_region_rect.width(), mapped_region_rect.height(), QImage.Format_RGBA8888) region_painter = QPainter(self.region_qimage) + region_painter.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing, True) + region_painter.scale(scale, scale) self.render(region_painter, QPoint(0,0), QRegion(mapped_region_rect, QRegion.Rectangle)) region_painter.end() From 612b1ea452fac475c7424be459d5f5126791af3f Mon Sep 17 00:00:00 2001 From: Brenno Date: Tue, 28 Jul 2020 21:00:40 -0300 Subject: [PATCH 08/18] Fixed objectDetection wrong file path --- src/windows/process_effect.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index a2994f78ef..a3218e3061 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -195,7 +195,7 @@ def __init__(self, clip_id, effect_name, effect_params): # flag to close the clip processing thread self.cancel_clip_processing = False self.effect = None - + def spinner_value_changed(self, widget, param, value): """Spinner value change callback""" self.context[param["setting"]] = value @@ -327,15 +327,14 @@ def accept(self): # if the cancel button was pressed, close the processing thread if(self.cancel_clip_processing): processing.CancelProcessing() - break if(not self.cancel_clip_processing): - + # Load processed data into effect self.effect = openshot.EffectInfo().CreateEffect(self.effect_name) self.effect.SetJson( '{"protobuf_data_path": "%s"}' % protobufPath ) self.effect.Id(ID) - + # Accept dialog super(ProcessEffect, self).accept() @@ -364,7 +363,7 @@ def generateJson(self, protobufPath): trackerType = self.context["tracker-type"] jsonString += ',"tracker_type": "%s"' % trackerType - # Get bounding box coordinates + # Get bounding box coordinates tracker_dict = self.context["region"] bbox = (tracker_dict["x"],tracker_dict["y"],tracker_dict["width"],tracker_dict["height"]) jsonString += ',"bbox": {"x": %d, "y": %d, "w": %d, "h": %d}' % (bbox) @@ -375,18 +374,29 @@ def generateJson(self, protobufPath): # Special case where more info is needed for the JSON string if self.effect_name == "Object Detector": + + # Get model weights path modelweightsPath = self.context["model-weights"] + if not os.path.exists(modelweightsPath): + modelweightsPath = "" jsonString += ', "model_weights": "%s"' % modelweightsPath + # Get model configuration path modelConfigPath = self.context["model-config"] + if not os.path.exists(modelConfigPath): + modelConfigPath = "" jsonString += ', "model_configuration": "%s"' % modelConfigPath - + + # Get class names file path classNamesPath = self.context["class-names"] + if not os.path.exists(classNamesPath): + classNamesPath = "" jsonString += ', "classes_file": "%s"' % classNamesPath + # Get processing device processingDevice = self.context["processing-device"] jsonString += ', "processing_device": "%s"' % processingDevice # Finish JSON string jsonString+='}' - return jsonString + return jsonString \ No newline at end of file From d5e9786ab57c98bbd60ba8796ea1a5074f354995 Mon Sep 17 00:00:00 2001 From: Brenno Date: Wed, 29 Jul 2020 19:59:13 -0300 Subject: [PATCH 09/18] fixed bug with effects when cutting a clip --- src/windows/views/timeline_webview.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index 84318fd553..9a4f3cf215 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -1915,6 +1915,11 @@ def Slice_Triggered(self, action, clip_ids, trans_ids, playhead_position=0): right_clip.data.pop('id') right_clip.key.pop(1) + for clip_propertie_name, propertie_value in right_clip.data.items() : + if clip_propertie_name == "effects": + for item in propertie_value: + item['id'] = get_app().project.generate_id() + # Set new 'start' of right_clip (need to bump 1 frame duration more, so we don't repeat a frame) right_clip.data["position"] = (round(float(playhead_position) * fps_float) + 1) / fps_float right_clip.data["start"] = (round(float(clip.data["end"]) * fps_float) + 2) / fps_float From 38411b0ac0a0664f280eca9fb85ae14a3e0b19a6 Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 1 Aug 2020 14:16:22 -0300 Subject: [PATCH 10/18] Error handler for OpenCV effect not compiled with library --- src/classes/effect_init.py | 8 ++------ src/windows/process_effect.py | 18 +++++++----------- src/windows/views/timeline_webview.py | 19 ++++++++++++++----- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/classes/effect_init.py b/src/classes/effect_init.py index d12b880cfa..8add54fc59 100644 --- a/src/classes/effect_init.py +++ b/src/classes/effect_init.py @@ -30,8 +30,8 @@ # solution to providing the pre-processing params needed for these special effects. effect_options = { - # TODO: Remove Bars example options - "Bars": [ + # TODO: Remove Example example options + "Example": [ { "title": "Region", "setting": "region", @@ -124,10 +124,6 @@ "value": "MEDIANFLOW", "name": "MEDIANFLOW" }, - { - "value": "GOTURN", - "name": "GOTURN" - }, { "value": "MOSSE", "name": "MOSSE" diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index a3218e3061..47ab8d33a4 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -51,6 +51,9 @@ class ProcessEffect(QDialog): ui_path = os.path.join(info.PATH, 'windows', 'ui', 'process-effect.ui') def __init__(self, clip_id, effect_name, effect_params): + + if not openshot.Clip().COMPILED_WITH_CV: + raise ModuleNotFoundError("Openshot not compiled with OpenCV") # Create dialog class QDialog.__init__(self) @@ -59,6 +62,7 @@ def __init__(self, clip_id, effect_name, effect_params): self.effect_name = effect_name self.context = {} + # Access C++ timeline and find the Clip instance which this effect should be applied to timeline_instance = get_app().window.timeline_sync.timeline for clip_instance in timeline_instance.Clips(): if clip_instance.Id() == self.clip_id: @@ -195,7 +199,7 @@ def __init__(self, clip_id, effect_name, effect_params): # flag to close the clip processing thread self.cancel_clip_processing = False self.effect = None - + def spinner_value_changed(self, widget, param, value): """Spinner value change callback""" self.context[param["setting"]] = value @@ -293,19 +297,11 @@ def accept(self): # Print effect settings log.info(self.context) - # DO WORK HERE, and periodically set progressBar value - # Access C++ timeline and find the Clip instance which this effect should be applied to - # Create effect Id and protobuf data path ID = get_app().project.generate_id() - protobufFolderPath = os.path.join(info.PATH, '..', 'protobuf_data') - # Check if protobuf data folder exists, otherwise it will create one - if not os.path.exists(protobufFolderPath): - os.mkdir(protobufFolderPath) - # Create protobuf data path - protobufPath = os.path.join(protobufFolderPath, ID + '.data') + protobufPath = os.path.join(info.PROTOBUF_DATA_PATH, ID + '.data') # Load into JSON string info abou protobuf data path jsonString = self.generateJson(protobufPath) @@ -359,7 +355,7 @@ def generateJson(self, protobufPath): if self.effect_name == "Tracker": # Set tracker info in JSON string - # Get selected tracker [KCF, MIL, TLD, BOOSTING, MEDIANFLOW, GOTURN, MOOSE, CSRT] + # Get selected tracker [KCF, MIL, TLD, BOOSTING, MEDIANFLOW, MOOSE, CSRT] trackerType = self.context["tracker-type"] jsonString += ',"tracker_type": "%s"' % trackerType diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index 9a4f3cf215..f3c023195e 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -1915,6 +1915,7 @@ def Slice_Triggered(self, action, clip_ids, trans_ids, playhead_position=0): right_clip.data.pop('id') right_clip.key.pop(1) + # Generate new ID to effects on the right (so they become new ones) for clip_propertie_name, propertie_value in right_clip.data.items() : if clip_propertie_name == "effects": for item in propertie_value: @@ -2962,18 +2963,25 @@ def addEffect(self, effect_names, position): for clip in possible_clips: if js_position == 0 or (clip.data["position"] <= js_position <= clip.data["position"] + (clip.data["end"] - clip.data["start"])): log.info("Applying effect to clip") - log.info(clip) - print(name) - print(effect_options) # Handle custom effect dialogs if name in effect_options: - + # Get effect options effect_params = effect_options.get(name) # Show effect pre-processing window from windows.process_effect import ProcessEffect - win = ProcessEffect(clip.id, name, effect_params) + + try: + win = ProcessEffect(clip.id, name, effect_params) + + except ModuleNotFoundError as e: + print("[ERROR]: " + str(e)) + return + + print("Effect %s" % name) + print("Effect options: %s" % effect_options) + # Run the dialog event loop - blocking interaction on this window during this time result = win.exec_() @@ -2985,6 +2993,7 @@ def addEffect(self, effect_names, position): # Create Effect effect = win.effect # effect.Id already set + if effect is None: break else: From 6ade04974226e9cf5a2c99eb4014337a8cc8da33 Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 1 Aug 2020 14:45:42 -0300 Subject: [PATCH 11/18] minor fixes --- src/classes/info.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/classes/info.py b/src/classes/info.py index 27b140d3f5..bc526f489b 100644 --- a/src/classes/info.py +++ b/src/classes/info.py @@ -61,6 +61,7 @@ USER_PROFILES_PATH = os.path.join(USER_PATH, "profiles") USER_PRESETS_PATH = os.path.join(USER_PATH, "presets") USER_TITLES_PATH = os.path.join(USER_PATH, "title_templates") +PROTOBUF_DATA_PATH = os.path.join(USER_PATH, "protobuf_data") # User files BACKUP_FILE = os.path.join(BACKUP_PATH, "backup.osp") USER_DEFAULT_PROJECT = os.path.join(USER_PATH, "default.project") @@ -70,7 +71,8 @@ for folder in [ USER_PATH, BACKUP_PATH, RECOVERY_PATH, THUMBNAIL_PATH, CACHE_PATH, BLENDER_PATH, TITLE_PATH, TRANSITIONS_PATH, PREVIEW_CACHE_PATH, - USER_PROFILES_PATH, USER_PRESETS_PATH, USER_TITLES_PATH, EMOJIS_PATH ]: + USER_PROFILES_PATH, USER_PRESETS_PATH, USER_TITLES_PATH, EMOJIS_PATH, + PROTOBUF_DATA_PATH ]: if not os.path.exists(os.fsencode(folder)): os.makedirs(folder, exist_ok=True) From 1ee194d620e5396c7bfa03b0747f4c0a09f297f6 Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 8 Aug 2020 15:25:13 -0300 Subject: [PATCH 12/18] Fixed effects bug on cut clips --- src/windows/views/timeline_webview.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index f3c023195e..9a4f3cf215 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -1915,7 +1915,6 @@ def Slice_Triggered(self, action, clip_ids, trans_ids, playhead_position=0): right_clip.data.pop('id') right_clip.key.pop(1) - # Generate new ID to effects on the right (so they become new ones) for clip_propertie_name, propertie_value in right_clip.data.items() : if clip_propertie_name == "effects": for item in propertie_value: @@ -2963,25 +2962,18 @@ def addEffect(self, effect_names, position): for clip in possible_clips: if js_position == 0 or (clip.data["position"] <= js_position <= clip.data["position"] + (clip.data["end"] - clip.data["start"])): log.info("Applying effect to clip") + log.info(clip) + print(name) + print(effect_options) # Handle custom effect dialogs if name in effect_options: - + # Get effect options effect_params = effect_options.get(name) # Show effect pre-processing window from windows.process_effect import ProcessEffect - - try: - win = ProcessEffect(clip.id, name, effect_params) - - except ModuleNotFoundError as e: - print("[ERROR]: " + str(e)) - return - - print("Effect %s" % name) - print("Effect options: %s" % effect_options) - + win = ProcessEffect(clip.id, name, effect_params) # Run the dialog event loop - blocking interaction on this window during this time result = win.exec_() @@ -2993,7 +2985,6 @@ def addEffect(self, effect_names, position): # Create Effect effect = win.effect # effect.Id already set - if effect is None: break else: From cf7157ff289080f2100027194075c36c6d4d2a14 Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 8 Aug 2020 15:26:37 -0300 Subject: [PATCH 13/18] Changed protobuf saving path and check OpenCV compatibility --- src/classes/effect_init.py | 4 ++-- src/windows/process_effect.py | 8 ++++---- src/windows/views/timeline_webview.py | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/classes/effect_init.py b/src/classes/effect_init.py index 8add54fc59..73fdb84141 100644 --- a/src/classes/effect_init.py +++ b/src/classes/effect_init.py @@ -30,8 +30,8 @@ # solution to providing the pre-processing params needed for these special effects. effect_options = { - # TODO: Remove Example example options - "Example": [ + # TODO: Remove Bars example options + "Bars": [ { "title": "Region", "setting": "region", diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index 47ab8d33a4..2f834168fe 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -51,9 +51,6 @@ class ProcessEffect(QDialog): ui_path = os.path.join(info.PATH, 'windows', 'ui', 'process-effect.ui') def __init__(self, clip_id, effect_name, effect_params): - - if not openshot.Clip().COMPILED_WITH_CV: - raise ModuleNotFoundError("Openshot not compiled with OpenCV") # Create dialog class QDialog.__init__(self) @@ -68,6 +65,9 @@ def __init__(self, clip_id, effect_name, effect_params): if clip_instance.Id() == self.clip_id: self.clip_instance = clip_instance break + + if not clip_instance.COMPILED_WITH_CV: + super(ProcessEffect, self).reject() # Load UI from designer & init ui_util.load_ui(self, self.ui_path) @@ -199,7 +199,7 @@ def __init__(self, clip_id, effect_name, effect_params): # flag to close the clip processing thread self.cancel_clip_processing = False self.effect = None - + def spinner_value_changed(self, widget, param, value): """Spinner value change callback""" self.context[param["setting"]] = value diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index 9a4f3cf215..3c9c7452c0 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -1915,6 +1915,7 @@ def Slice_Triggered(self, action, clip_ids, trans_ids, playhead_position=0): right_clip.data.pop('id') right_clip.key.pop(1) + # Generate new ID to effects on the right (so they become new ones) for clip_propertie_name, propertie_value in right_clip.data.items() : if clip_propertie_name == "effects": for item in propertie_value: @@ -2967,7 +2968,7 @@ def addEffect(self, effect_names, position): print(effect_options) # Handle custom effect dialogs if name in effect_options: - + # Get effect options effect_params = effect_options.get(name) From f3c1985b96392ea3d4006b29af14ef5e60715f6a Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 8 Aug 2020 15:28:20 -0300 Subject: [PATCH 14/18] Error handler for OpenCV effect not compiled with library --- src/classes/effect_init.py | 4 +- src/windows/process_effect.py | 295 +++++++++++++------------- src/windows/views/timeline_webview.py | 16 +- 3 files changed, 162 insertions(+), 153 deletions(-) diff --git a/src/classes/effect_init.py b/src/classes/effect_init.py index 73fdb84141..8add54fc59 100644 --- a/src/classes/effect_init.py +++ b/src/classes/effect_init.py @@ -30,8 +30,8 @@ # solution to providing the pre-processing params needed for these special effects. effect_options = { - # TODO: Remove Bars example options - "Bars": [ + # TODO: Remove Example example options + "Example": [ { "title": "Region", "setting": "region", diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index 2f834168fe..45a755039e 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -51,154 +51,155 @@ class ProcessEffect(QDialog): ui_path = os.path.join(info.PATH, 'windows', 'ui', 'process-effect.ui') def __init__(self, clip_id, effect_name, effect_params): + if openshot.Clip().COMPILED_WITH_CV: + + # Create dialog class + QDialog.__init__(self) + # Track effect details + self.clip_id = clip_id + self.effect_name = effect_name + self.context = {} + + # Access C++ timeline and find the Clip instance which this effect should be applied to + timeline_instance = get_app().window.timeline_sync.timeline + for clip_instance in timeline_instance.Clips(): + if clip_instance.Id() == self.clip_id: + self.clip_instance = clip_instance + break + + # Load UI from designer & init + ui_util.load_ui(self, self.ui_path) + ui_util.init_ui(self) + + # Update window title + self.setWindowTitle(self.windowTitle() % self.effect_name) + + # get translations + _ = get_app()._tr + + # Pause playback (to prevent crash since we are fixing to change the timeline's max size) + get_app().window.actionPlay_trigger(None, force="pause") + + # Track metrics + track_metric_screen("process-effect-screen") + + # Loop through options and create widgets + row_count = 0 + for param in effect_params: + + # Create Label + widget = None + label = QLabel() + label.setText(_(param["title"])) + label.setToolTip(_(param["title"])) + + if param["type"] == "spinner": + # create QDoubleSpinBox + widget = QDoubleSpinBox() + widget.setMinimum(float(param["min"])) + widget.setMaximum(float(param["max"])) + widget.setValue(float(param["value"])) + widget.setSingleStep(1.0) + widget.setToolTip(param["title"]) + widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = float(param["value"]) + + if param["type"] == "rect": + # create QPushButton which opens up a display of the clip, with ability to select Rectangle + widget = QPushButton(_("Click to Select")) + widget.setMinimumHeight(80) + widget.setToolTip(param["title"]) + widget.clicked.connect(functools.partial(self.rect_select_clicked, widget, param)) + + # Set initial context + self.context[param["setting"]] = {"button-clicked": False, "x": 0, "y": 0, "width": 0, "height": 0} + + if param["type"] == "spinner-int": + # create QDoubleSpinBox + widget = QSpinBox() + widget.setMinimum(int(param["min"])) + widget.setMaximum(int(param["max"])) + widget.setValue(int(param["value"])) + widget.setSingleStep(1.0) + widget.setToolTip(param["title"]) + widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = int(param["value"]) + + elif param["type"] == "text": + # create QLineEdit + widget = QLineEdit() + widget.setText(_(param["value"])) + widget.textChanged.connect(functools.partial(self.text_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = param["value"] + + elif param["type"] == "bool": + # create spinner + widget = QCheckBox() + if param["value"] == True: + widget.setCheckState(Qt.Checked) + self.context[param["setting"]] = True + else: + widget.setCheckState(Qt.Unchecked) + self.context[param["setting"]] = False + widget.stateChanged.connect(functools.partial(self.bool_value_changed, widget, param)) + + elif param["type"] == "dropdown": + + # create spinner + widget = QComboBox() + + # Get values + value_list = param["values"] + + # Add normal values + box_index = 0 + for value_item in value_list: + k = value_item["name"] + v = value_item["value"] + i = value_item.get("icon", None) + + # add dropdown item + widget.addItem(_(k), v) + + # select dropdown (if default) + if v == param["value"]: + widget.setCurrentIndex(box_index) + + # Set initial context + self.context[param["setting"]] = param["value"] + box_index = box_index + 1 + + widget.currentIndexChanged.connect(functools.partial(self.dropdown_index_changed, widget, param)) + + # Add Label and Widget to the form + if (widget and label): + # Add minimum size + label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Create HBoxLayout for each field + self.scrollAreaWidgetContents.layout().insertRow(row_count - 1, label, widget) + + row_count += 1 + + # Add buttons + self.cancel_button = QPushButton(_('Cancel')) + self.process_button = QPushButton(_('Process Effect')) + self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) + self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) + + # flag to close the clip processing thread + self.cancel_clip_processing = False + self.effect = None - # Create dialog class - QDialog.__init__(self) - # Track effect details - self.clip_id = clip_id - self.effect_name = effect_name - self.context = {} - - # Access C++ timeline and find the Clip instance which this effect should be applied to - timeline_instance = get_app().window.timeline_sync.timeline - for clip_instance in timeline_instance.Clips(): - if clip_instance.Id() == self.clip_id: - self.clip_instance = clip_instance - break - - if not clip_instance.COMPILED_WITH_CV: - super(ProcessEffect, self).reject() - - # Load UI from designer & init - ui_util.load_ui(self, self.ui_path) - ui_util.init_ui(self) - - # Update window title - self.setWindowTitle(self.windowTitle() % self.effect_name) - - # get translations - _ = get_app()._tr - - # Pause playback (to prevent crash since we are fixing to change the timeline's max size) - get_app().window.actionPlay_trigger(None, force="pause") - - # Track metrics - track_metric_screen("process-effect-screen") - - # Loop through options and create widgets - row_count = 0 - for param in effect_params: - - # Create Label - widget = None - label = QLabel() - label.setText(_(param["title"])) - label.setToolTip(_(param["title"])) - - if param["type"] == "spinner": - # create QDoubleSpinBox - widget = QDoubleSpinBox() - widget.setMinimum(float(param["min"])) - widget.setMaximum(float(param["max"])) - widget.setValue(float(param["value"])) - widget.setSingleStep(1.0) - widget.setToolTip(param["title"]) - widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) - - # Set initial context - self.context[param["setting"]] = float(param["value"]) - - if param["type"] == "rect": - # create QPushButton which opens up a display of the clip, with ability to select Rectangle - widget = QPushButton(_("Click to Select")) - widget.setMinimumHeight(80) - widget.setToolTip(param["title"]) - widget.clicked.connect(functools.partial(self.rect_select_clicked, widget, param)) - - # Set initial context - self.context[param["setting"]] = {"button-clicked": False, "x": 0, "y": 0, "width": 0, "height": 0} - - if param["type"] == "spinner-int": - # create QDoubleSpinBox - widget = QSpinBox() - widget.setMinimum(int(param["min"])) - widget.setMaximum(int(param["max"])) - widget.setValue(int(param["value"])) - widget.setSingleStep(1.0) - widget.setToolTip(param["title"]) - widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) - - # Set initial context - self.context[param["setting"]] = int(param["value"]) - - elif param["type"] == "text": - # create QLineEdit - widget = QLineEdit() - widget.setText(_(param["value"])) - widget.textChanged.connect(functools.partial(self.text_value_changed, widget, param)) - - # Set initial context - self.context[param["setting"]] = param["value"] - - elif param["type"] == "bool": - # create spinner - widget = QCheckBox() - if param["value"] == True: - widget.setCheckState(Qt.Checked) - self.context[param["setting"]] = True - else: - widget.setCheckState(Qt.Unchecked) - self.context[param["setting"]] = False - widget.stateChanged.connect(functools.partial(self.bool_value_changed, widget, param)) - - elif param["type"] == "dropdown": - - # create spinner - widget = QComboBox() - - # Get values - value_list = param["values"] - - # Add normal values - box_index = 0 - for value_item in value_list: - k = value_item["name"] - v = value_item["value"] - i = value_item.get("icon", None) - - # add dropdown item - widget.addItem(_(k), v) - - # select dropdown (if default) - if v == param["value"]: - widget.setCurrentIndex(box_index) - - # Set initial context - self.context[param["setting"]] = param["value"] - box_index = box_index + 1 - - widget.currentIndexChanged.connect(functools.partial(self.dropdown_index_changed, widget, param)) - - # Add Label and Widget to the form - if (widget and label): - # Add minimum size - label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - # Create HBoxLayout for each field - self.scrollAreaWidgetContents.layout().insertRow(row_count - 1, label, widget) - - row_count += 1 - - # Add buttons - self.cancel_button = QPushButton(_('Cancel')) - self.process_button = QPushButton(_('Process Effect')) - self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) - self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) - - # flag to close the clip processing thread - self.cancel_clip_processing = False - self.effect = None + else: + raise ModuleNotFoundError("Openshot not compiled with OpenCV") def spinner_value_changed(self, widget, param, value): """Spinner value change callback""" diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index 3c9c7452c0..f3c023195e 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -2963,9 +2963,6 @@ def addEffect(self, effect_names, position): for clip in possible_clips: if js_position == 0 or (clip.data["position"] <= js_position <= clip.data["position"] + (clip.data["end"] - clip.data["start"])): log.info("Applying effect to clip") - log.info(clip) - print(name) - print(effect_options) # Handle custom effect dialogs if name in effect_options: @@ -2974,7 +2971,17 @@ def addEffect(self, effect_names, position): # Show effect pre-processing window from windows.process_effect import ProcessEffect - win = ProcessEffect(clip.id, name, effect_params) + + try: + win = ProcessEffect(clip.id, name, effect_params) + + except ModuleNotFoundError as e: + print("[ERROR]: " + str(e)) + return + + print("Effect %s" % name) + print("Effect options: %s" % effect_options) + # Run the dialog event loop - blocking interaction on this window during this time result = win.exec_() @@ -2986,6 +2993,7 @@ def addEffect(self, effect_names, position): # Create Effect effect = win.effect # effect.Id already set + if effect is None: break else: From 8b34e2648eb8afb243a2ed1459ce37809bd25be4 Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 8 Aug 2020 16:00:08 -0300 Subject: [PATCH 15/18] Branch for merging with new-webengine-suppor --- src/windows/process_effect.py | 297 +++++++++++++++++----------------- 1 file changed, 148 insertions(+), 149 deletions(-) diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index 45a755039e..47ab8d33a4 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -51,156 +51,155 @@ class ProcessEffect(QDialog): ui_path = os.path.join(info.PATH, 'windows', 'ui', 'process-effect.ui') def __init__(self, clip_id, effect_name, effect_params): - if openshot.Clip().COMPILED_WITH_CV: - - # Create dialog class - QDialog.__init__(self) - # Track effect details - self.clip_id = clip_id - self.effect_name = effect_name - self.context = {} - - # Access C++ timeline and find the Clip instance which this effect should be applied to - timeline_instance = get_app().window.timeline_sync.timeline - for clip_instance in timeline_instance.Clips(): - if clip_instance.Id() == self.clip_id: - self.clip_instance = clip_instance - break - - # Load UI from designer & init - ui_util.load_ui(self, self.ui_path) - ui_util.init_ui(self) - - # Update window title - self.setWindowTitle(self.windowTitle() % self.effect_name) - - # get translations - _ = get_app()._tr - - # Pause playback (to prevent crash since we are fixing to change the timeline's max size) - get_app().window.actionPlay_trigger(None, force="pause") - - # Track metrics - track_metric_screen("process-effect-screen") - - # Loop through options and create widgets - row_count = 0 - for param in effect_params: - - # Create Label - widget = None - label = QLabel() - label.setText(_(param["title"])) - label.setToolTip(_(param["title"])) - - if param["type"] == "spinner": - # create QDoubleSpinBox - widget = QDoubleSpinBox() - widget.setMinimum(float(param["min"])) - widget.setMaximum(float(param["max"])) - widget.setValue(float(param["value"])) - widget.setSingleStep(1.0) - widget.setToolTip(param["title"]) - widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) - - # Set initial context - self.context[param["setting"]] = float(param["value"]) - - if param["type"] == "rect": - # create QPushButton which opens up a display of the clip, with ability to select Rectangle - widget = QPushButton(_("Click to Select")) - widget.setMinimumHeight(80) - widget.setToolTip(param["title"]) - widget.clicked.connect(functools.partial(self.rect_select_clicked, widget, param)) - - # Set initial context - self.context[param["setting"]] = {"button-clicked": False, "x": 0, "y": 0, "width": 0, "height": 0} - - if param["type"] == "spinner-int": - # create QDoubleSpinBox - widget = QSpinBox() - widget.setMinimum(int(param["min"])) - widget.setMaximum(int(param["max"])) - widget.setValue(int(param["value"])) - widget.setSingleStep(1.0) - widget.setToolTip(param["title"]) - widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) - - # Set initial context - self.context[param["setting"]] = int(param["value"]) - - elif param["type"] == "text": - # create QLineEdit - widget = QLineEdit() - widget.setText(_(param["value"])) - widget.textChanged.connect(functools.partial(self.text_value_changed, widget, param)) - - # Set initial context - self.context[param["setting"]] = param["value"] - - elif param["type"] == "bool": - # create spinner - widget = QCheckBox() - if param["value"] == True: - widget.setCheckState(Qt.Checked) - self.context[param["setting"]] = True - else: - widget.setCheckState(Qt.Unchecked) - self.context[param["setting"]] = False - widget.stateChanged.connect(functools.partial(self.bool_value_changed, widget, param)) - - elif param["type"] == "dropdown": - - # create spinner - widget = QComboBox() - - # Get values - value_list = param["values"] - - # Add normal values - box_index = 0 - for value_item in value_list: - k = value_item["name"] - v = value_item["value"] - i = value_item.get("icon", None) - - # add dropdown item - widget.addItem(_(k), v) - - # select dropdown (if default) - if v == param["value"]: - widget.setCurrentIndex(box_index) - - # Set initial context - self.context[param["setting"]] = param["value"] - box_index = box_index + 1 - - widget.currentIndexChanged.connect(functools.partial(self.dropdown_index_changed, widget, param)) - - # Add Label and Widget to the form - if (widget and label): - # Add minimum size - label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - # Create HBoxLayout for each field - self.scrollAreaWidgetContents.layout().insertRow(row_count - 1, label, widget) - - row_count += 1 - - # Add buttons - self.cancel_button = QPushButton(_('Cancel')) - self.process_button = QPushButton(_('Process Effect')) - self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) - self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) - - # flag to close the clip processing thread - self.cancel_clip_processing = False - self.effect = None - - else: - raise ModuleNotFoundError("Openshot not compiled with OpenCV") + if not openshot.Clip().COMPILED_WITH_CV: + raise ModuleNotFoundError("Openshot not compiled with OpenCV") + + # Create dialog class + QDialog.__init__(self) + # Track effect details + self.clip_id = clip_id + self.effect_name = effect_name + self.context = {} + + # Access C++ timeline and find the Clip instance which this effect should be applied to + timeline_instance = get_app().window.timeline_sync.timeline + for clip_instance in timeline_instance.Clips(): + if clip_instance.Id() == self.clip_id: + self.clip_instance = clip_instance + break + + # Load UI from designer & init + ui_util.load_ui(self, self.ui_path) + ui_util.init_ui(self) + + # Update window title + self.setWindowTitle(self.windowTitle() % self.effect_name) + + # get translations + _ = get_app()._tr + + # Pause playback (to prevent crash since we are fixing to change the timeline's max size) + get_app().window.actionPlay_trigger(None, force="pause") + + # Track metrics + track_metric_screen("process-effect-screen") + + # Loop through options and create widgets + row_count = 0 + for param in effect_params: + + # Create Label + widget = None + label = QLabel() + label.setText(_(param["title"])) + label.setToolTip(_(param["title"])) + + if param["type"] == "spinner": + # create QDoubleSpinBox + widget = QDoubleSpinBox() + widget.setMinimum(float(param["min"])) + widget.setMaximum(float(param["max"])) + widget.setValue(float(param["value"])) + widget.setSingleStep(1.0) + widget.setToolTip(param["title"]) + widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = float(param["value"]) + + if param["type"] == "rect": + # create QPushButton which opens up a display of the clip, with ability to select Rectangle + widget = QPushButton(_("Click to Select")) + widget.setMinimumHeight(80) + widget.setToolTip(param["title"]) + widget.clicked.connect(functools.partial(self.rect_select_clicked, widget, param)) + + # Set initial context + self.context[param["setting"]] = {"button-clicked": False, "x": 0, "y": 0, "width": 0, "height": 0} + + if param["type"] == "spinner-int": + # create QDoubleSpinBox + widget = QSpinBox() + widget.setMinimum(int(param["min"])) + widget.setMaximum(int(param["max"])) + widget.setValue(int(param["value"])) + widget.setSingleStep(1.0) + widget.setToolTip(param["title"]) + widget.valueChanged.connect(functools.partial(self.spinner_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = int(param["value"]) + + elif param["type"] == "text": + # create QLineEdit + widget = QLineEdit() + widget.setText(_(param["value"])) + widget.textChanged.connect(functools.partial(self.text_value_changed, widget, param)) + + # Set initial context + self.context[param["setting"]] = param["value"] + + elif param["type"] == "bool": + # create spinner + widget = QCheckBox() + if param["value"] == True: + widget.setCheckState(Qt.Checked) + self.context[param["setting"]] = True + else: + widget.setCheckState(Qt.Unchecked) + self.context[param["setting"]] = False + widget.stateChanged.connect(functools.partial(self.bool_value_changed, widget, param)) + + elif param["type"] == "dropdown": + + # create spinner + widget = QComboBox() + + # Get values + value_list = param["values"] + + # Add normal values + box_index = 0 + for value_item in value_list: + k = value_item["name"] + v = value_item["value"] + i = value_item.get("icon", None) + + # add dropdown item + widget.addItem(_(k), v) + + # select dropdown (if default) + if v == param["value"]: + widget.setCurrentIndex(box_index) + + # Set initial context + self.context[param["setting"]] = param["value"] + box_index = box_index + 1 + + widget.currentIndexChanged.connect(functools.partial(self.dropdown_index_changed, widget, param)) + + # Add Label and Widget to the form + if (widget and label): + # Add minimum size + label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Create HBoxLayout for each field + self.scrollAreaWidgetContents.layout().insertRow(row_count - 1, label, widget) + + row_count += 1 + + # Add buttons + self.cancel_button = QPushButton(_('Cancel')) + self.process_button = QPushButton(_('Process Effect')) + self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) + self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) + + # flag to close the clip processing thread + self.cancel_clip_processing = False + self.effect = None + def spinner_value_changed(self, widget, param, value): """Spinner value change callback""" self.context[param["setting"]] = value From be6422b1e1656a24a76349fa51ef3f2d1be711c9 Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 8 Aug 2020 16:08:53 -0300 Subject: [PATCH 16/18] Correction bad path in pre-processing effects --- src/windows/process_effect.py | 4 +++- src/windows/region.py | 26 ++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/windows/process_effect.py b/src/windows/process_effect.py index 47ab8d33a4..a158c18baa 100644 --- a/src/windows/process_effect.py +++ b/src/windows/process_effect.py @@ -302,6 +302,7 @@ def accept(self): # Create protobuf data path protobufPath = os.path.join(info.PROTOBUF_DATA_PATH, ID + '.data') + if os.name == 'nt' : protobufPath = protobufPath.replace("\\", "/") # Load into JSON string info abou protobuf data path jsonString = self.generateJson(protobufPath) @@ -395,4 +396,5 @@ def generateJson(self, protobufPath): # Finish JSON string jsonString+='}' - return jsonString \ No newline at end of file + if os.name == 'nt' : jsonString = jsonString.replace("\\", "/") + return jsonString diff --git a/src/windows/region.py b/src/windows/region.py index 3e8f68f30d..d2e07fbdb2 100644 --- a/src/windows/region.py +++ b/src/windows/region.py @@ -80,10 +80,17 @@ def __init__(self, file=None, clip=None): self.end_image = None self.current_frame = 1 - self.clip = clip + # Create region clip with Reader + self.clip = openshot.Clip(clip.Reader()) + self.clip.Open() - self.clip_position = self.clip.Position() - self.clip.Position(0) + + # Set region clip start and end + self.clip.Start(clip.Start()) + self.clip.End(clip.End()) + self.clip.Id( get_app().project.generate_id() ) + + print("IDS {} {}".format(clip.Id(), self.clip.Id())) # Keep track of file object # self.file = file @@ -100,6 +107,10 @@ def __init__(self, file=None, clip=None): self.channel_layout = c_info.channel_layout #int(file.data['channel_layout']) self.video_length = int(self.clip.Duration() * self.fps) + 1 #int(file.data['video_length']) + # Apply effects to region frames + for effect in clip.Effects(): + self.clip.AddEffect(effect) + # Open video file with Reader log.info(self.clip.Reader()) @@ -233,15 +244,11 @@ def sliderVideo_valueChanged(self, new_frame): def accept(self): """ Ok button clicked """ - self.clip.Position(self.clip_position) - self.shutdownPlayer() super(SelectRegion, self).accept() def shutdownPlayer(self): - self.clip.Position(self.clip_position) - log.info('shutdownPlayer') # Stop playback @@ -253,15 +260,14 @@ def shutdownPlayer(self): self.preview_parent.background.wait(5000) # Close readers - self.r.RemoveClip(self.clip) + self.clip.Close() + # self.r.RemoveClip(self.clip) self.r.Close() # self.clip.Close() self.r.ClearAllCache() def reject(self): - self.clip.Position(self.clip_position) - # Cancel dialog self.shutdownPlayer() super(SelectRegion, self).reject() From 516692621235ce4674334ca2f6d1372b6ca37aa6 Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 8 Aug 2020 16:44:29 -0300 Subject: [PATCH 17/18] Fixed file that was changed after merging with webengine branch --- src/windows/views/timeline_webview.py | 44 ++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/windows/views/timeline_webview.py b/src/windows/views/timeline_webview.py index 9fba9e2d81..7f0ef783ea 100644 --- a/src/windows/views/timeline_webview.py +++ b/src/windows/views/timeline_webview.py @@ -40,7 +40,7 @@ from PyQt5.QtGui import QCursor, QKeySequence, QColor from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWebChannel import QWebChannel -from PyQt5.QtWidgets import QMenu +from PyQt5.QtWidgets import QMenu, QDialog from classes import info, updates from classes import settings @@ -2990,11 +2990,45 @@ def callback(self, effect_names, callback_data): log.info("Applying effect {} to clip ID {}".format(name, clip.id)) log.debug(clip) - # Create Effect - effect = openshot.EffectInfo().CreateEffect(name) + # Handle custom effect dialogs + if name in effect_options: - # Get Effect JSON - effect.Id(get_app().project.generate_id()) + # Get effect options + effect_params = effect_options.get(name) + + # Show effect pre-processing window + from windows.process_effect import ProcessEffect + + try: + win = ProcessEffect(clip.id, name, effect_params) + + except ModuleNotFoundError as e: + print("[ERROR]: " + str(e)) + return + + print("Effect %s" % name) + print("Effect options: %s" % effect_options) + + # Run the dialog event loop - blocking interaction on this window during this time + result = win.exec_() + + if result == QDialog.Accepted: + log.info('Start processing') + else: + log.info('Cancel processing') + return + + # Create Effect + effect = win.effect # effect.Id already set + + if effect is None: + break + else: + # Create Effect + effect = openshot.EffectInfo().CreateEffect(name) + + # Get Effect JSON + effect.Id(get_app().project.generate_id()) effect_json = json.loads(effect.Json()) # Append effect JSON to clip From 49e5cd6333fdd70706298a0b4dfc9caf4ae4f28c Mon Sep 17 00:00:00 2001 From: Brenno Date: Sat, 8 Aug 2020 18:33:58 -0300 Subject: [PATCH 18/18] removed unnecessary file --- src/example-effect-init.patch | 298 ---------------------------------- 1 file changed, 298 deletions(-) delete mode 100644 src/example-effect-init.patch diff --git a/src/example-effect-init.patch b/src/example-effect-init.patch deleted file mode 100644 index 4926ad7255..0000000000 --- a/src/example-effect-init.patch +++ /dev/null @@ -1,298 +0,0 @@ -Index: src/windows/process_effect.py -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- src/windows/process_effect.py (date 1594417794000) -+++ src/windows/process_effect.py (date 1594417794000) -@@ -0,0 +1,115 @@ -+""" -+ @file -+ @brief This file loads the Initialize Effects / Pre-process effects dialog -+ @author Jonathan Thomas -+ -+ @section LICENSE -+ -+ Copyright (c) 2008-2018 OpenShot Studios, LLC -+ (http://www.openshotstudios.com). This file is part of -+ OpenShot Video Editor (http://www.openshot.org), an open-source project -+ dedicated to delivering high quality video editing and animation solutions -+ to the world. -+ -+ OpenShot Video Editor is free software: you can redistribute it and/or modify -+ it under the terms of the GNU General Public License as published by -+ the Free Software Foundation, either version 3 of the License, or -+ (at your option) any later version. -+ -+ OpenShot Video Editor is distributed in the hope that it will be useful, -+ but WITHOUT ANY WARRANTY; without even the implied warranty of -+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ GNU General Public License for more details. -+ -+ You should have received a copy of the GNU General Public License -+ along with OpenShot Library. If not, see . -+ """ -+ -+import os -+import sys -+import time -+ -+from PyQt5.QtCore import * -+from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem -+from PyQt5.QtWidgets import * -+from PyQt5 import uic -+import openshot # Python module for libopenshot (required video editing module installed separately) -+ -+from classes import info, ui_util, settings, qt_types, updates -+from classes.app import get_app -+from classes.logger import log -+from classes.metrics import * -+ -+ -+class ProcessEffect(QDialog): -+ """ Choose Profile Dialog """ -+ progress = pyqtSignal(int) -+ -+ # Path to ui file -+ ui_path = os.path.join(info.PATH, 'windows', 'ui', 'process-effect.ui') -+ -+ def __init__(self, clip_id, effect_name): -+ -+ # Create dialog class -+ QDialog.__init__(self) -+ -+ # Load UI from designer & init -+ ui_util.load_ui(self, self.ui_path) -+ ui_util.init_ui(self) -+ -+ # get translations -+ _ = get_app()._tr -+ -+ # Pause playback (to prevent crash since we are fixing to change the timeline's max size) -+ get_app().window.actionPlay_trigger(None, force="pause") -+ -+ # Track metrics -+ track_metric_screen("process-effect-screen") -+ -+ # Init form -+ self.progressBar.setValue(0) -+ self.txtAdvanced.setText("{}") -+ self.setWindowTitle(_("%s: Initialize Effect") % effect_name) -+ self.clip_id = clip_id -+ self.effect_name = effect_name -+ -+ # Add combo entries -+ self.cboOptions.addItem("Option 1", 1) -+ self.cboOptions.addItem("Option 2", 2) -+ self.cboOptions.addItem("Option 3", 3) -+ -+ # Add buttons -+ self.cancel_button = QPushButton(_('Cancel')) -+ self.process_button = QPushButton(_('Process Effect')) -+ self.buttonBox.addButton(self.process_button, QDialogButtonBox.AcceptRole) -+ self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) -+ -+ def accept(self): -+ """ Start processing effect """ -+ # Disable UI -+ self.cboOptions.setEnabled(False) -+ self.txtAdvanced.setEnabled(False) -+ self.process_button.setEnabled(False) -+ -+ # DO WORK HERE, and periodically set progressBar value -+ # Access C++ timeline and find the Clip instance which this effect should be applied to -+ timeline_instance = get_app().window.timeline_sync.timeline -+ for clip_instance in timeline_instance.Clips(): -+ if clip_instance.Id() == self.clip_id: -+ print("Apply effect: %s to clip: %s" % (self.effect_name, clip_instance.Id())) -+ -+ # EXAMPLE progress updates -+ for value in range(1, 100, 4): -+ self.progressBar.setValue(value) -+ time.sleep(0.25) -+ -+ # Process any queued events -+ QCoreApplication.processEvents() -+ -+ # Accept dialog -+ super(ProcessEffect, self).accept() -+ -+ def reject(self): -+ # Cancel dialog -+ self.exporting = False -+ super(ProcessEffect, self).reject() -Index: src/windows/ui/process-effect.ui -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- src/windows/ui/process-effect.ui (date 1594417794000) -+++ src/windows/ui/process-effect.ui (date 1594417794000) -@@ -0,0 +1,105 @@ -+ -+ -+ Dialog -+ -+ -+ -+ 0 -+ 0 -+ 410 -+ 193 -+ -+ -+ -+ %s: Initialize Effect -+ -+ -+ -+ -+ -+ Progress: -+ -+ -+ -+ -+ -+ -+ 24 -+ -+ -+ -+ -+ -+ -+ Option: -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ Advanced: -+ -+ -+ -+ -+ -+ -+ -+ 0 -+ 75 -+ -+ -+ -+ -+ -+ -+ -+ Qt::Horizontal -+ -+ -+ QDialogButtonBox::NoButton -+ -+ -+ -+ -+ -+ -+ -+ -+ buttonBox -+ accepted() -+ Dialog -+ accept() -+ -+ -+ 248 -+ 254 -+ -+ -+ 157 -+ 274 -+ -+ -+ -+ -+ buttonBox -+ rejected() -+ Dialog -+ reject() -+ -+ -+ 316 -+ 260 -+ -+ -+ 286 -+ 274 -+ -+ -+ -+ -+ -Index: src/windows/views/timeline_webview.py -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== ---- src/windows/views/timeline_webview.py (date 1592265000000) -+++ src/windows/views/timeline_webview.py (date 1594417794000) -@@ -38,7 +38,7 @@ - from PyQt5.QtCore import QFileInfo, pyqtSlot, QUrl, Qt, QCoreApplication, QTimer - from PyQt5.QtGui import QCursor, QKeySequence - from PyQt5.QtWebKitWidgets import QWebView --from PyQt5.QtWidgets import QMenu -+from PyQt5.QtWidgets import QMenu, QDialog - - from classes import info, updates - from classes import settings -@@ -2939,6 +2939,9 @@ - # Add Effect - def addEffect(self, effect_names, position): - log.info("addEffect: %s at %s" % (effect_names, position)) -+ # Translation object -+ _ = get_app()._tr -+ - # Get name of effect - name = effect_names[0] - -@@ -2951,13 +2954,23 @@ - # Loop through clips on the closest layer - possible_clips = Clip.filter(layer=closest_layer) - for clip in possible_clips: -- if js_position == 0 or ( -- clip.data["position"] <= js_position <= clip.data["position"] -- + (clip.data["end"] - clip.data["start"]) -- ): -+ if js_position == 0 or (clip.data["position"] <= js_position <= clip.data["position"] + (clip.data["end"] - clip.data["start"])): - log.info("Applying effect to clip") - log.info(clip) - -+ # Handle custom effect dialogs -+ if name in ["Bars", "Stabilize", "Tracker"]: -+ -+ from windows.process_effect import ProcessEffect -+ win = ProcessEffect(clip.id, name) -+ # Run the dialog event loop - blocking interaction on this window during this time -+ result = win.exec_() -+ if result == QDialog.Accepted: -+ log.info('Start processing') -+ else: -+ log.info('Cancel processing') -+ return -+ - # Create Effect - effect = openshot.EffectInfo().CreateEffect(name) - -@@ -2970,6 +2983,7 @@ - - # Update clip data for project - self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True) -+ break - - # Without defining this method, the 'copy' action doesn't show with cursor - def dragMoveEvent(self, event):