Docker基础知识 (17) - 使用 Docker 部署 Python + OpenCV-Python/Moviepy 编辑图像和视频
Python 是一种由 Guido van Rossum 开发的通用编程语言,它很快就变得非常流行,主要是因为它的简单性和代码可读性。它使程序员能够用更少的代码行表达思想,而不会降低可读性。
Python 可以轻松使用 C/C++ 扩展,这使我们可以在 C/C++ 中编写计算密集型代码,并创建可用作 Python 模块的 Python 包装器。这给我们带来了两个好处:首先,代码与原始 C/C++ 代码一样快(因为它在后台工作的实际 c/C++ 代码),其次,在 Python 中编写代码比使用 C/C++ 更容易。
Python: https://www.python.org/
OpenCV
OpenCV(Open Source Computer Vision)由英特尔公司于 1999 年推出,如今由 Willow Garage 提供支持。它是一个基于 BSD 许可(开源)发行的跨平台计算机视觉库,可以运行在 Linux、Windows、MacOS 操作系统上。
OpenCV 轻量级而且高效,由一系列 C 函数和少量 C++ 类构成,同时提供了 Python、Ruby、MATLAB 等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法,它在 Python 中也被广泛用于计算机视觉。
简言之,通过 OpenCV 可实现计算机图像、视频的编辑。广泛应用于图像识别、运动跟踪、机器视觉等领域。
OpenCV: https://opencv.org/
OpenCV GitHub: https://github.com/opencv
OpenCV-Python
OpenCV-Python 是一个 Python 库/包,旨在解决计算机视觉问题。OpenCV-Python 是原始 OpenCV C/C++ 实现的 Python 库/包。
OpenCV-Python 使用 Numpy,这是一个高度优化的数据库操作库,具有 MATLAB 风格的语法。所有 OpenCV 数组结构都转换为 Numpy 数组。这也使得与使用 Numpy 的其他库(如 SciPy 和 Matplotlib )集成更容易。
OpenCV-Python: https://pypi.org/project/opencv-python
MoviePy
MoviePy 是一个用于视频编辑的 Python 模块,可用于进行视频的基本操作(如剪切、连接、标题插入)、视频合成(也称非线性编辑)、视频处理或创建高级效果。
MoviePy 能处理的视频是 ffmpeg 格式的,支持的文件类型:*.mp4 *.wmv *.rm *.avi *.flv *.webm *.wav *rmvb 。
MoviePy: https://pypi.org/project/moviepy/
Github: https://github.com/Zulko/moviepy
注:Moviepy 是一个 Python 的音视频剪辑库,OpenCV 是一个图形处理库,视频的一帧就是一幅图像,因此在处理视频时可以结合 OpenCV 进行帧处理,将二者结合可以用来进行视频特效的处理。
1. 部署环境
IP 地址(本地测试环境):192.168.0.10
操作系统:Linux CentOS 7.9
Docker 版本: 20.10.7
Docker Compose 版本: 2.6.1
OpenCV-Python 版本:3.4.8.29
工作目录:/home/docker/python_media
2. 创建 Dockerfile
$ cd /home/docker/python_media
$ vim Dockerfile
FROM python:3.8 WORKDIR /home/docker/python_media/work #COPY . /home/docker/python_media/work RUN pip install -i https://mirrors.aliyun.com/pypi/simple opencv-python==3.4.8.29 RUN pip install -i https://mirrors.aliyun.com/pypi/simple moviepy==1.0.3
3. 创建 python_media 镜像
$ cd /home/docker/python_media
$ docker build -t python_media:3.8 .
Sending build context to Docker daemon 9.216kB Step 1/4 : FROM python:3.8 ---> 5e51aed29a27 Step 2/4 : WORKDIR /home/docker/python_media ---> Running in bfa5952eec12 Removing intermediate container bfa5952eec12 ---> 536afc6d0b85 Step 3/4 : RUN pip install -i https://mirrors.aliyun.com/pypi/simple opencv-python==3.4.8.29 ---> Running in 4238ef77d8e2 Looking in indexes: https://mirrors.aliyun.com/pypi/simple Collecting opencv-python==3.4.8.29 Downloading https://mirrors.aliyun.com/pypi/packages/bd/95/f06beabb5e1fa319f02278004efce19e7ebe7a67e69dd7d1820d64b2dab8/opencv_python-3.4.8.29-cp38-cp38-manylinux1_x86_64.whl (28.3 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 28.3/28.3 MB 261.3 kB/s eta 0:00:00 Collecting numpy>=1.17.3 Downloading https://mirrors.aliyun.com/pypi/packages/56/df/2f6016171ebce9875e7de0292a2131bea86e0340607a313a04b332d35c8e/numpy-1.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.1 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 17.1/17.1 MB 257.3 kB/s eta 0:00:00 Installing collected packages: numpy, opencv-python ... Step 4/4 : RUN pip install -i https://mirrors.aliyun.com/pypi/simple moviepy==1.0.3 ---> Running in 4011b24e7bc4 Looking in indexes: https://mirrors.aliyun.com/pypi/simple Collecting moviepy==1.0.3 Downloading https://mirrors.aliyun.com/pypi/packages/18/54/01a8c4e35c75ca9724d19a7e4de9dc23f0ceb8769102c7de056113af61c3/moviepy-1.0.3.tar.gz (388 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 388.3/388.3 KB 201.0 kB/s eta 0:00:00 Preparing metadata (setup.py): started Preparing metadata (setup.py): finished with status 'done' Collecting decorator<5.0,>=4.0.2 Downloading https://mirrors.aliyun.com/pypi/packages/ed/1b/72a1821152d07cf1d8b6fce298aeb06a7eb90f4d6d41acec9861e7cc6df0/decorator-4.4.2-py2.py3-none-any.whl (9.2 kB) Collecting tqdm<5.0,>=4.11.2 Downloading https://mirrors.aliyun.com/pypi/packages/47/bb/849011636c4da2e44f1253cd927cfb20ada4374d8b3a4e425416e84900cc/tqdm-4.64.1-py2.py3-none-any.whl (78 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 78.5/78.5 KB 200.4 kB/s eta 0:00:00 Collecting requests<3.0,>=2.8.1 Downloading https://mirrors.aliyun.com/pypi/packages/ca/91/6d9b8ccacd0412c08820f72cebaa4f0c0441b5cda699c90f618b6f8a1b42/requests-2.28.1-py3-none-any.whl (62 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.8/62.8 KB 189.6 kB/s eta 0:00:00 Collecting proglog<=1.0.0 Downloading https://mirrors.aliyun.com/pypi/packages/8b/f5/cab5cf6a540c31f5099043de0ae43990fd9cf66f75ecb5e9f254a4e4d4ee/proglog-0.1.10-py3-none-any.whl (6.1 kB) Requirement already satisfied: numpy>=1.17.3 in /usr/local/lib/python3.8/site-packages (from moviepy==1.0.3) (1.23.4) Collecting imageio<3.0,>=2.5 Downloading https://mirrors.aliyun.com/pypi/packages/97/e2/c5bb16905ab91a0fac03f2a4f1579835bcfea3e297f8cf53e4e2b43c270c/imageio-2.22.2-py3-none-any.whl (3.4 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.4/3.4 MB 184.6 kB/s eta 0:00:00 Collecting imageio_ffmpeg>=0.2.0 Downloading https://mirrors.aliyun.com/pypi/packages/e5/3b/fdf3e75462e93b7806ffecad6c5aa35f2cc76b9f2faaedf5e43194ceff09/imageio_ffmpeg-0.4.7-py3-none-manylinux2010_x86_64.whl (26.9 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 26.9/26.9 MB 177.0 kB/s eta 0:00:00 Collecting pillow>=8.3.2 Downloading https://mirrors.aliyun.com/pypi/packages/d8/80/ff6b6ae88982f73d050907dc2c307f387f6a04ce2ca7230ef3a568fbccac/Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl (3.2 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.2/3.2 MB 180.3 kB/s eta 0:00:00 Collecting idna<4,>=2.5 Downloading https://mirrors.aliyun.com/pypi/packages/fc/34/3030de6f1370931b9dbb4dad48f6ab1015ab1d32447850b9fc94e60097be/idna-3.4-py3-none-any.whl (61 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.5/61.5 KB 158.4 kB/s eta 0:00:00 Collecting urllib3<1.27,>=1.21.1 Downloading https://mirrors.aliyun.com/pypi/packages/6f/de/5be2e3eed8426f871b170663333a0f627fc2924cc386cd41be065e7ea870/urllib3-1.26.12-py2.py3-none-any.whl (140 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 140.4/140.4 KB 194.1 kB/s eta 0:00:00 Collecting certifi>=2017.4.17 Downloading https://mirrors.aliyun.com/pypi/packages/1d/38/fa96a426e0c0e68aabc68e896584b83ad1eec779265a028e156ce509630e/certifi-2022.9.24-py3-none-any.whl (161 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 161.1/161.1 KB 184.5 kB/s eta 0:00:00 Collecting charset-normalizer<3,>=2 Downloading https://mirrors.aliyun.com/pypi/packages/db/51/a507c856293ab05cdc1db77ff4bc1268ddd39f29e7dc4919aa497f0adbec/charset_normalizer-2.1.1-py3-none-any.whl (39 kB) Building wheels for collected packages: moviepy Building wheel for moviepy (setup.py): started Building wheel for moviepy (setup.py): finished with status 'done' Created wheel for moviepy: filename=moviepy-1.0.3-py3-none-any.whl size=110743 sha256=26a5092d88a9ce5490d3fe9177dfbb78aec86fcd55bb206de0d3d35d47a787de Stored in directory: /root/.cache/pip/wheels/fb/56/20/ea8039f0f19ff16db00131559d9290ff7cddf4711b64a3ff27 Successfully built moviepy Installing collected packages: urllib3, tqdm, pillow, imageio_ffmpeg, idna, decorator, charset-normalizer, certifi, requests, proglog, imageio, moviepy Successfully installed certifi-2022.9.24 charset-normalizer-2.1.1 decorator-4.4.2 idna-3.4 imageio-2.22.2 imageio_ffmpeg-0.4.7 moviepy-1.0.3 pillow-9.2.0 proglog-0.1.10 requests-2.28.1 tqdm-4.64.1 urllib3-1.26.12
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE python_media 3.8 755ea9005f1c 10 minutes ago 1.24GB python 3.8 5e51aed29a27 2 days ago 913MB
4. 创建 docker-compose.yml
$ cd /home/docker/python_media
$ vim docker-compose.yml
version: "3" services: python_media: image: python_media:3.8 container_name: python_media-3.8 restart: always volumes: - /home/docker/python_media/work:/home/docker/python_media/work entrypoint: "sleep infinity"
注:当容器启动后没有 service 运行,但又想继续保持 running 状态,可以添加 entrypoint: "tail -f /dev/null" (即查看空设备的日志,一直会处于阻塞状态),也可以使用 entrypoint: "sleep infinity" (睡眠阻塞)。
5. 启动
$ cd /home/docker/python_media # 进入 docker-compose.yml 所在目录
$ docker-compose up -d # 在后台运行
[+] Running 2/2
⠿ Network python_opencv_default Created 0.0s
⠿ Container python_media-3.8 Started 0.3s
$ docker ps
CONTAINER ID IMAGE COMMAND PORTS NAMES
627ad779b82a python_media:3.8 "sleep infinity" python_media-3.8
6. Python 测试程序
$ cd /home/docker/python_media/work
$ mkdir scripts && cd scripts
$ vim hello.py
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
print("Hello world - 你好!")
$ docker exec -t python_media-3.8 python /home/docker/python_media/work/scripts/hello.py
Hello world - 你好!
7. OpenCV 程序
OpenCV 的几个基础模块:
(1) core 模块: 实现了最核心的数据结构及其基本运算,如绘图函数、数组操作相关函数等;
(2) highgui 模块:实现了视频与图像的读取、显示、存储等接口;
(3) imgproc 模块:实现了图像处理的基础方法,包括图像滤波、图像的几何变换、平滑、阈值分割、形态学处理、边缘检测、目标检测、运动分析和对象跟踪等;
图像处理其它应用的模块:
(1) features2d 模块:用于提取图像特征以及特征匹配;
(2) nonfree 模块:实现了一些专利算法,如 sift 特征;
(3) objdetect 模块:实现了一些目标检测的功能,经典的基于 Haar、LBP 特征的人脸检测,基于 HOG 的行人、汽车等目标检测,分类器使用 Cascade Classification(级联分类)和 Latent SVM 等;
(4) stitching 模块:实现了图像拼接功能;
(5) FLANN(Fast Library for Approximate Nearest Neighbors)模块,包含快速近似最近邻搜索 FLANN 和聚类 Clustering 算法;
(6) ml 模块:机器学习模块(SVM,决策树,Boosting 等);
(7) photo 模块:包含图像修复和图像去噪两部分;
(8) video 模块:针对视频处理,如背景分离,前景检测、对象跟踪等;
(9) calib3d (即 Calibration 3D)模块:主要是相机校准和三维重建相关的内容。包含了基本的多视角几何算法,单个立体摄像头标定,物体姿态估计,立体相似性算法,3D 信息的重建等等;
(10) G-API 模块:包含超高效的图像处理 pipeline 引擎。
这里演示一个把长视频切割成固定时间的短视频的实例。
1) 视频资源
文件名:test.mp4
文件大小:9.52 MB
时长:14.93 秒
视频源文件:/home/docker/python_media/work/res/test.mp4
2) 创建脚本
$ cd /home/docker/python_media/work/scripts
$ vim opencv_split.py
1 #!/usr/bin/python3 2 3 import os, sys, time 4 import os.path as osp 5 6 import cv2 7 8 9 # 默认值 10 save_dir = '/out' 11 default_seconds = 1 12 max_count = 999 13 14 def splitFile(filename, dura, sdir): 15 16 # Check parameters and file path 17 if filename.isspace() == True or dura < default_seconds or sdir.isspace == True: 18 print("Error: invalid parameters") 19 return 20 21 if osp.exists(filename) != True: 22 print(f"Error: \'{filename}\' not exists") 23 return 24 25 if osp.exists(sdir) != True: 26 os.mkdir(sdir) 27 28 # Open video file and get video parameters 29 print(f"Info: opening \'{filename}\' ...") 30 video_cap = cv2.VideoCapture(filename) 31 32 video_fps = int(video_cap.get(cv2.CAP_PROP_FPS)) 33 video_frames = int(video_cap.get(cv2.CAP_PROP_FRAME_COUNT)) 34 35 duration_end = float(format(video_frames/video_fps, '.2f')) 36 if duration_end <= dura: 37 print(f"Error: video duration {duration_end} seconds <= {dura} seconds, unable to split") 38 return 39 40 # Calculate split file count 41 filecount = int(duration_end // dura) 42 if (duration_end % dura) > 1.0: 43 filecount += 1 44 45 if filecount > max_count: 46 print(f"Error: file count {filecount} > {max_count}, unable to split") 47 return 48 49 # 50 print(f"Info: split to {filecount} files ...") 51 52 frame_index = 1 53 file_index = 1 54 55 fileObj = osp.splitext(osp.basename(filename)) 56 output_fullname = "{0}/{1}_{2:03d}{3}".format(sdir, fileObj[0], file_index, fileObj[1]) 57 58 dura_frames = dura * video_fps 59 frame_size = (int(video_cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(video_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))) 60 61 fourcc = cv2.VideoWriter_fourcc('m','p','4','v') 62 video_writer = cv2.VideoWriter(output_fullname, fourcc, video_fps, frame_size) 63 64 print(f"Info: writing \'{output_fullname}\' - {file_index}/{filecount}\n... ") 65 66 while True: 67 68 # Read a frame 69 is_success, bgr_im = video_cap.read() 70 71 # Break when no more frame 72 if not is_success: 73 video_writer.release() 74 video_cap.release() 75 break 76 77 # Write current frame to output file 78 if ((frame_index % dura_frames) != 0): 79 if video_writer.isOpened(): 80 video_writer.write(bgr_im) 81 82 # Final frame of the current output file: 83 # 1. write the final frame , 2. close output file, 3. create next output file 84 if ((frame_index % dura_frames) == 0): 85 if video_writer.isOpened(): 86 video_writer.write(bgr_im) 87 video_writer.release() 88 89 file_index += 1 90 output_fullname = "{0}/{1}_{2:03d}{3}".format(sdir, fileObj[0], file_index, fileObj[1]) 91 video_writer = cv2.VideoWriter(output_fullname, fourcc, video_fps, frame_size) 92 93 print(f"Info: writing \'{output_fullname}\' - {file_index}/{filecount}\n... ") 94 95 frame_index += 1 96 97 video_cap.release() 98 99 100 # 命令行入口 101 if __name__=="__main__": 102 msg = """Usage: python %s [file] [seconds] [sdir] 103 file - Source file, full path 104 seconds - Duration, default 10 seconds 105 sdir - Output dir 106 """ 107 count = len(sys.argv) 108 if count < 2: 109 print(msg % sys.argv[0]) 110 elif count == 2: 111 dura = default_seconds 112 sdir = osp.abspath('.') + save_dir 113 splitFile(sys.argv[1], dura, sdir) 114 elif count == 3: 115 dura = int(sys.argv[2]) 116 if dura < default_seconds: 117 dura = default_seconds 118 sdir = osp.abspath('.') + save_dir 119 splitFile(sys.argv[1], dura, sdir) 120 else: 121 dura = int(sys.argv[2]) 122 if dura < default_seconds: 123 dura = default_seconds 124 sdir = sys.argv[3] 125 if sdir.isspace() == True: 126 sdir = osp.abspath('.') + save_dir 127 splitFile(sys.argv[1], dura, sdir)
3) 运行
$ docker exec -t python_media-3.8 python /home/docker/python_media/work/scripts/opencv_split.py /home/docker/python_media/work/res/test.mp4 5
Info: opening '/home/docker/python_media/work/res/test.mp4' ... Info: split to 3 files ... Info: writing '/home/docker/python_media/work/out/test_001.mp4' - 1/3 ... Info: writing '/home/docker/python_media/work/out/test_002.mp4' - 2/3 ... Info: writing '/home/docker/python_media/work/out/test_003.mp4' - 3/3 ...
分割后的小视频在 /home/docker/python_media/work/out 目录,可以正常播放,但没有音频(声音),test_001.mp4 ~ test_002.mp4 时长都是 5 秒,test_003.mp4 时长 4 秒。
注:以上程序分割的小视频,没有音频(声音),而且生成的文件比源文件还大,这是因为输出文件时没有设置比特率。本文只演示如何分割文件,Opencv 的音频和参数的设置在以后关于 Opencv 的文章里再讲解。简单的文件分割,建议使用 Moviepy 来实现。
8. Moviepy 程序
1) 创建脚本
$ cd /home/docker/python_media/work/scripts
$ vim moviepy_split.py
1 #!/usr/bin/python3 2 3 import os, sys 4 import os.path as osp 5 6 from moviepy import editor 7 8 # 默认值 9 save_dir = '/out2' 10 default_seconds = 1 11 max_count = 999 12 13 # 分割函数 14 def splitFile(filename, dura, sdir): 15 if filename.isspace() == True or dura < default_seconds or sdir.isspace == True: 16 print("Error: invalid parameters") 17 return 18 19 if osp.exists(filename) != True: 20 print(f"Error: \'{filename}\' not exists") 21 return 22 23 if osp.exists(sdir) != True: 24 os.mkdir(sdir) 25 26 print(f"Info: opening \'{filename}\' ...") 27 video_clip = editor.VideoFileClip(filename) 28 duration_end = video_clip.duration 29 if duration_end <= dura: 30 print(f"Error: video duration {video_clip.duration} seconds <= {dura} seconds, unable to split") 31 return 32 33 filecount = int(duration_end // dura) 34 if (duration_end % dura) > 1.0: 35 filecount += 1 36 37 if filecount > max_count: 38 print(f"Error: file count {filecount} > {max_count}, unable to split") 39 return 40 41 print(f"Info: split to {filecount} files ...") 42 # 43 fileObj = osp.splitext(osp.basename(filename)) 44 45 i = 1 46 start = 0 47 end = dura 48 while i <= filecount: 49 50 if start > duration_end: 51 break 52 53 if end > duration_end: 54 end = duration_end 55 56 sub_clip = video_clip.subclip(start, end) 57 sub_clip.write_videofile("{0}/{1}_{2:03d}{3}".format(sdir, fileObj[0], i, fileObj[1])) 58 59 print(f"Info: in progress -> {i} / {filecount}\n") 60 61 start += dura 62 end += dura 63 i += 1 64 65 66 # 命令行入口 67 if __name__=="__main__": 68 msg = """Usage: python %s [file] [seconds] [sdir] 69 file - Source file, full path 70 seconds - Duration, default 10 seconds 71 sdir - Output dir 72 """ 73 count = len(sys.argv) 74 if count < 2: 75 print(msg % sys.argv[0]) 76 elif count == 2: 77 dura = default_seconds 78 sdir = osp.abspath('.') + save_dir 79 splitFile(sys.argv[1], dura, sdir) 80 elif count == 3: 81 dura = int(sys.argv[2]) 82 if dura < default_seconds: 83 dura = default_seconds 84 sdir = osp.abspath('.') + save_dir 85 splitFile(sys.argv[1], dura, sdir) 86 else: 87 dura = int(sys.argv[2]) 88 if dura < default_seconds: 89 dura = default_seconds 90 sdir = sys.argv[3] 91 if sdir.isspace() == True: 92 sdir = osp.abspath('.') + save_dir 93 splitFile(sys.argv[1], dura, sdir)
2) 运行
$ docker exec -t python_media-3.8 python /home/docker/python_media/work/scripts/moviepy_split.py /home/docker/python_media/work/res/test.mp4 5
Info: opening '/home/docker/python_media/work/res/test.mp4' ... Info: split to 3 files ... Moviepy - Building video /home/docker/python_media/work/out2/test_001.mp4. MoviePy - Writing audio in test_001TEMP_MPY_wvf_snd.mp3 MoviePy - Done. Moviepy - Writing video /home/docker/python_media/work/out2/test_001.mp4 Moviepy - Done ! Moviepy - video ready /home/docker/python_media/work/out2/test_001.mp4 Info: in progress -> 1 / 3 Moviepy - Building video /home/docker/python_media/work/out2/test_002.mp4. MoviePy - Writing audio in test_002TEMP_MPY_wvf_snd.mp3 MoviePy - Done. Moviepy - Writing video /home/docker/python_media/work/out2/test_002.mp4 Moviepy - Done ! Moviepy - video ready /home/docker/python_media/work/out2/test_002.mp4 Info: in progress -> 2 / 3 Moviepy - Building video /home/docker/python_media/work/out2/test_003.mp4. MoviePy - Writing audio in test_003TEMP_MPY_wvf_snd.mp3 MoviePy - Done. Moviepy - Writing video /home/docker/python_media/work/out2/test_003.mp4 Moviepy - Done ! Moviepy - video ready /home/docker/python_media/work/out2/test_003.mp4 Info: in progress -> 3 / 3
分割后的小视频在 /home/docker/python_media/work/out2 目录,可以正常播放,音频(声音)正常,test_001.mp4 ~ test_002.mp4 时长都是 5 秒,test_003.mp4 时长 4 秒。