首页 > 解决方案 > 合成为 MP4 时 png 周围的moviepy黑色边框

问题描述

将 png 合成到 MP4 视频中会在边缘周围创建黑色边框。

这是使用moviepy 1.0.0

下面的代码复制了带有红色文本 png 的 MP4。

在此处输入图像描述

import numpy as np
import moviepy.editor as mped
def composite_txtpng_on_colour():
    bg_color = mped.ColorClip(size=[400, 300], color=np.array([0, 255, 0]).astype(np.uint8),
                          duration=2).set_position((0, 0))
    text_png_postition = [5, 5]
    text_png = mped.ImageClip("./txtpng.png", duration=3).set_position((text_png_postition))

    canvas_size = bg_color.size
    stacked_clips = mped.CompositeVideoClip([bg_color, text_png], size=canvas_size).set_duration(2)
    stacked_clips.write_videofile('text_with_black_border_video.mp4', fps=24)

composite_txtpng_on_colour()

结果是可以在 VLC 播放器中播放的 MP4。黑边的截图如下所示:-

在此处输入图像描述

任何删除黑色边框的建议将不胜感激。

更新:看起来moviepy 做了一个blit 而不是alpha 合成。

def blit(im1, im2, pos=None, mask=None, ismask=False):
    """ Blit an image over another.  Blits ``im1`` on ``im2`` as position ``pos=(x,y)``, using the
    ``mask`` if provided. If ``im1`` and ``im2`` are mask pictures
    (2D float arrays) then ``ismask`` must be ``True``.
    """
    if pos is None:
        pos = [0, 0]

    # xp1,yp1,xp2,yp2 = blit area on im2
    # x1,y1,x2,y2 = area of im1 to blit on im2
    xp, yp = pos
    x1 = max(0, -xp)
    y1 = max(0, -yp)
    h1, w1 = im1.shape[:2]
    h2, w2 = im2.shape[:2]
    xp2 = min(w2, xp + w1)
    yp2 = min(h2, yp + h1)
    x2 = min(w1, w2 - xp)
    y2 = min(h1, h2 - yp)
    xp1 = max(0, xp)
    yp1 = max(0, yp)

    if (xp1 >= xp2) or (yp1 >= yp2):
        return im2

    blitted = im1[y1:y2, x1:x2]

    new_im2 = +im2

    if mask is None:
        new_im2[yp1:yp2, xp1:xp2] = blitted
    else:
        mask = mask[y1:y2, x1:x2]
        if len(im1.shape) == 3:
            mask = np.dstack(3 * [mask])
        blit_region = new_im2[yp1:yp2, xp1:xp2]
        new_im2[yp1:yp2, xp1:xp2] = (1.0 * mask * blitted + (1.0 - mask) * blit_region)
    
    return new_im2.astype('uint8') if (not ismask) else new_im2

所以,Rotem 是对的。

new_im2[yp1:yp2, xp1:xp2] = (1.0 * mask * blitted + (1.0 - mask) * blit_region)

(alpha * img_rgb + (1.0 - alpha) * bg)

这就是moviepy合成的方式。这就是为什么我们在边缘看到黑色的原因。

标签: python-3.xffmpegmoviepy

解决方案


主要问题是YUV420颜色子采样,但这也是压缩伪影和不完美的“文本红色”图像的结果。

图像缺陷只是在 Alpha 通道中。
文本周围的 alpha(透明度)值不是 255,也不是 0(半透明)像素。

以下代码示例对其进行了更正,并显示了差异(使用 OpenCV):

orig_img = cv2.imread('txtpng.png', cv2.IMREAD_UNCHANGED)
img = orig_img.copy()
img[(img != 255) & (img != 0)] = 255  # Keep only two values: 0 and 255
cv2.imwrite('txtpng2.png', img)  # Write img to txtpng2.png
cv2.imshow('alpha diff', cv2.absdiff(orig_img[:,:,3], img[:,:,3])*100) # Show the difference in alpha channels
cv2.waitKey()
cv2.destroyAllWindows()

上面的代码只保留了两个值: 0 和 255 in img

结果('alpha diff'): 如您所见,存在差异。
在此处输入图像描述


设置编解码器参数:

作为参考,我创建了一个未压缩的 AVI 视频文件:

# Save uncompressed AVI as reference
stacked_clips.write_videofile('text_with_black_border_video.avi', fps=24, codec='rawvideo', ffmpeg_params=['-pix_fmt', 'bgr24'])

我还尝试选择yuv444p像素格式的 H.264 编解码器,但由于某种原因它不起作用。
我选择了 H.265 编解码器。
使用ffmpeg_params,我还设置'-crf', '10'了几乎无损的视频压缩:

stacked_clips.write_videofile('text_with_black_border_video.mp4', fps=24, codec='libx265', ffmpeg_params=['-pix_fmt', 'yuv444p', '-crf', '10'])

这是完整的代码示例:

import numpy as np
import moviepy.editor as mped
import cv2

orig_img = cv2.imread('txtpng.png', cv2.IMREAD_UNCHANGED)
img = orig_img.copy()
img[(img != 255) & (img != 0)] = 255  # Keep only two values: 0 and 255
cv2.imwrite('txtpng2.png', img)

cv2.imshow('img', img)
cv2.imshow('alpha diff', cv2.absdiff(orig_img[:,:,3], img[:,:,3])*100) # Show the difference in alpha channels
cv2.waitKey()
cv2.destroyAllWindows()

def composite_txtpng_on_colour():
    bg_color = mped.ColorClip(size=[400, 300], color=np.array([0, 255, 0]).astype(np.uint8),
                          duration=2).set_position((0, 0))
    text_png_postition = [5, 5]
    text_png = mped.ImageClip('txtpng2.png', duration=3).set_position((text_png_postition))

    canvas_size = bg_color.size
    stacked_clips = mped.CompositeVideoClip([bg_color, text_png], size=canvas_size).set_duration(2)
    stacked_clips.write_videofile('text_with_black_border_video.mp4', fps=24, codec='libx265', ffmpeg_params=['-pix_fmt', 'yuv444p', '-crf', '10'])

    # Save uncompressed AVI as reference
    stacked_clips.write_videofile('text_with_black_border_video.avi', fps=24, codec='rawvideo', ffmpeg_params=['-pix_fmt', 'bgr24'])

composite_txtpng_on_colour()

结果(text_with_black_border_video.mp4):
在此处输入图像描述

参考(未压缩text_with_black_border_video.avi):
在此处输入图像描述

放大部分:
在此处输入图像描述


笔记:

  • 我正在使用moviepy 1.03版

我弄清楚了为什么 H.264 编解码器不适用于像素格式yuv444p

我们可以添加'-report'到列表中ffmpeg_params

stacked_clips.write_videofile('text_with_black_border_video.mp4', fps=24, codec='libx264', ffmpeg_params=['-pix_fmt', 'yuv444p', '-crf', '10', '-report'])

记录的报告以:

... \\lib\\site-packages\\imageio_ffmpeg\\binaries\\ffmpeg-win64-v4.2.2.exe" -y -loglevel error -f rawvideo -vcodec rawvideo -s 400x300 -pix_fmt rgb24 -r 24.00 -an -i - -vcodec libx264 -preset medium -pix_fmt yuv444p -crf 10 -report -pix_fmt yuv420p text_with_black_border_video.mp4

日志显示moviepy 添加了-pix_fmt yuv420p参数。
pix_fmt参数被添加两次:-pix_fmt yuv444p -pix_fmt yuv420p.
论点“yuv420p获胜”。


更新:

保持边缘稍微模糊的方法:

边缘颜色不是背面,而是深绿色。
边缘颜色是RGB 颜色空间中Alpha 合成的结果。
我想合成是使用简单的公式执行的:

dst_img = alpha*img_rgb + (1-alpha)*bg
哪里alpha = img[:,:,0:3]/255

应用上述公式时,我们得到以下图像:

在此处输入图像描述
文本边缘颜色为深绿色。

建议的解决方案:在LAB颜色空间
中应用 alpha 合成。

优势:

与 RGB 和 CMYK 颜色模型不同,CIELAB 旨在接近人类视觉。

LAB色彩空间中的线性运算被人类视觉视为(近似)线性。

这是在 LAB 颜色空间中进行 alpha 合成的代码示例:

img = cv2.imread('txtpng.png', cv2.IMREAD_UNCHANGED)
img2 = np.zeros((300, 400, 4), np.uint8)
img2[(300-img.shape[0])//2:(300+img.shape[0])//2, (400-img.shape[1])//2:(400+img.shape[1])//2, :] = img
alpha = img2[:, :, 3].astype(np.float64)/255  # Convert alpha to range [0, 1]
alpha = np.dstack((alpha, alpha, alpha))  # Duplicate alpha to 3 channels
img2 = img2[:, :, 0:3]  # Only BGR without alpha

bg = np.full_like(img2, (0, 255, 0)) # Green background

bg = cv2.cvtColor(bg, cv2.COLOR_BGR2LAB)
img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2LAB)
composed_img = (img2.astype(np.float64)*alpha + bg.astype(np.float64)*(1-alpha)).astype(np.uint8)
composed_img = cv2.cvtColor(composed_img, cv2.COLOR_LAB2BGR)

cv2.imwrite('composed_img.png', composed_img) # Store the result

结果: 文本边缘颜色看起来更好。
在此处输入图像描述

注意:
我在 LAB 颜色空间中找不到任何关于 alpha 合成的论文(但我看起来并不难)。


推荐阅读