OpenCV:实时视频防抖,python版(改进连续帧处理)

  • 改进思路

上个版本的代码,每30帧就全清。显然不符合我们要求。

我们期望:分析当前帧的前30帧的情况。

  • 代码

同时去掉了无用的gray数组。

复制代码
import numpy as np
import cv2
import time



# 给画面画网格线
def video_grid(frame, width, height, direction, color):
    if (not merge_video):
        return

    if (direction):
        for i in range(1, int(width/grid_size)+1):
            cv2.line(frame,
                     (int(grid_size*i), int(0)          ),
                     (int(grid_size*i), int(height     )),
                     color, 2)
    else:
        for i in range(1, int(height/grid_size)+1):
            cv2.line(frame,
                     (int(0          ), int(grid_size*i)),
                     (int(width      ), int(grid_size*i)),
                     color, 2)

def video_circle(frame, width, height, color):
    cv2.circle(frame,
               (int(width/2), int(height/2)),
               5, color, 3)

    cv2.circle(frame,
                   (int(width/2), int(height/2)),
                   grid_size, color, 2)
                   
    size = min(width, height)
    for i in range(1, int(size/grid_size)+1):
        cv2.circle(frame,
                   (int(width/2), int(height/2)),
                   grid_size, color, 2)
                   
# 标出匹配的关键点
def video_keypoints(frame, matches, keypoints, check_x, check_y):
    for match in matches:
        curr_point = keypoints[match.trainIdx].pt
        cv2.circle(frame,
                   (int(curr_point[0]+check_x), int(curr_point[1]+check_y)),
                   3, (0, 255, 0), 2)

# 文件名
def get_filename():
    filename = 'stab'
    if (merge_video):
        if (merge_horizontal):
            filename += '_h'
        else:
            filename += '_v'

    filename += '.mp4'
    return filename

# 计算剪裁位置,避免越界
def get_dst_pos(crop_p, mean_motion_p):
    dst_p = int(crop_p + mean_motion_p)
    if (dst_p < 0):
        dst_p = 0
    elif (dst_p > crop_p*2):
        dst_p = crop_p*2

    return dst_p


def movingAverage(curve, radius):
    window_size = 2 * radius + 1
    # 定义过滤器
    f = np.ones(window_size) / window_size
    # 为边界添加填充
    curve_pad = np.pad(curve, (radius, radius), 'edge')
    # 应用卷积
    curve_smoothed = np.convolve(curve_pad, f, mode='same')
    # 删除填充
    curve_smoothed = curve_smoothed[radius:-radius]
    # 返回平滑曲线
    return curve_smoothed


def smooth(trajectory):
    smoothed_trajectory = np.copy(trajectory)
    # 过滤x, y和角度曲线
    for i in range(3):
        smoothed_trajectory[:, i] = movingAverage(
            trajectory[:, i], radius=SMOOTHING_RADIUS)

    return smoothed_trajectory


def fixBorder(frame):
    s = frame.shape
    T = cv2.getRotationMatrix2D((s[1] / 2, s[0] / 2), 0, ZOOM_RATIO)
    frame = cv2.warpAffine(frame, T, (s[1], s[0]))
    return frame

# 在不移动中心的情况下,将图像缩放
ZOOM_RATIO = 1.05

FRAME_BUFFER_COUNT = 30

# 尺寸越大,视频越稳定,但对突然平移的反应越小
SMOOTHING_RADIUS = 50

# 是否合并视频
merge_video      = True
# 合并视频时,水平或垂直
merge_horizontal = True

# 网格线间隔
grid_size        = 100


# 读取输入视频
cap = cv2.VideoCapture('../test-1920X1080.mp4')

# 获取视频流的宽度和高度
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# 获取每秒帧数(fps)
fps = cap.get(cv2.CAP_PROP_FPS)

src_width = w
src_height= h
# 只检测中心位置的区域。
check_n = 1/5
check_w = int( src_width *check_n)
check_h = int( src_height*check_n)
check_x = int((src_width -check_w)/2)
check_y = int((src_height-check_h)/2)

# 剪裁范围
crop_n  = 1/5
crop_x  = int(src_width *   crop_n/2)
crop_y  = int(src_height*   crop_n/2)
crop_w  = int(src_width *(1-crop_n) )
crop_h  = int(src_height*(1-crop_n) )

write_w = w
write_h = h
if (merge_video):
    if (merge_horizontal):
        write_w += w
    else:
        write_h += h
        
# 设置视频输出格式
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out    = cv2.VideoWriter(get_filename(), fourcc, cap.get(cv2.CAP_PROP_FPS), (write_w, write_h))

# # 得到帧数
# n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

# 预定义转换numpy矩阵
# transforms = np.zeros((n_frames - 1, 3), np.float32)

# 准备存储
transforms = np.zeros((FRAME_BUFFER_COUNT, 3), np.float32)

work_cost      = 0
frame_counter  = 0
buffer_pointer = 0
while cap.isOpened():

    # print(k)

    # 读取一帧
    success, curr = cap.read()

    # 耗时开始
    frame_counter += 1
    current_time = time.time()
    
    # 是否还有下一帧,关闭
    if not success:
        break
 
    video_circle(curr, w, h, (0, 0, 255))
    # 转换为灰度图
    curr_gray = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY)

    if buffer_pointer >= 2:
        # 检测前一帧的特征点
        prev_pts = cv2.goodFeaturesToTrack(prev_gray,
                                           maxCorners=200,
                                           qualityLevel=0.01,
                                           minDistance=30,
                                           blockSize=3)
        # 计算光流(即轨迹特征点) 前一张 当前张 前一张特征
        curr_pts, status, err = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, None)

        # 检查完整性
        assert prev_pts.shape == curr_pts.shape

        # 只过滤有效点
        idx = np.where(status == 1)[0]
        prev_pts = prev_pts[idx]
        curr_pts = curr_pts[idx]

        # 找到变换矩阵
        m, inlier = cv2.estimateAffine2D(prev_pts, curr_pts)

        # 提取traslation
        dx = m[0, 2]
        dy = m[1, 2]

        # 提取旋转角
        da = np.arctan2(m[1, 0], m[0, 0])

        # 存储转换
        transforms[buffer_pointer] = [dx, dy, da]

        # cv2.imshow("B", curr_gray)
        # cv2.waitKey(1)

    if buffer_pointer >= 3:
        # 使用累积变换和计算轨迹
        trajectory = np.cumsum(transforms, axis=0)

        # 创建变量来存储平滑的轨迹
        smoothed_trajectory = smooth(trajectory)

        # 计算smoothed_trajectory与trajectory的差值
        difference = smoothed_trajectory - trajectory

        # 计算更新的转换数组
        transforms_smooth = transforms + difference

        # 从新的转换数组中提取转换
        dx = transforms_smooth[buffer_pointer+1, 0]
        dy = transforms_smooth[buffer_pointer+1, 1]
        da = transforms_smooth[buffer_pointer+1, 2]
        # print('frame_counter=', frame_counter, 'dx=', int(dx), 'dy=', int(dy), 'da=', int(da*360))
        
        # 根据新的值重构变换矩阵
        m = np.zeros((2, 3), np.float32)
        m[0, 0] =  np.cos(da)
        m[0, 1] = -np.sin(da)
        m[1, 0] =  np.sin(da)
        m[1, 1] =  np.cos(da)
        m[0, 2] =  dx
        m[1, 2] =  dy

        # 应用仿射包装到给定的框架
        frame_stabilized = cv2.warpAffine(curr, m, (w, h))

        # Fix border artifacts
        frame_stabilized = fixBorder(frame_stabilized)
        video_circle(frame_stabilized, w, h, (0, 255, 255))
        # cv2.imshow("B", frame_stabilized)
        # cv2.waitKey(1)
        
        if (merge_horizontal):
            out_frame = cv2.hconcat([curr, frame_stabilized])
        else:
            out_frame = cv2.vconcat([curr, frame_stabilized])
        
        work_cost += (time.time()-current_time)
        
        # video_grid(out_frame, write_w, write_h, (not merge_horizontal), (0, 0, 255))
        out.write(out_frame)
        
    buffer_pointer += 1
    # transforms_smooth use FRAME_BUFFER_COUNT-1
    if buffer_pointer >= FRAME_BUFFER_COUNT-1:
        buffer_pointer = FRAME_BUFFER_COUNT-2
        for i in range(0, FRAME_BUFFER_COUNT-2):
            transforms[i] = transforms[i+1]

    prev_gray = curr_gray

print('cost per frame(ms)=', (work_cost/frame_counter*1000))
# 释放视频读取和写入对象
cap.release()
out.release()
# 关闭所有OpenCV窗口
cv2.destroyAllWindows()
相关推荐
qq_3106585113 小时前
webrtc代码走读(十七)-音频QOS-NetEQ
服务器·网络·c++·音视频·webrtc
weixin_4624462316 小时前
Python 使用 FFmpeg 给视频添加内嵌字幕(SRT)完整教程(含代码示例)
python·ffmpeg·音视频
txp玩Linux1 天前
rk3568上解析webrtc音频降噪算法处理流程
算法·音视频·webrtc
浅笑离愁12341 天前
VI视频输入模块学习
linux·音视频
EasyDSS1 天前
视频推拉流平台EasyDSS平台如何赋能无人机推流直播新纪元
音视频·无人机
nvd111 天前
niri 音频图形界面工具
前端·chrome·音视频
lqj_本人1 天前
鸿蒙Qt音频实战:解决QMediaPlayer的高延迟与杂音问题
qt·音视频·harmonyos
0***141 天前
JavaScript视频处理案例
开发语言·javascript·音视频
勇气要爆发1 天前
第三阶段:ExoPlayer进阶播放器
android·音视频·exoplayer