12 实战:Python视频处理系统:光流分析与实时增强的实现

本文旨在详细解析一个复杂的视频处理与分析系统,该系统利用Python及其相关库(如OpenCV、Pygame、moviepy等)实现了视频信息的提取、帧处理、光流计算、音频同步播放、视频缓冲、帧增强和视频统计分析等功能。我们将通过分级标题的方式,逐步深入解析系统的各个部分。

系统概述

该系统是一个综合的视频处理与分析平台,通过多个模块和类的协同工作,实现了对视频文件的全面处理与分析。主要功能模块包括视频信息提取、音频播放、光流计算、帧差异计算、视频缓冲、帧增强和视频统计分析等。

首先看效果:

播放的视频中有两位小女生唱歌,有原始视频、光流和帧差异以及视频信息等窗口

系统架构

系统的主要架构可以分为以下几个部分:

  1. 视频信息提取:通过VideoInfo类获取视频的基本信息,如分辨率、帧率、总帧数、时长和编解码器等。
  2. 音频播放:通过AudioPlayer类实现视频音频的同步播放,支持播放、暂停、恢复和停止功能。
  3. 光流计算:通过OpticalFlowProcessor类计算和可视化视频帧间的光流。
  4. 帧差异计算:通过FrameProcessor类计算视频帧间的差异,用于检测运动。
  5. 视频缓冲:通过VideoBuffer类实现视频帧的预加载和缓存,防止播放卡顿。
  6. 帧增强:通过FrameEnhancer类对视频帧进行增强处理,如去噪和时域滤波。
  7. 视频统计分析:通过VideoAnalyzer类对视频进行统计分析,如计算平均运动量、最大运动量等。
  8. 视频文件选择:通过VideoFileSelector类实现视频文件的选择功能。

详细解析

1. 视频信息提取

VideoInfo类

VideoInfo类用于存储和管理视频的基本信息。它使用OpenCV的VideoCapture类打开视频文件,并获取视频的帧率、总帧数、分辨率、时长和编解码器等信息。这些信息对于后续的视频处理和分析至关重要。

  • fps:帧率,表示每秒播放的帧数。
  • frame_count:总帧数,表示视频中的总帧数。
  • widthheight:分辨率,表示视频的宽度和高度。
  • duration:时长,表示视频的总播放时间(秒)。
  • fourcccodec:编解码器信息,表示视频使用的编码格式。

2. 音频播放

AudioPlayer类

AudioPlayer类负责视频音频的同步播放。它使用moviepy库提取视频中的音频,并使用pygame库进行播放。该类支持播放、暂停、恢复和停止功能,并提供了音频同步播放的机制。

  • start:开始播放音频。
  • pause:暂停音频播放。
  • resume:恢复音频播放。
  • stop:停止音频播放并清理资源。

3. 光流计算

OpticalFlowProcessor类

OpticalFlowProcessor类用于计算和可视化视频帧间的光流。光流是一种描述图像中物体运动模式的方法,通过计算相邻帧之间像素点的位移来估计物体的运动。

  • init_flow:初始化光流计算,包括选择特征点和设置跟踪参数。
  • calculate_flow:计算光流并返回可视化结果。
  • reset:重置光流处理器状态。

4. 帧差异计算

FrameProcessor类

FrameProcessor类用于计算视频帧间的差异,以检测运动。它通过计算相邻帧之间像素点的绝对差异,并应用阈值来生成二值化的帧差图像。

  • calculate_frame_difference:计算帧间差异并返回二值化结果。
  • reset:重置帧处理器状态。

5. 视频缓冲

VideoBuffer类

VideoBuffer类实现视频帧的预加载和缓存,以防止播放卡顿。它使用队列来存储预加载的帧,并在播放时按需提供帧。

  • start_buffering:开始帧缓冲。
  • get_frame:获取缓冲的帧。
  • stop:停止缓冲并清理资源。

6. 帧增强

FrameEnhancer类

FrameEnhancer类用于对视频帧进行增强处理,如去噪和时域滤波。它使用双边滤波来减少噪声,并使用时域滤波来防止闪烁。

  • enhance_frame:增强视频帧。
  • reset:重置增强器状态。

7. 视频统计分析

VideoAnalyzer类

VideoAnalyzer类用于对视频进行统计分析,如计算平均运动量、最大运动量等。它通过分析帧差图像来提取运动信息,并计算相关的统计指标。

  • analyze_motion:分析帧间运动并返回运动量。
  • calculate_statistics:计算视频统计信息。
  • add_processing_time:添加帧处理时间。
  • reset:重置分析器状态。

8. 视频文件选择

VideoFileSelector类

VideoFileSelector类实现视频文件的选择功能。它使用tkinter库创建文件选择对话框,并返回用户选择的文件路径。

  • select_file:打开文件选择对话框并返回所选文件路径。

主函数流程

main函数

main函数是系统的入口点,它负责初始化各个模块、处理视频文件并清理资源。主要流程如下:

  1. 创建文件选择器并获取视频路径:使用VideoFileSelector类打开文件选择对话框,并获取用户选择的视频文件路径。
  2. 初始化视频处理器:创建VideoProcessor对象,并传入视频文件路径。
  3. 创建帧缓冲区:创建VideoBuffer对象,并开始帧缓冲。
  4. 创建帧增强器:创建FrameEnhancer对象。
  5. 创建视频分析器:创建VideoAnalyzer对象。
  6. 开始处理视频:调用VideoProcessor对象的play方法开始处理视频。
  7. 清理资源:捕获异常并清理资源,如停止帧缓冲、销毁OpenCV窗口等。

系统特点与优势

该系统具有以下几个特点和优势:

  • 综合性:集成了视频信息提取、音频播放、光流计算、帧差异计算、视频缓冲、帧增强和视频统计分析等多个功能模块。
  • 实时性:通过帧缓冲和音频同步播放机制,实现了视频的实时处理和播放。
  • 可扩展性:各个模块相对独立,易于扩展和定制。例如,可以添加更多的帧处理方法或统计分析指标。
  • 用户友好性:提供了图形化的视频文件选择界面,方便用户选择视频文件。

总结

本文详细解析了一个复杂的视频处理与分析系统,从系统架构、主要功能模块到主函数流程进行了全面的阐述。该系统通过多个模块和类的协同工作,实现了对视频文件的全面处理与分析,具有综合性、实时性、可扩展性和用户友好性等特点。希望本文能够为读者提供有价值的参考和启示。

完整代码如下:

"""
作者:1248693038 版权所有,转载请注明出处,二创请联系作者获得授权。
"""
import cv2
import numpy as np
import pygame
from threading import Thread, Lock, Event
from queue import Queue
import time
import os
from datetime import datetime, timedelta
import moviepy.editor as mp
import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
from PIL import Image, ImageDraw, ImageFont


class VideoInfo:
    """
    视频信息类
    存储和管理视频的基本信息
    """

    def __init__(self, video_path):
        self.video_path = video_path
        self.cap = cv2.VideoCapture(video_path)

        # 获取视频基本信息
        self.fps = self.cap.get(cv2.CAP_PROP_FPS)
        self.frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
        self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        self.duration = self.frame_count / self.fps

        # 获取视频编解码器信息
        self.fourcc = int(self.cap.get(cv2.CAP_PROP_FOURCC))
        self.codec = "".join([chr((self.fourcc >> 8 * i) & 0xFF) for i in range(4)])

    def get_formatted_info(self):
        """
        返回格式化的视频信息字符串
        """
        info = [
            f"分辨率: {self.width}x{self.height}",
            f"帧率: {self.fps:.2f} FPS",
            f"总帧数: {self.frame_count}",
            f"时长: {timedelta(seconds=int(self.duration))}",
            f"编解码器: {self.codec}"
        ]
        return "\n".join(info)

    def release(self):
        """
        释放视频资源
        """
        if self.cap is not None:
            self.cap.release()


class AudioPlayer:
    """
    音频播放器类
    负责视频音频的同步播放
    """

    def __init__(self, video_path):
        self.video_path = video_path
        self.audio_started = False
        self.start_time = 0
        self.pause_time = 0
        self.is_paused = False
        self.is_playing = False
        self.audio_thread = None
        self.pause_event = Event()

        # 初始化pygame音频系统
        pygame.mixer.init()

        # 创建临时文件夹
        if not os.path.exists('temp'):
            os.makedirs('temp')

        self.temp_audio_path = os.path.join('temp', 'temp_audio.mp3')

        # 提取音频
        try:
            video = mp.VideoFileClip(video_path)
            audio = video.audio
            if audio is not None:
                audio.write_audiofile(self.temp_audio_path, verbose=False, logger=None)
                self.has_audio = True
            else:
                self.has_audio = False
            video.close()
        except Exception as e:
            print(f"音频提取错误: {str(e)}")
            self.has_audio = False

    def start(self):
        """
        开始播放音频
        """
        if not self.has_audio:
            return

        if not self.is_playing:
            self.is_playing = True
            self.audio_thread = Thread(target=self._audio_player_thread)
            self.audio_thread.daemon = True
            self.audio_thread.start()

    def _audio_player_thread(self):
        """
        音频播放线程
        """
        try:
            pygame.mixer.music.load(self.temp_audio_path)
            pygame.mixer.music.play()
            self.start_time = time.time()
            self.audio_started = True

            while self.is_playing:
                if self.pause_event.is_set():
                    if pygame.mixer.music.get_busy():
                        pygame.mixer.music.pause()
                        self.pause_time = time.time()
                    time.sleep(0.1)
                else:
                    if not pygame.mixer.music.get_busy() and self.is_playing:
                        break
                    time.sleep(0.1)

        except Exception as e:
            print(f"音频播放错误: {str(e)}")

    def pause(self):
        """
        暂停音频播放
        """
        if self.has_audio and self.is_playing:
            self.pause_event.set()
            self.is_paused = True

    def resume(self):
        """
        恢复音频播放
        """
        if self.has_audio and self.is_playing and self.is_paused:
            pygame.mixer.music.unpause()
            self.pause_event.clear()
            self.is_paused = False

    def stop(self):
        """
        停止音频播放
        """
        self.is_playing = False
        if self.has_audio:
            pygame.mixer.music.stop()
            pygame.mixer.quit()

        # 清理临时文件
        try:
            if os.path.exists(self.temp_audio_path):
                os.remove(self.temp_audio_path)
        except Exception as e:
            print(f"清理临时文件错误: {str(e)}")


class OpticalFlowProcessor:
    """
    光流处理器类
    计算和可视化光流
    """

    def __init__(self):
        # 光流计算参数
        self.feature_params = dict(
            maxCorners=100,
            qualityLevel=0.3,
            minDistance=7,
            blockSize=7
        )

        self.lk_params = dict(
            winSize=(15, 15),
            maxLevel=2,
            criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)
        )

        # 用于跟踪的颜色
        self.color = np.random.randint(0, 255, (100, 3))
        self.old_frame = None
        self.old_gray = None
        self.p0 = None
        self.mask = None

    def init_flow(self, frame):
        """
        初始化光流计算
        """
        self.old_frame = frame.copy()
        self.old_gray = cv2.cvtColor(self.old_frame, cv2.COLOR_BGR2GRAY)
        self.p0 = cv2.goodFeaturesToTrack(self.old_gray, mask=None, **self.feature_params)
        self.mask = np.zeros_like(self.old_frame)

    def calculate_flow(self, frame):
        """
        计算光流并返回可视化结果
        """
        if self.old_frame is None:
            self.init_flow(frame)
            return frame

        frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        # 计算光流
        if self.p0 is not None and len(self.p0) > 0:
            p1, st, err = cv2.calcOpticalFlowPyrLK(
                self.old_gray,
                frame_gray,
                self.p0,
                None,
                **self.lk_params
            )

            # 选择好的点
            if p1 is not None:
                good_new = p1[st == 1]
                good_old = self.p0[st == 1]

                # 绘制轨迹
                for i, (new, old) in enumerate(zip(good_new, good_old)):
                    a, b = new.ravel()
                    c, d = old.ravel()
                    self.mask = cv2.line(self.mask, (int(a), int(b)), (int(c), int(d)),
                                         self.color[i].tolist(), 2)
                    frame = cv2.circle(frame, (int(a), int(b)), 5,
                                       self.color[i].tolist(), -1)

                # 更新下一帧的点
                self.p0 = good_new.reshape(-1, 1, 2)

        # 合并帧与轨迹
        output = cv2.add(frame, self.mask)

        # 更新前一帧
        self.old_gray = frame_gray.copy()

        return output

    def reset(self):
        """
        重置光流处理器状态
        """
        self.old_frame = None
        self.old_gray = None
        self.p0 = None
        self.mask = None


class FrameProcessor:
    """
    帧处理器类
    处理视频帧并计算帧间差异
    """

    def __init__(self):
        self.previous_frame = None
        self.frame_diff_threshold = 30

    def calculate_frame_difference(self, frame):
        """
        计算帧间差异
        """
        if self.previous_frame is None:
            self.previous_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            return np.zeros_like(frame)

        # 转换为灰度图
        current_frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        # 计算帧差
        frame_diff = cv2.absdiff(current_frame_gray, self.previous_frame)

        # 应用阈值
        _, frame_diff_binary = cv2.threshold(
            frame_diff,
            self.frame_diff_threshold,
            255,
            cv2.THRESH_BINARY
        )

        # 转换回彩色图像以便显示
        frame_diff_color = cv2.cvtColor(frame_diff_binary, cv2.COLOR_GRAY2BGR)

        # 更新前一帧
        self.previous_frame = current_frame_gray

        return frame_diff_color

    def reset(self):
        """
        重置帧处理器状态
        """
        self.previous_frame = None


class VideoProcessor:
    """
    视频处理器类
    整合视频处理、显示和控制功能
    """

    def __init__(self, video_path):
        self.video_info = VideoInfo(video_path)
        self.audio_player = AudioPlayer(video_path)
        self.optical_flow = OpticalFlowProcessor()
        self.frame_processor = FrameProcessor()

        # 视频处理状态
        self.is_playing = False
        self.is_paused = False
        self.current_frame_number = 0

        # 创建显示窗口
        cv2.namedWindow('Original Video', cv2.WINDOW_NORMAL)
        cv2.namedWindow('Optical Flow', cv2.WINDOW_NORMAL)
        cv2.namedWindow('Frame Difference', cv2.WINDOW_NORMAL)
        cv2.namedWindow('Video Info', cv2.WINDOW_NORMAL)

        # 调整窗口大小
        window_width = self.video_info.width // 2
        window_height = self.video_info.height // 2
        cv2.resizeWindow('Original Video', window_width, window_height)
        cv2.resizeWindow('Optical Flow', window_width, window_height)
        cv2.resizeWindow('Frame Difference', window_width, window_height)
        cv2.resizeWindow('Video Info', 400, 200)

        # 添加字体路径
        self.font_path = self._get_system_font()

    def _get_system_font(self):
        """
        获取系统中文字体路径
        """
        # 常见的中文字体路径
        possible_fonts = [
            # Windows 字体路径
            "C:/Windows/Fonts/simhei.ttf",  # 黑体
            "C:/Windows/Fonts/simsun.ttc",  # 宋体
            "C:/Windows/Fonts/msyh.ttc",  # 微软雅黑

            # Linux 字体路径
            "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",

            # macOS 字体路径
            "/System/Library/Fonts/PingFang.ttc",
            "/Library/Fonts/Arial Unicode.ttf"
        ]

        # 查找第一个存在的字体文件
        for font_path in possible_fonts:
            if os.path.exists(font_path):
                return font_path

        # 如果找不到任何中文字体,抛出异常
        raise Exception("未找到可用的中文字体文件")

    def process_frame(self, frame):
        """
        处理单个视频帧
        """
        if frame is None:
            return None, None, None

        # 计算光流
        optical_flow_frame = self.optical_flow.calculate_flow(frame.copy())

        # 计算帧差
        frame_diff = self.frame_processor.calculate_frame_difference(frame.copy())

        return frame, optical_flow_frame, frame_diff

    def create_info_frame(self):
        """
        创建信息显示帧,支持中文
        """
        # 创建黑色背景图像
        img = Image.new('RGB', (400, 200), color='black')
        draw = ImageDraw.Draw(img)

        try:
            # 使用系统字体,大小为15
            font = ImageFont.truetype(self.font_path, 15)
        except Exception as e:
            print(f"加载字体失败: {str(e)}")
            # 如果加载字体失败,使用默认字体
            font = ImageFont.load_default()

        # 获取基本信息文本
        info_text = self.video_info.get_formatted_info()

        # 添加当前帧信息
        current_time = self.current_frame_number / self.video_info.fps
        info_text += f"\n当前帧: {self.current_frame_number}/{self.video_info.frame_count}"
        info_text += f"\n当前时间: {timedelta(seconds=int(current_time))}"

        # 绘制文本
        y = 10
        for line in info_text.split('\n'):
            draw.text((10, y), line, font=font, fill=(255, 255, 255))
            y += 25

        # 转换回OpenCV格式
        info_frame = np.array(img)
        # 转换RGB为BGR(OpenCV使用BGR格式)
        info_frame = cv2.cvtColor(info_frame, cv2.COLOR_RGB2BGR)

        return info_frame

    def play(self):
        """
        播放视频
        """
        self.is_playing = True
        self.audio_player.start()

        try:
            while self.is_playing:
                if self.is_paused:
                    time.sleep(0.1)
                    continue

                ret, frame = self.video_info.cap.read()
                if not ret:
                    break

                # 处理帧
                original, flow, diff = self.process_frame(frame)
                info_frame = self.create_info_frame()

                # 显示帧
                cv2.imshow('Original Video', original)
                cv2.imshow('Optical Flow', flow)
                cv2.imshow('Frame Difference', diff)
                cv2.imshow('Video Info', info_frame)

                # 更新帧计数
                self.current_frame_number += 1

                # 控制播放速度
                target_time = self.current_frame_number / self.video_info.fps
                if self.audio_player.audio_started:
                    current_time = time.time() - self.audio_player.start_time
                    if current_time < target_time:
                        time.sleep(target_time - current_time)
                else:
                    time.sleep(1 / self.video_info.fps)

                # 处理键盘事件
                key = cv2.waitKey(1) & 0xFF
                if key == ord('q'):
                    break
                elif key == ord('p'):
                    self.toggle_pause()
                elif key == ord('r'):
                    self.reset()

        finally:
            self.stop()

    def toggle_pause(self):
        """
        切换暂停/播放状态
        """
        self.is_paused = not self.is_paused
        if self.is_paused:
            self.audio_player.pause()
        else:
            self.audio_player.resume()

    def reset(self):
        """
        重置视频播放状态
        """
        self.video_info.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
        self.current_frame_number = 0
        self.optical_flow.reset()
        self.frame_processor.reset()
        self.audio_player.stop()
        self.audio_player = AudioPlayer(self.video_info.video_path)

    def stop(self):
        """
        停止视频播放并清理资源
        """
        self.is_playing = False
        self.audio_player.stop()
        self.video_info.release()
        cv2.destroyAllWindows()


class VideoBuffer:
    """
    视频缓冲区类
    实现视频帧的预加载和缓存,防止播放卡顿
    """

    def __init__(self, video_path, buffer_size=30):
        self.cap = cv2.VideoCapture(video_path)
        self.buffer_size = buffer_size
        self.frame_buffer = Queue(maxsize=buffer_size)
        self.lock = Lock()
        self.is_running = False
        self.buffer_thread = None

    def start_buffering(self):
        """
        开始帧缓冲
        """
        self.is_running = True
        self.buffer_thread = Thread(target=self._buffer_frames)
        self.buffer_thread.daemon = True
        self.buffer_thread.start()

    def _buffer_frames(self):
        """
        帧缓冲线程
        """
        while self.is_running:
            if self.frame_buffer.qsize() < self.buffer_size:
                ret, frame = self.cap.read()
                if ret:
                    self.frame_buffer.put((ret, frame))
                else:
                    # 视频结束时重新开始
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
            else:
                time.sleep(0.01)

    def get_frame(self):
        """
        获取缓冲的帧
        """
        if not self.frame_buffer.empty():
            return self.frame_buffer.get()
        return False, None

    def stop(self):
        """
        停止缓冲
        """
        self.is_running = False
        if self.buffer_thread is not None:
            self.buffer_thread.join()
        self.cap.release()


class FrameEnhancer:
    """
    帧增强类
    实现防闪烁和图像增强功能
    """

    def __init__(self):
        self.previous_frame = None
        self.temporal_filter_alpha = 0.5

    def enhance_frame(self, frame):
        """
        增强视频帧
        """
        if frame is None:
            return None

        # 应用双边滤波减少噪声同时保持边缘
        denoised = cv2.bilateralFilter(frame, 9, 75, 75)

        # 时域滤波防止闪烁
        if self.previous_frame is not None:
            enhanced = cv2.addWeighted(
                denoised,
                self.temporal_filter_alpha,
                self.previous_frame,
                1 - self.temporal_filter_alpha,
                0
            )
        else:
            enhanced = denoised

        self.previous_frame = enhanced.copy()

        return enhanced

    def reset(self):
        """
        重置增强器状态
        """
        self.previous_frame = None


class VideoAnalyzer:
    """
    视频分析类
    实现视频统计分析功能
    """

    def __init__(self):
        self.motion_history = []
        self.frame_processing_times = []

    def analyze_motion(self, frame_diff):
        """
        分析帧间运动
        """
        if frame_diff is None:
            return 0

        motion_magnitude = np.mean(frame_diff)
        self.motion_history.append(motion_magnitude)
        return motion_magnitude

    def calculate_statistics(self):
        """
        计算视频统计信息
        """
        if not self.motion_history:
            return {}

        stats = {
            '平均运动量': np.mean(self.motion_history),
            '最大运动量': np.max(self.motion_history),
            '运动量标准差': np.std(self.motion_history),
            '平均处理时间': np.mean(self.frame_processing_times) if self.frame_processing_times else 0
        }
        return stats

    def add_processing_time(self, processing_time):
        """
        添加帧处理时间
        """
        self.frame_processing_times.append(processing_time)

    def reset(self):
        """
        重置分析器状态
        """
        self.motion_history = []
        self.frame_processing_times = []


class VideoFileSelector:
    """
    视频文件选择器类
    实现视频文件的选择功能
    """

    def __init__(self):
        self.root = tk.Tk()
        self.root.withdraw()  # 隐藏主窗口

    def select_file(self):
        """
        打开文件选择对话框并返回所选文件路径
        """
        file_path = filedialog.askopenfilename(
            title="选择视频文件",
            filetypes=[
                ("视频文件", "*.mp4;*.avi;*.mkv;*.mov;*.wmv"),
                ("所有文件", "*.*")
            ]
        )

        if file_path:
            if os.path.exists(file_path):
                return file_path
            else:
                messagebox.showerror("错误", "所选文件不存在")
                return None
        return None


def main():
    """
    主函数
    """
    try:
        # 创建文件选择器并获取视频路径
        file_selector = VideoFileSelector()
        video_path = file_selector.select_file()

        if video_path is None:
            print("未选择视频文件")
            return

        print("\n初始化视频处理器...")
        print("控制键:")
        print("  'p': 暂停/继续")
        print("  'r': 重置")
        print("  'q': 退出")

        # 创建视频处理对象
        processor = VideoProcessor(video_path)

        # 创建帧缓冲区
        buffer = VideoBuffer(video_path)
        buffer.start_buffering()

        # 创建帧增强器
        enhancer = FrameEnhancer()

        # 创建视频分析器
        analyzer = VideoAnalyzer()

        # 开始处理视频
        processor.play()

    except Exception as e:
        print(f"发生错误: {str(e)}")
        messagebox.showerror("错误", str(e))

    finally:
        # 清理资源
        if 'buffer' in locals():
            buffer.stop()
        cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值