目录

作者: admin

OpenCV-Python 保存视频帧

本文代码实现目的:通过当前电脑摄像头,录制一段视频并进行垂直翻转处理后,保存为处理后的视频文件。

实现

所以我们捕获一个当前计算机的摄像头视频流并逐帧处理它,我们想要保存该视频。对于图像,它非常简单:只需使用cv.imwrite(). 在这里,需要做更多的工作。

这次我们创建了一个VideoWriter对象。我们应该指定输出文件名(例如:output.avi)。然后我们应该指定FourCC代码。然后应传递每秒帧数 (fps) 和帧大小。最后一个是isColor标志。如果是True,编码器期望彩色帧,否则它适用于灰度帧。

FourCC是一个 4 字节的代码,用于指定视频编解码器。可用代码列表可在fourcc.org中找到。它依赖于平台。以下编解码器对我来说很好用。

执行效果(最终会输出一个 output.avi 视频文件,也就是当前代码运行结果所录制并处理过的的视频):

main.py 代码:

import numpy as np
import cv2 as cv

# 从当前电脑中的摄像头读取视频流
cap = cv.VideoCapture(0)

width = int(cap.get(cv.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv.CAP_PROP_FRAME_HEIGHT))

# 定义编解码器并创建 VideoWriter 对象
fourcc = cv.VideoWriter_fourcc(*'XVID')
out = cv.VideoWriter('output.avi', fourcc, 20.0, (width,  height))

# 处理视频
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        print('无法收到视频帧数据(该视频流是否已结束?),程序正在退出')
        break

    # 将每一帧视频进行翻转处理,并写入
    frame = cv.flip(frame, 0)
    out.write(frame)

    cv.imshow('frame', frame)
    if cv.waitKey(1) == ord('q'):
        break

# 程序结束时,释放资源
cap.release()
out.release()
cv.destroyAllWindows()

本示例直接运行以上代码即可(以上即完整代码)

示例参考原文:https://docs.opencv.org/4.5.5/dd/d43/tutorial_py_video_display.html

OpenCV-Python 读取视频文件并播放

从文件播放视频与从相机捕获视频相同,只需将相机索引更改为视频文件名即可。此外,在显示框架时,请使用适当的时间 cv.waitKey() 如果太少,视频会很快,如果太高,视频会很慢(嗯,这就是您可以慢动作显示视频的方式),在正常情况下,25 毫秒就可以了。

代码运行效果:

main.py 代码:

import numpy as np
import cv2 as cv

cap = cv.VideoCapture('vtest.mp4')

while cap.isOpened():
    ret, frame = cap.read()

    # 如果帧读取正确,ret 为 True
    if not ret:
        print('无法收到视频帧数据(该视频流是否已结束?),程序正在退出')
        break
    
    # 转换该视频帧为灰度图像
    gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    # 显示该帧
    cv.imshow('frame', gray)
    # 当按下键盘 q 时,退出程序
    if cv.waitKey(1) == ord('q'):
        break

cap.release()
cv.destroyAllWindows()

示例代码打包下载:

示例原文参考:https://docs.opencv.org/4.5.5/dd/d43/tutorial_py_video_display.html

OpenCV-Python 捕捉摄像头视频帧,并进行展示

通常,我们必须用摄像头捕捉实时流。OpenCV 提供了一个非常简单的接口来执行此操作,让我们从摄像头中捕捉视频(我使用笔记本电脑上的内置网络摄像头),将其转换为灰度视频并显示。只需一个简单的任务即可开始。

要捕获视频,您需要创建一个VideoCapture对象。它的参数可以是设备索引或视频文件的名称。设备索引只是指定哪个相机的数字。通常会连接一台相机(如我的情况)。所以我只是传递0(或-1)。您可以通过传递 1 来选择第二个相机,依此类推。之后,您可以逐帧捕获。但最后,不要忘记释放捕获。

本示例运行效果:

mian.py 代码:

import numpy as np
import cv2 as cv

cap = cv.VideoCapture(0)
if not cap.isOpened():
    print('无法打开该摄像头')
    exit()

while True:
    # 逐帧捕捉
    ret, frame = cap.read()
    
    # 如果帧读取正确,ret 为 True
    if not ret:
        print('无法收到视频帧数据(该视频流是否已结束?),程序正在退出')
        break
    
    # 转换该视频帧为灰度图像
    gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    # 显示该帧
    cv.imshow('frame', gray)
    # 当按下键盘 q 时,退出程序
    if cv.waitKey(1) == ord('q'):
        break

# 当程序结束时,释放该摄像头资源
cap.release()
cv.destroyAllWindows()

本示例代码打包下载:

附参考教程原文:https://docs.opencv.org/4.5.5/dd/d43/tutorial_py_video_display.html

OpenCV-Python 读取图片、展示图片窗口、保存图片

本文通过 OpenCV-Python 对图片文件进行读取并弹出展示窗口进行展示该图片,当键盘按下s键时则会保存当前图片为png格式到当前程序 目录中。

文件结构如下:

main.py 代码如下:

import cv2 as cv
import sys

# 在本教程中,您将学习如何:

# 从文件中读取图像(使用cv::imread)
# 在 OpenCV 窗口中显示图像(使用cv::imshow)
# 将图像写入文件(使用cv::imwrite)


# 读取图片
img = cv.imread('starry_night.jpeg')
print(img)

if img is None:
    sys.exit("无法读取该图像")

# 弹出窗口显示图片
cv.imshow('窗口标题', img)
k = cv.waitKey(0)

# 如果按下 s 键,则保存当前图片为 png 格式在当前目录
if k == ord('s'):
    cv.imwrite('starry_night.png', img)

下载本示例代码包:

附参考教程原文:https://docs.opencv.org/4.5.5/db/deb/tutorial_display_image.html

基于 Electron 的屏幕录制工具(带画中画)

本文主要实现了基于 Electron 技术的屏幕录制、麦克风声音录制、摄像头(画中画)录制。

整体效果如下所示:

代码中,比较重要的就是对多个媒体轨道进行合成、视频合成。

核心代码如下:

// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// No Node.js APIs are available in this process because
// `nodeIntegration` is turned off. Use `preload.js` to
// selectively enable features needed in the rendering
// process.

const jQuery = (window.jQuery = require('jquery'));
require('@popperjs/core');
require('bootstrap');
const Vue = require('vue/dist/vue');
const electron = require('electron');
const fs = require('fs');
const SCREEN_WIDTH = 3072;
const SCREEN_HEIGHT = 1920;
const PlayerCanvas = require('./PlayerCanvas');

new Vue({
  el: '#vueapp',
  data: {
    recording: false,
  },
  mounted() {
    this._playerCanvas = new PlayerCanvas(SCREEN_WIDTH, SCREEN_HEIGHT);
  },
  methods: {
    // 开始录制
    async btnStartRecordClicked(e) {
      this._stream = new MediaStream();
      await this.attachAudioStream();

      // 摄像头 stream
      this._cameraStream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: false,
      });
      this._playerCanvas.setCameraVideo(
        this.createVideoElementWithStream(this._cameraStream)
      );

      // 屏幕 stream
      this._screenStream = await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          mandatory: {
            chromeMediaSource: 'desktop',
            minWidth: SCREEN_WIDTH,
            maxWidth: SCREEN_WIDTH,
            minHeight: SCREEN_HEIGHT,
            maxHeight: SCREEN_HEIGHT,
          },
        },
      });
      this._playerCanvas.setScreenVideo(
        this.createVideoElementWithStream(this._screenStream)
      );
      //
      this._audioStream
        .getAudioTracks()
        .forEach((value) => this._stream.addTrack(value));
      let playerCanvasStream = this._playerCanvas.canvas.captureStream();
      playerCanvasStream.getTracks().forEach((t) => this._stream.addTrack(t));
      this.$refs.preview.srcObject = playerCanvasStream;
      this.startRecord();
    },
    // 附加音频流
    async attachAudioStream() {
      // 获取麦克风流
      this._audioStream = await navigator.mediaDevices.getUserMedia({
        video: false,
        audio: true,
      });
      // 将麦克风的流,附加到主流上
      this._audioStream
        .getAudioTracks()
        .forEach((value) => this._stream.addTrack(value));
    },
    // 停止录制
    btnStopRecordClicked(e) {
      this.recording = false;
      this._recorder.stop();
    },
    // 创建一个 HTMLVideoElement
    createVideoElementWithStream(stream) {
      let video = document.createElement('video');
      video.autoplay = true;
      video.srcObject = stream;
      return video;
    },
    // 开始录制
    startRecord() {
      this._recorder = new MediaRecorder(this._stream, {
        mimeType: 'video/webm;codes=h264',
      });
      this._recorder.ondataavailable = async (e) => {
        let path = electron.remote.dialog.showSaveDialogSync(
          electron.remote.getCurrentWindow(),
          {
            title: '保存文件',
            defaultPath: 'ScreenData.webm',
          }
        );

        let dataArrayBuffer = await e.data.arrayBuffer();
        fs.writeFileSync(path, Buffer.from(dataArrayBuffer));
        // fs.writeFileSync(path, new Uint8Array(dataArrayBuffer));
      };
      this._recorder.start();
      this.recording = true;
    },
  },
});

完整源代码下载:

基于 Electron 的屏幕录制工具

主要基于 Chrome 内核提供的屏幕录制流(Stream)、麦克风流(Stream),进行画面与音频轨道合并。

最终会生成录制好的 webm 格式的文件,可以使用浏览器来打开它进行播放。或者使用 ffmpeg 类似的工具进行视频格式转换,使用其他工具进行播放。

实现效果如图所示:

核心代码如下:


const jQuery = (window.jQuery = require('jquery'));
require('@popperjs/core');
require('bootstrap');
const Vue = require('vue/dist/vue');
const electron = require('electron');
const fs = require('fs');

new Vue({
  el: '#vueapp',
  data: {
    recording: false,
  },
  methods: {
    async btnStartRecordClicked(e) {
      this._stream = await navigator.mediaDevices.getUserMedia({
        video: false,
        audio: true,
      });
      let screenStream = await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          mandatory: {
            chromeMediaSource: 'desktop',
            minWidth: 3072,
            maxWidth: 3072,
            minHeight: 1920,
            maxHeight: 1920,
          },
        },
      });
      screenStream
        .getVideoTracks()
        .forEach((value) => this._stream.addTrack(value));
      this.$refs.preview.srcObject = screenStream;
      this._recorder = new MediaRecorder(this._stream, {
        mimeType: 'video/webm;codes=h264',
      });
      this._recorder.ondataavailable = async (e) => {
        let path = electron.remote.dialog.showSaveDialogSync(
          electron.remote.getCurrentWindow(),
          {
            title: '保存文件',
            defaultPath: 'ScreenData.webm',
          }
        );

        let dataArrayBuffer = await e.data.arrayBuffer();
        fs.writeFileSync(path, Buffer.from(dataArrayBuffer));
        // fs.writeFileSync(path, new Uint8Array(dataArrayBuffer));
      };
      this._recorder.start();
      this.recording = true;
    },
    btnStopRecordClicked(e) {
      this.recording = false;
      this._recorder.stop();
    },
  },
});

源码下载:

HTML/JS 实现视频、音频录制

Web 使用 HTML/JS 实现摄像头、麦克风的录制,可实现:录制、暂停、继续、停止、回放。

最终浏览器会提供一个 Bolb 数据,需要保存到本地的话,可自行搜索相关操作。

实现截图:

录制完成后,点击 Play,然后点击播放器的播放按钮即可播放。

代码:

Flex 各种常用布局示例

一、传统布局与 flex 布局

参考视频:点击进入

Flex布局笔记:下载Flex笔记.html(内容同本文)

传统布局:

  • 兼容性好
  • 布局繁琐
  • 局限性,不能在移动端很好的布局

flex 弹性布局

  • 操作方便,布局极为简单,移动端应用很广泛
  • IE 10 (含)以下浏览器支持情况较差

建议:

1. 如果是 PC 端页面布局,我们还是使用传统布局
2. 如果是移动端布局或者不考虑兼容性问题的 PC 端页面布局,我们还是使用 flex 弹性布局

布局原理:flex 是 flexible Box 的缩写,意为“弹性布局”,用来为盒状模型提供最大的灵活性,任何一个容器都可以指定为 flex 布局。

二、开始学习 Flex 布局

以下由 6 个属性是对父元素设置的:

  • flex-direction:设置主轴的方向
  • justify-content:设置主轴上的子元素排列方式
  • flex-wrap:设置子元素是否允许换行
  • align-content:设置侧轴上的子元素的排列方式(多行)
  • align-items:设置侧轴上的子元素排列方式(单行)
  • flex-flow:复合属性,相当于同时设置了 flex-direction 和 flex-wrap

2.1 基础排列

<style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
</style>
      
<div style="display: flex; background-color: #e6f7ff">
        <div class="card">1</div>
        <div class="card">2</div>
        <div class="card">3</div>
</div>

2.2 垂直排列

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
      </style>
      
      <div style="display: flex; flex-direction: column; background-color: #e6f7ff">
        <div class="card">1</div>
        <div class="card">2</div>
        <div class="card">3</div>
      </div>

2.3 靠末尾对齐(靠右、靠下)

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; justify-content: flex-end; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
        </div>

2.4 中间对齐

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; justify-content: center; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
        </div>

2.5 平分剩余空间

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; justify-content: space-around; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
        </div>

2.6 两边贴边,再平均分配剩余空间

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; justify-content: space-between; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
        </div>

2.7 超过空间长度,自动换行

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; flex-wrap: wrap; width: 280px; height: 400px; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
        </div>

Tips:中间出现了大面积空白区域,并没有跟紧第一行进行排列,可参考示例 9. 横向+纵向居中(多行) 添加 align-content: flex-start; 即可解决该问题。

2.8 横向+纵向居中(单行)

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; justify-content: center; align-items: center; height: 400px; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
        </div>

之前的示例,一直设置的都是主轴justify-content属性,我们再加上侧轴的align-items属性可以实现更多排列可能性

两者都设置为center则完成了整体画面居中的效果。

注意:该排列方式只适用于单行排列

2.9 横向+纵向居中(多行)

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; justify-content: center; align-content: center; flex-wrap: wrap; height: 400px; width: 400px; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
          <div class="card">4</div>
          <div class="card">5</div>
          <div class="card">6</div>
        </div>

2.10 上下平均分配(多行)

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; justify-content: center; align-content: space-around; flex-wrap: wrap; height: 400px; width: 400px; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
          <div class="card">4</div>
          <div class="card">5</div>
          <div class="card">6</div>
        </div>

2.11 上下贴到底后,再平均分配(多行)

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; justify-content: center; align-content: space-between; flex-wrap: wrap; height: 400px; width: 400px; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
          <div class="card">4</div>
          <div class="card">5</div>
          <div class="card">6</div>
          <div class="card">7</div>
          <div class="card">8</div>
          <div class="card">9</div>
        </div>

2.12 flex-flow 复合属性

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex-flow: column wrap; height: 400px; background-color: #e6f7ff">
          <div class="card">1</div>
          <div class="card">2</div>
          <div class="card">3</div>
          <div class="card">4</div>
          <div class="card">5</div>
        </div>

相当于同时设置了flex-direction: column;以及flex-wrap: wrap; 属性的一种简写形式

父级 flex 属性示例结束提示

如果以上示例没有您想要的效果,请尝试以上相似示例并多加上一个flex-direction: column;来设置主轴的方向(也就是主要排列方向)

三、子项属性布局示例

3.1 flex 属性实现(圣杯布局)

实现目标:左侧占据100px、右侧占据100px、中间占满剩余

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; background-color: #e6f7ff">
          <div class="card" style="width: 100px; background-color: #ff7875;">1</div>
          <div class="card" style="flex: 1;">2</div>
          <div class="card" style="width: 100px; background-color: #ffc53d;">3</div>
        </div>

子项的flex:1属性,表示一共占用多少份,这里的 1 ,即表示占掉1份。 因为没有其他的同级元素有赋值 flex: number 属性,所以它占满全部。
请前往 3.2.flex 属性实现平均分配 例子进行参考 flex 属性的应用

3.2 flex 属性实现平均分配

实现目标:左侧1/4,中间2/4,右边1/4 的宽度占比

        <style>
        .card {
          background-color: #91d5ff;
          width: 100px;
          height: 100px;
          margin: 4px;
        }
        </style>
        
        <div style="display: flex; background-color: #e6f7ff">
          <div class="card" style="flex: 1; background-color: #ff7875;">1</div>
          <div class="card" style="flex: 2;">2</div>
          <div class="card" style="flex: 1; background-color: #ffc53d;">3</div>
        </div>

至此,flex 常用布局就完结了, flex 的属性不止于本文档中所示。它还有其他的不太常用的属性,可以自行查阅相关文档进行学习。