diff --git a/bindings/python/openshot.i b/bindings/python/openshot.i index 2bca526a..e673f3f1 100644 --- a/bindings/python/openshot.i +++ b/bindings/python/openshot.i @@ -114,6 +114,7 @@ #include "effects/Stabilizer.h" #include "effects/Tracker.h" #include "effects/ObjectDetection.h" + #include "effects/Outline.h" #include "TrackedObjectBase.h" #include "TrackedObjectBBox.h" %} @@ -351,4 +352,5 @@ %include "effects/Stabilizer.h" %include "effects/Tracker.h" %include "effects/ObjectDetection.h" + %include "effects/Outline.h" #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d5879d63..6713d5a9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -99,6 +99,7 @@ set(OPENSHOT_CV_SOURCES effects/Stabilizer.cpp effects/Tracker.cpp effects/ObjectDetection.cpp + effects/Outline.cpp ./sort_filter/sort.cpp ./sort_filter/Hungarian.cpp ./sort_filter/KalmanTracker.cpp) diff --git a/src/EffectInfo.cpp b/src/EffectInfo.cpp index 658450b0..94221aed 100644 --- a/src/EffectInfo.cpp +++ b/src/EffectInfo.cpp @@ -98,6 +98,9 @@ EffectBase* EffectInfo::CreateEffect(std::string effect_type) { return new Whisperization(); #ifdef USE_OPENCV + else if (effect_type == "Outline") + return new Outline(); + else if(effect_type == "Stabilizer") return new Stabilizer(); @@ -145,7 +148,8 @@ Json::Value EffectInfo::JsonValue() { root.append(Whisperization().JsonInfo()); #ifdef USE_OPENCV - root.append(Stabilizer().JsonInfo()); + root.append(Outline().JsonInfo()); + root.append(Stabilizer().JsonInfo()); root.append(Tracker().JsonInfo()); root.append(ObjectDetection().JsonInfo()); #endif diff --git a/src/Effects.h b/src/Effects.h index c1005c59..bfc3fcf0 100644 --- a/src/Effects.h +++ b/src/Effects.h @@ -43,6 +43,7 @@ /* OpenCV Effects */ #ifdef USE_OPENCV +#include "effects/Outline.h" #include "effects/ObjectDetection.h" #include "effects/Tracker.h" #include "effects/Stabilizer.h" diff --git a/src/effects/Outline.cpp b/src/effects/Outline.cpp new file mode 100644 index 00000000..80999f89 --- /dev/null +++ b/src/effects/Outline.cpp @@ -0,0 +1,203 @@ +/** + * @file + * @brief Source file for Outline effect class + * @author Jonathan Thomas , HaiVQ + * + * @ref License + */ + +// Copyright (c) 2008-2019 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "Outline.h" +#include "Exceptions.h" + +using namespace openshot; + +/// Blank constructor, useful when using Json to load the effect properties +Outline::Outline() : width(3.0), blue(0.0), green(0.0), red(0.0), alpha(255.0) { + // Init effect properties + init_effect_details(); +} + +// Default constructor +Outline::Outline(Keyframe width, Keyframe blue, Keyframe green, Keyframe red, Keyframe alpha) : + width(width), blue(blue), green(green), red(red), alpha(alpha) +{ + // Init effect properties + init_effect_details(); +} + +// Init effect settings +void Outline::init_effect_details() +{ + /// Initialize the values of the EffectInfo struct. + InitEffectInfo(); + + /// Set the effect info + info.class_name = "Outline"; + info.name = "Outline"; + info.description = "Add outline around the image with transparent background."; + info.has_audio = false; + info.has_video = true; +} + +// This method is required for all derived classes of EffectBase, and returns a +// modified openshot::Frame object +std::shared_ptr Outline::GetFrame(std::shared_ptr frame, int64_t frame_number) +{ + // Get the frame's image + std::shared_ptr frame_image = frame->GetImage(); + + int widthValue = width.GetValue(frame_number); + int blueValue = blue.GetValue(frame_number); + int greenValue = green.GetValue(frame_number); + int redValue = red.GetValue(frame_number); + int alphaValue = alpha.GetValue(frame_number); + + if ((widthValue <= 0) || (alphaValue <= 0)) { + // If alpha or width is zero, return the original frame + return frame; + } + + int sigmaValue = widthValue / 3; + + // Get BGRA image from QImage + cv::Mat cv_image = QImageToBGRACvMat(frame_image); + + // extract alpha channel to create the alpha mask from the image + std::vector channels(4); + cv::split(cv_image, channels); + cv::Mat alpha_mask = channels[3].clone(); + + // Disable de-antialiased + // cv::threshold(alpha_mask, alpha_mask, 254, 255, cv::ThresholdTypes::THRESH_BINARY); // threshold the alpha channel to remove aliased edges + + + // Create the outline mask + cv::Mat outline_mask; + cv::GaussianBlur(alpha_mask, outline_mask, cv::Size(0, 0), sigmaValue, sigmaValue, cv::BorderTypes::BORDER_DEFAULT); + cv::threshold(outline_mask, outline_mask, 0, 255, cv::ThresholdTypes::THRESH_BINARY); + + // Antialias the outline edge + // Apply Canny edge detection to the outline mask + cv::Mat edge_mask; + cv::Canny(outline_mask, edge_mask, 250, 255); + + // Apply Gaussian blur only to the edge mask + cv::Mat blurred_edge_mask; + cv::GaussianBlur(edge_mask, blurred_edge_mask, cv::Size(0, 0), 0.8, 0.8, cv::BorderTypes::BORDER_DEFAULT); + + // Combine the blurred edge mask with the original alpha mask + cv::Mat combined_mask; + cv::bitwise_or(outline_mask, blurred_edge_mask, outline_mask); + + cv::Mat final_image; + + // create solid color source mat + cv::Mat solid_color_mat(cv::Size(cv_image.cols, cv_image.rows), CV_8UC4, cv::Scalar(blueValue, greenValue, redValue, alphaValue)); + + // place outline image first, then place the original image (de-antialiased) on top + solid_color_mat.copyTo(final_image, outline_mask); + cv_image.copyTo(final_image, alpha_mask); + + std::shared_ptr new_frame_image = BGRACvMatToQImage(final_image); + + // FIXME: The shared_ptr::swap does not work somehow + // frame_image.swap(new_frame_image); + *frame_image = *new_frame_image; + + + // return the modified frame + return frame; +} + +cv::Mat Outline::QImageToBGRACvMat(std::shared_ptr& qimage) { + cv::Mat cv_img(qimage->height(), qimage->width(), CV_8UC4, (uchar*)qimage->constBits(), qimage->bytesPerLine()); + return cv_img; +} + +std::shared_ptr Outline::BGRACvMatToQImage(cv::Mat img) { + cv::Mat final_img; + cv::cvtColor(img, final_img, cv::COLOR_RGBA2BGRA); + QImage qimage(final_img.data, final_img.cols, final_img.rows, final_img.step, QImage::Format_ARGB32); + std::shared_ptr imgIn = std::make_shared(qimage.convertToFormat(QImage::Format_RGBA8888_Premultiplied)); + return imgIn; +} + +// Generate JSON string of this object +std::string Outline::Json() const { + + // Return formatted string + return JsonValue().toStyledString(); +} + +// Generate Json::Value for this object +Json::Value Outline::JsonValue() const { + + // Create root json object + Json::Value root = EffectBase::JsonValue(); // get parent properties + root["type"] = info.class_name; + root["width"] = width.JsonValue(); + root["blue"] = blue.JsonValue(); + root["green"] = green.JsonValue(); + root["red"] = red.JsonValue(); + root["alpha"] = alpha.JsonValue(); + + // return JsonValue + return root; +} + +// Load JSON string into this object +void Outline::SetJson(const std::string value) { + + // Parse JSON string into JSON objects + try + { + const Json::Value root = openshot::stringToJson(value); + // Set all values that match + SetJsonValue(root); + } + catch (const std::exception& e) + { + // Error parsing JSON (or missing keys) + throw InvalidJSON("JSON is invalid (missing keys or invalid data types)"); + } +} + +// Load Json::Value into this object +void Outline::SetJsonValue(const Json::Value root) { + + // Set parent data + EffectBase::SetJsonValue(root); + + // Set data from Json (if key is found) + if (!root["width"].isNull()) + width.SetJsonValue(root["width"]); + if (!root["blue"].isNull()) + blue.SetJsonValue(root["blue"]); + if (!root["green"].isNull()) + green.SetJsonValue(root["green"]); + if (!root["red"].isNull()) + red.SetJsonValue(root["red"]); + if (!root["alpha"].isNull()) + alpha.SetJsonValue(root["alpha"]); +} + +// Get all properties for a specific frame +std::string Outline::PropertiesJSON(int64_t requested_frame) const { + + // Generate JSON properties list + Json::Value root = BasePropertiesJSON(requested_frame); + + // Keyframes + root["width"] = add_property_json("Width", width.GetValue(requested_frame), "float", "", &width, 0, 10000, false, requested_frame); + root["blue"] = add_property_json("Blue", blue.GetValue(requested_frame), "float", "", &blue, 0, 255, false, requested_frame); + root["green"] = add_property_json("Green", green.GetValue(requested_frame), "float", "", &green, 0, 255, false, requested_frame); + root["red"] = add_property_json("Red", red.GetValue(requested_frame), "float", "", &red, 0, 255, false, requested_frame); + root["alpha"] = add_property_json("Alpha", alpha.GetValue(requested_frame), "float", "", &alpha, 0, 255, false, requested_frame); + + // Return formatted string + return root.toStyledString(); +} diff --git a/src/effects/Outline.h b/src/effects/Outline.h new file mode 100644 index 00000000..215d957a --- /dev/null +++ b/src/effects/Outline.h @@ -0,0 +1,98 @@ +/** + * @file + * @brief Header file for Outline effect class + * @author Jonathan Thomas , HaiVQ + * + * @ref License + */ + +// Copyright (c) 2008-2019 OpenShot Studios, LLC +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef OPENSHOT_OUTLINE_EFFECT_H +#define OPENSHOT_OUTLINE_EFFECT_H + +#include "../EffectBase.h" + +#include "../Frame.h" +#include "../Json.h" +#include "../KeyFrame.h" + +#include +#include + + +namespace openshot +{ + + /** + * @brief This class add the outline around image with transparent background and can be animated + * with openshot::Keyframe curves over time. + * + * Since outline effect is pretty useful in many cases, this effect is added to libopenshot. + */ + class Outline : public EffectBase + { + private: + /// Init effect settings + void init_effect_details(); + + // Convert QImage to cv::Mat and vice versa + // Although Frame class has GetImageCV, but it does not include alpha channel + // so we need a separate methods which preserve alpha channel + // Idea from: https://stackoverflow.com/a/78480103 + cv::Mat QImageToBGRACvMat(std::shared_ptr& qimage); + std::shared_ptr BGRACvMatToQImage(cv::Mat img); + + public: + Keyframe width; ///< Width of the outline + Keyframe blue; ///< Blue of the outline + Keyframe green; ///< Green of the outline + Keyframe red; ///< Red channel of the outline + Keyframe alpha; ///< Alpha of the outline + + /// Blank constructor, useful when using Json to load the effect properties + Outline(); + + /// Default constructor, which require width, red, green, blue, alpha + /// + /// @param width the width of the outline (between 0 and 1000, rounded to int) + /// @param blue the blue channel of the outline (between 0 and 255, rounded to int) + /// @param green the green channel of the outline (between 0 and 255, rounded to int) + /// @param red the red channel of the outline (between 0 and 255, rounded to int) + /// @param alpha the alpha channel of the outline (between 0 and 255, rounded to int) + Outline(Keyframe width, Keyframe blue, Keyframe green, Keyframe red, Keyframe alpha); + + /// @brief This method is required for all derived classes of ClipBase, and returns a + /// new openshot::Frame object. All Clip keyframes and effects are resolved into + /// pixels. + /// + /// @returns A new openshot::Frame object + /// @param frame_number The frame number (starting at 1) of the clip or effect on the timeline. + std::shared_ptr GetFrame(int64_t frame_number) override { return GetFrame(std::make_shared(), frame_number); } + + /// @brief This method is required for all derived classes of ClipBase, and returns a + /// modified openshot::Frame object + /// + /// The frame object is passed into this method and used as a starting point (pixels and audio). + /// All Clip keyframes and effects are resolved into pixels. + /// + /// @returns The modified openshot::Frame object + /// @param frame The frame object that needs the clip or effect applied to it + /// @param frame_number The frame number (starting at 1) of the clip or effect on the timeline. + std::shared_ptr GetFrame(std::shared_ptr frame, int64_t frame_number) override; + + // Get and Set JSON methods + std::string Json() const override; ///< Generate JSON string of this object + void SetJson(const std::string value) override; ///< Load JSON string into this object + Json::Value JsonValue() const override; ///< Generate Json::Value for this object + void SetJsonValue(const Json::Value root) override; ///< Load Json::Value into this object + + /// Get all properties for a specific frame (perfect for a UI to display the current state + /// of all properties at any time) + std::string PropertiesJSON(int64_t requested_frame) const override; + }; +} + +#endif