Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Outline effect class #982

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open

Conversation

aperture147
Copy link

@aperture147 aperture147 commented Dec 10, 2024

Since OpenShot reader classes don't support stroke (some are intentionally disabled for some reason like ImageMagick's Reader, some are not trivial to implement like Qt's Readers). In my experience, a lot of works require adding stroke to the image/text.

One approach is adding the outline to the image before importing into OpenShot. This is not really handy since the stroke width cannot be keyframed. And adding text requires rasterizing the text into a blank, which is not easy to preview.

To make it easier, I created a rough Outline effect implementation based on OpenCV to solve this problem.

My implementation in general:

  • Convert QImage frame into RGBA cv::Mat
  • Extract alpha channel from the cv::Mat
  • De-antialias by threshold the alpha channel and create an alpha mask base on the de-antialiased channel
  • Create a outline mask by applying GaussianBlur to spread out the alpha mask above base on the input outline width
  • Use Threshold to finish the outline mask
  • Use Canny to get the edge of the outline mask
  • Use GaussianBlur to blur the edge and add it to the outline mask as the antialiased edge
  • Create a blank final image
  • Create a solid color cv::Mat and mask copy it into the final image
  • Mask copy the original cv::Mat image into the final image
  • Convert cv::Mat into QImage and swap it on the original frame

Note: I'm not a skilled C++ programmer and not understand the libopenshot code base thoroughly, so the implementation in opencv might not be as fast as it should. Currently my implementation has two problem:

  • To de-antialiasing the image, I have to remove all pixels that have the value < 255 in the alpha channel, which obviously not a good practice. This method has been disabled in the code since the de-antialias step has to be done in the Frame, not the Effect
  • The outline is pretty jaggy, which requires antialiasing. Partly solved by utilizing canny and gaussian blur.

Please check this out! Thanks for reading

@jonoomph
Copy link
Member

Thanks for sharing! I was able to compile and test this effect, but I'm unclear on exactly it's purpose. Can you add a few images to this PR, which illustrate the before / after of this effect, and describe it's purpose clearly (i.e. when is this useful). Also, I've attached a few changes below, to make this effect available to the OpenShot UI. NOTE: When testing in the UI, an icon needs to be added here: src/effects/icons or you will get an error.

Subject: [PATCH] add effect to openshot ui
---
Index: src/EffectInfo.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/EffectInfo.cpp b/src/EffectInfo.cpp
--- a/src/EffectInfo.cpp	(revision 1107e9ff04a957a413576298b43c3548e9c0fc01)
+++ b/src/EffectInfo.cpp	(date 1734468080766)
@@ -58,6 +58,9 @@
 	else if (effect_type == "Negate")
 		return new Negate();
 
+	else if (effect_type == "Outline")
+		return new Outline();
+
 	else if (effect_type == "Pixelate")
 		return new Pixelate();
 
@@ -129,6 +132,7 @@
 	root.append(Hue().JsonInfo());
 	root.append(Mask().JsonInfo());
 	root.append(Negate().JsonInfo());
+	root.append(Outline().JsonInfo());
 	root.append(Pixelate().JsonInfo());
 	root.append(Saturation().JsonInfo());
 	root.append(Shift().JsonInfo());

@aperture147
Copy link
Author

aperture147 commented Dec 18, 2024

NOTE: Please switch to Chrome if your Safari web player could not play this H265 video. For some reason H265 video cannot be played normally on macOS.

Hi, I've just apply your patch and modify it a bit since this effect requires OpenCV, so it will only be available if OpenCV is linked. Sorry I didn't notice those modifications since I only used libopenshot directly.

The purpose is pretty simple: Adding outline (or stroke) to the edge of non-transparent parts of an transparent image since a Clip does not have the ability to add outline around it. You might have seen a lot of short videos which has the text outlined/shadowed to make it distinctive from the video behind, like this frame cut out of a random video in TikTok:

Screenshot 2024-12-24 at 3 12 41 PM

In my use case, this is my example:

The video with original text:

original.mp4

The video with outlined text, color, outline width are are keyframed:

outlined.mp4

And this is the example Python code:

import openshot
import os
from contextlib import ExitStack

setting = openshot.Settings.Instance()
setting.OMP_THREADS = 16
setting.FF_THREADS = 8
setting.VIDEO_CACHE_PERCENT_AHEAD = 0
setting.VIDEO_CACHE_MIN_PREROLL_FRAMES = 0
setting.VIDEO_CACHE_MAX_PREROLL_FRAMES = 0
setting.VIDEO_CACHE_MAX_FRAMES = 0
setting.ENABLE_PLAYBACK_CACHING = False
setting.HIGH_QUALITY_SCALING = True
setting.DEBUG_TO_STDERR = False

width = 1080
height = 1920
fps = openshot.Fraction(24, 1)
audio_bitrate = 44100
audio_channels = 2
audio_channel_layout = openshot.LAYOUT_STEREO
pixel_ratio = openshot.Fraction(1, 1)

location = os.path.dirname(__file__)

with ExitStack() as stack:

    timeline = openshot.Timeline(width, height, fps, audio_bitrate, audio_channels, audio_channel_layout)

    r = openshot.FFmpegReader("background.mp4")
    r.Open()
    stack.callback(r.Close)

    c = openshot.Clip(r)
    c.Position(0)
    c.gravity = openshot.GRAVITY_CENTER
    c.Layer(1)
    c.Open()
    stack.callback(c.Close)

    timeline.AddClip(c)

    outline_width = openshot.Keyframe()
    outline_width.AddPoint(1, 15, openshot.BEZIER)
    outline_width.AddPoint(24, 30, openshot.BEZIER)
    outline_width.AddPoint(48, 45, openshot.BEZIER)
    outline_width.AddPoint(96, 30, openshot.BEZIER)
    outline_width.AddPoint(120, 15, openshot.BEZIER)
    color = openshot.Keyframe()
    color.AddPoint(1, 0, openshot.BEZIER)
    color.AddPoint(48, 144, openshot.BEZIER)
    color.AddPoint(120, 0, openshot.BEZIER)
    alpha = openshot.Keyframe()
    alpha.AddPoint(1, 255)
    outline_fx = openshot.Outline(outline_width, color, color, color, alpha)
    

    txt = openshot.TextReader(
        width, height,
        0, 0,
        openshot.GRAVITY_CENTER,
        "OpenShot is awesome!",
        "Roboto-Regular.ttf",
        100,
        "white",
        "transparent"
    )

    txt.Open()
    stack.callback(txt.Close)
    

    txt_c = openshot.Clip(txt)
    txt_c.Start(0)
    txt_c.End(20)
    txt_c.Position(0)
    txt_c.AddEffect(outline_fx)
    txt_c.scale = openshot.SCALE_NONE
    txt_c.gravity = openshot.GRAVITY_CENTER
    txt_c.Layer(2)
    txt_c.Open()
    stack.callback(txt_c.Close)

    timeline.AddClip(txt_c)

    timeline.Open()
    stack.callback(timeline.Close)

    w = openshot.FFmpegWriter(os.path.join(location, "example.mp4"))

    w.SetAudioOptions(False, "libvorbis", 44100, 2, openshot.LAYOUT_STEREO, 128000)
    w.SetVideoOptions(True, "libx265", fps, width, height, pixel_ratio, False, False, 3000000)

    w.PrepareStreams()

    w.SetOption(openshot.VIDEO_STREAM, "crf", "28")
    w.SetOption(openshot.VIDEO_STREAM, "preset", "ultrafast")

    w.WriteHeader()
    w.Open()
    stack.callback(w.Close)
    # w.WriteFrame(timeline, 1, math.floor(r.info.duration * float(fps)))
    w.WriteFrame(timeline, 1, 120)
    w.WriteTrailer()

@aperture147
Copy link
Author

@jonoomph Hi Jon, I've added my description and update some more details to it. Please check it out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants