[Video and Audio Data Processing] 图像的表示方法:RGB模型 vs YUV模型

本文深入讲解YUV模型,包括其与RGB模型的区别,色深、色度半采样概念,以及在视频处理中的应用。探讨了亮度与色度在图像中的作用,分析了空间与时间的低频和高频信息对视频质量的影响。

0. YUV模型介绍

光的三原色是红(Red)、绿(Green)、蓝(Blue)。现代的显示器技术就是通过组合不同强度的三原色,来达成任何一种可见光的颜色。图像储存中,通过记录每个像素红绿蓝强度,来记录图像的方法,称为RGB模型 (RGB Model)

常见的图片格式中,PNG和BMP这两种就是基于RGB模型的。

比如说原图:

在这里插入图片描述
分别只显示R G B通道的强度,效果如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

三个通道下,信息量和细节程度不一定是均匀分布的。比如说可以注意南小鸟脸上的红晕,在3个平面上的区分程度就不同——红色平面下几乎无从区分,造成区别的主要是绿色和蓝色的平面。外围白色的脸颊,三色都近乎饱和;但是红晕部分,只有红色饱和,绿色和蓝色不饱和。这是造成红色凸显的原因。

除了RGB模型,还有一种广泛采用的模型,称为YUV模型,又被称为亮度-色度模型(Luma-Chroma)。它是通过数学转换,将RGB三个通道,转换为一个代表亮度的通道(Luma),和两个代表色度的通道(并成为Chroma)。

举个形象点的例子:一家养殖场饲养猪和牛,一种记数方式是:(猪的数量,牛的数量)
但是也可以这么记录:(总数量=猪的数量+牛的数量,相差=猪的数量-牛的数量)。两种方法之间有公式可以互转。

YUV模型干的是类似的事儿。通过对RGB数据的合理转换,得到另一种表示方式。YUV模型下,还有不同的实现方式。举个用的比较多的YCbCr模型:它把RGB转换成一个亮度(Y),和 蓝色色度(Cb) 以及 红色色度(Cr)。转换背后复杂的公式大家不需要了解,只需要看看效果:

只有亮度通道:

在这里插入图片描述
只有蓝色色度:

在这里插入图片描述
只有红色色度:

在这里插入图片描述
在图像视频的加工与储存中,YUV格式一般更受欢迎,理由如下:

0.1、人眼对亮度的敏感度远高于色度,因此人眼看到的有效信息主要来自于亮度。YUV模型可以将绝大多数的有效信息分配到Y通道。UV通道相对记录的信息少的多。相对于RGB模型较为平均的分配,YUV模型将多数有效信息集中在Y通道,不但减少了冗余信息量,还为压缩提供了便利

0.2、保持了对黑白显示设备的向下兼容

0.3、图像编辑中,调节亮度和颜色饱和度,在YUV模型下更方便。

几乎所有的视频格式,以及广泛使用的JPEG图像格式,都是基于YCbCr模型的。播放的时候,播放器需要将YCbCr的信息转换为RGB。这个步骤称为渲染(Rendering)

每个通道的记录,通常是用整数来表示。比如RGB24,就是RGB各8个bit,用0~255 (8bit的二进制数范围)来表示某个颜色的强弱。YUV模型也不例外,也是用整数来表示每个通道的高低。

1. 色深

色深(bit-depth),就是我们通常说的8bit和10bit,是指每个通道的精度。8bit就是每个通道用一个8bit整数(0255)代表,10bit就是用10bit整数(01023)来显示。16bit则是0~65535

你的显示器是8bit的,代表它能显示RGB每个通道0~255所有强度。但是视频的色深是YUV的色深,播放的时候,YUV需要通过计算转换到RGB。因此,10bit的高精度是间接的,它使得运算过程中精度增加,以让最后的颜色更细腻。

如何理解8bit显示器,播放10bit是有必要的呢:

一个圆的半径是12.33m, 求它的面积,保留两位小数。
半径的精度给定两位小数,结果也要求两位小数,那么圆周率精度需要给多高呢?也只要两位小数么?
取pi=3.14, 面积算出来是477.37平方米
取pi=3.1416,面积算出来是477.61平方米
取pi精度足够高,面积算出来是477.61平方米。所以取pi=3.1416是足够的,但是3.14就不够了。

换言之,即便最终输出的精度要求较低,也不意味着参与运算的数字,以及运算过程,可以保持较低的精度。在最终输出是8bit RGB的前提下,10bit YUV比起8bit YUV依旧具有精度优势的原因就在这里。事实上,8bit YUV转换后,覆盖的精度大概相当于8bit RGB的26%,而10bit转换后的精度大约可以覆盖97%——你想让你家8bit显示器发挥97%的细腻度么?看10bit吧。

8bit精度不足,主要表现在亮度较低的区域,容易形成色带:

在这里插入图片描述
注意这图右边那一圈圈跟波浪一样的效果。这就是颜色精度不足的表现。如果你看的是pdf版本,你还应该可以注意到左边背景那网格状的效果,那也是精度不足造成的颜色过渡不自然。

10bit的优势不只在于显示精度的提高,在提高视频压缩率,减少失真方面,相对8bit也有优势。这方面就不展开了。

2. 色度半采样

2.1 420采样

在YUV模型的应用中,Y和UV的重要性是不等同的。图像视频的实际储存和传输中,通常将Y以全分辨率记录,UV以减半甚至1/4的分辨率记录。这个手段被称为色度半采样(Chroma Sub-Sampling)。色度半采样可以有效减少传输带宽,和加大UV平面的压缩率。

我们平常的视频,最常见的是420采样。这种采样是Y保留全部,UV只以(1/2) x (1/2)的分辨率记录。比如说1920x1080的视频,其实只有亮度平面是1920x1080。两个色度平面都只有960x540的分辨率。

2.2 444采样

当然了,你也可以选择不做缩减。这种称为444采样。YUV三个平面全是满分辨率。

在做YUV->RGB的时候,首先需要将UV分辨率拉升到Y的分辨率(madVR中允许自定义算法,在Chroma Upscaling当中),然后再转换到RGB。做RGB->YUV的转换,也是先转换到444,再将UV分辨率降低。

一般能拿到的片源,包括所有蓝光原盘,都是420采样的。所以成品一般也保留420采样。(除非需要在444/RGB平面下做处理,这样成品有必要保留444采样)

3.空间上的低频与高频:平面,纹理和线条

在视频处理中,空间(spatial)的概念指的是一帧图片以内。跟时间(temporal)相对;时间的概念就强调帧与帧之间的变换。

于是我们重新来看这张亮度的图:

在这里插入图片描述

亮度变化较快,变动幅度大的区域,我们称之为高频区域。否则,亮度变化缓慢且不明显的区域,我们称为低频区域。

图中的蓝圈就是一块典型的低频区域。亮度几乎没有变化
绿圈中,亮度呈现跳跃式的突变,通常是一般亮度较高,但是线条部分的一排像素亮度明显低于周围,这种高频区域我们称之为线条。
红圈中,亮度频繁变化,幅度有高有低,这种高频区域我们称为纹理。

有时候,线条和纹理统称为线条,平面又叫做非线条。

这是亮度平面。色度平面,高频低频,线条等概念也同样适用,就是描述色度变化的快慢轻重。一般我们所谓的“细节”,就是指图像中的高频信息。

一般来说,一张图的高频信息越多,意味着这张图信息量越大,所需要记录的数据量就越多,编码所需要的运算量也越大。如果一个视频包含的空间性高频信息很多(通俗点说就是每一帧内细节很多),意味着这个视频的空间复杂度很高。

记录一张图片,编码器需要决定给怎样的部分多少码率。码率在一张图内不同部分的分配,叫做码率的空间分配。分配较好的时候,往往整幅图目视观感比较统一;分配不好常见的后果,就是线条纹理尚可;背景平面区域出现大量色带色块;或者背景颜色过渡自然,纹理模糊,线条烂掉。

4. 时间上的低频与高频:动态

在视频处理中,时间(temporal)的概念强调帧与帧之间的变换。跟空间(spatial)相对。

动态的概念无需多解释;就是帧与帧之间图像变化的强弱,变化频率的高低。一段视频如果动态很高,变化剧烈,我们称为时间复杂度较高,时域上的高频信息多。否则如果视频本身舒缓多静态,我们称为时间复杂度低,时域上的低频信息多。

一般来说,一段视频的时域高频信息多,动态的信息量就大,所需要记录的数据量就越多,编码所需要的运算量也越大。但是另一方面,人眼对高速变化的场景,敏感度不如静态的图片来的高(你没有时间去仔细观察细节),所以动态场景的优先度可以低于静态场景。如何权衡以上两点去分配码率,被称为码率的时间分配。分配较好的时候,看视频无论动态还是静态效果都较好;分配不好的时候往往是静态部分看着还行,动态部分糊烂掉;或者动态部分效果过分的好,浪费了大量码率,造成静态部分欠码,瑕疵明显。

转载链接:

https://vcb-s.nmm-hd.org/Twilight%20Council/%5BVCB-Studio%5D%5B%E6%95%99%E7%A8%8B00%5D%E8%A7%86%E9%A2%91%E6%A0%BC%E5%BC%8F%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/%5BVCB-Studio%5D%5B%E6%95%99%E7%A8%8B00%5D%E8%A7%86%E9%A2%91%E6%A0%BC%E5%BC%8F%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.pdf

import os import sys import cv2 import numpy as np import subprocess import tempfile import shutil import random import wave import struct from PIL import Image, ImageDraw, ImageFont # 尝试导入PyQt5,如果失败则提供友好的错误信息 try: from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QMessageBox, QProgressBar, QGroupBox, QTextEdit, QCheckBox, QListWidget, QListWidgetItem, QComboBox) from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtGui import QFont, QPalette, QColor, QIcon except ImportError: print("错误: 需要安装PyQt5库") print("请运行: pip install PyQt5") sys.exit(1) # 尝试导入GPU相关库 try: import pyopencl as cl OPENCL_AVAILABLE = True except ImportError: OPENCL_AVAILABLE = False try: import cupy as cp CUDA_AVAILABLE = True except ImportError: CUDA_AVAILABLE = False # 尝试导入scipy try: import scipy.io.wavfile as wavfile SCIPY_AVAILABLE = True except ImportError: SCIPY_AVAILABLE = False class AudioSteganography: """音频隐写处理类""" @staticmethod def embed_message(audio_path, message, output_path): """使用改进的LSB算法在音频中嵌入消息""" try: # 读取音频文件 with wave.open(audio_path, 'rb') as audio: params = audio.getparams() frames = audio.readframes(audio.getnframes()) # 将音频数据转换为字节数组 audio_data = bytearray(frames) # 将消息转换为二进制 binary_message = ''.join(format(ord(char), '08b') for char in message) binary_message += '00000000' # 添加终止符 # 检查音频容量是否足够 if len(binary_message) > len(audio_data) * 8: raise ValueError("音频文件太小,无法嵌入消息") # 使用改进的LSB算法嵌入消息(每4个样本嵌入1位) sample_interval = 4 # 每4个样本嵌入1位 message_index = 0 for i in range(0, len(audio_data), sample_interval): if message_index >= len(binary_message): break # 修改每个样本的最低有效位 audio_data[i] = (audio_data[i] & 0xFE) | int(binary_message[message_index]) message_index += 1 # 保存带有隐写信息的音频 with wave.open(output_path, 'wb') as output_audio: output_audio.setparams(params) output_audio.writeframes(bytes(audio_data)) return True, "音频隐写成功" except Exception as e: return False, f"音频隐写失败: {str(e)}" @staticmethod def extract_message(audio_path): """从音频中提取隐藏的消息""" try: # 读取音频文件 with wave.open(audio_path, 'rb') as audio: frames = audio.readframes(audio.getnframes()) # 将音频数据转换为字节数组 audio_data = bytearray(frames) # 提取LSB位 binary_message = '' sample_interval = 4 # 与嵌入时保持一致 for i in range(0, len(audio_data), sample_interval): binary_message += str(audio_data[i] & 1) # 将二进制转换为字符 message = '' for i in range(0, len(binary_message), 8): byte = binary_message[i:i+8] if len(byte) < 8: break char = chr(int(byte, 2)) if char == '\0': # 遇到终止符停止 break message += char return True, message except Exception as e: return False, f"消息提取失败: {str(e)}" class VideoProcessor(QThread): progress_updated = pyqtSignal(int) status_updated = pyqtSignal(str) finished = pyqtSignal(bool, str) batch_progress = pyqtSignal(int, int) # 当前处理, 总计 def __init__(self, video_a_path, video_b_paths, output_dir, use_gpu=False, gpu_type="auto"): super().__init__() self.video_a_path = video_a_path self.video_b_paths = video_b_paths self.output_dir = output_dir self.use_gpu = use_gpu self.gpu_type = gpu_type self.temp_dir = tempfile.mkdtemp() self.gpu_context = None self.gpu_queue = None self.gpu_device = None # 初始化GPU环境 if self.use_gpu: self.init_gpu() def init_gpu(self): """初始化GPU环境""" try: if self.gpu_type == "cuda" or (self.gpu_type == "auto" and CUDA_AVAILABLE): # 使用CUDA self.status_updated.emit("初始化CUDA环境...") # 检查可用GPU devices = cp.cuda.runtime.getDeviceCount() if devices > 0: self.status_updated.emit(f"找到 {devices} 个NVIDIA GPU") # 使用第一个可用的GPU cp.cuda.Device(0).use() self.gpu_type = "cuda" return True else: self.status_updated.emit("未找到NVIDIA GPU,尝试使用OpenCL") self.gpu_type = "opencl" if self.gpu_type == "opencl" or (self.gpu_type == "auto" and OPENCL_AVAILABLE): # 使用OpenCL self.status_updated.emit("初始化OpenCL环境...") platforms = cl.get_platforms() # 优先寻找Intel Arc显卡 intel_arc_found = False for platform in platforms: devices = platform.get_devices(device_type=cl.device_type.GPU) for device in devices: device_name = device.name if "Intel" in device_name and ("Arc" in device_name or "A770" in device_name): self.status_updated.emit(f"找到Intel Arc显卡: {device_name}") self.gpu_context = cl.Context([device]) self.gpu_queue = cl.CommandQueue(self.gpu_context) self.gpu_device = device self.gpu_type = "opencl" intel_arc_found = True break if intel_arc_found: break # 如果没有找到Intel Arc,寻找其他GPU if not intel_arc_found: for platform in platforms: devices = platform.get_devices(device_type=cl.device_type.GPU) if devices: self.gpu_context = cl.Context(devices) self.gpu_queue = cl.CommandQueue(self.gpu_context) self.gpu_device = devices[0] self.status_updated.emit(f"找到OpenCL GPU: {devices[0].name}") self.gpu_type = "opencl" return True # 如果没有找到GPU,尝试使用CPU if not intel_arc_found: for platform in platforms: devices = platform.get_devices(device_type=cl.device_type.CPU) if devices: self.gpu_context = cl.Context(devices) self.gpu_queue = cl.CommandQueue(self.gpu_context) self.gpu_device = devices[0] self.status_updated.emit(f"使用OpenCL CPU: {devices[0].name}") self.gpu_type = "opencl" return True self.status_updated.emit("未找到OpenCL设备,将使用CPU") self.use_gpu = False return False else: self.status_updated.emit("未安装GPU支持库,将使用CPU") self.use_gpu = False return False except Exception as e: self.status_updated.emit(f"GPU初始化失败: {str(e)},将使用CPU") self.use_gpu = False return False def run(self): try: # 创建OK文件夹 ok_dir = os.path.join(self.output_dir, "OK") os.makedirs(ok_dir, exist_ok=True) total_videos = len(self.video_b_paths) for idx, video_b_path in enumerate(self.video_b_paths): output_filename = f"output_{os.path.basename(video_b_path).split('.')[0]}.mp4" output_path = os.path.join(ok_dir, output_filename) self.batch_progress.emit(idx + 1, total_videos) self.status_updated.emit(f"处理视频 {idx + 1}/{total_videos}: {os.path.basename(video_b_path)}") # 处理单个视频对 success, message = self.process_single_video(self.video_a_path, video_b_path, output_path) if not success: self.finished.emit(False, f"处理失败: {message}") return self.finished.emit(True, f"批量处理完成!共处理 {total_videos} 个视频,输出保存在 {ok_dir}") except Exception as e: import traceback error_details = traceback.format_exc() self.finished.emit(False, f"处理过程中出现错误: {str(e)}\n详细信息:\n{error_details}") finally: # 清理临时文件 if os.path.exists(self.temp_dir): try: shutil.rmtree(self.temp_dir) except: pass def process_single_video(self, video_a_path, video_b_path, output_path): """处理单个视频对""" try: self.status_updated.emit("开始处理视频...") # 提取音频 self.status_updated.emit("提取音频...") audio_path = self.extract_audio(video_a_path) self.progress_updated.emit(10) # 处理视频A self.status_updated.emit("处理视频A...") a_frames_dir = self.process_video_a(video_a_path) self.progress_updated.emit(30) # 处理视频B self.status_updated.emit("处理视频B...") b_frames_dir = self.process_video_b(video_b_path, len(os.listdir(a_frames_dir))) self.progress_updated.emit(50) # 嵌入隐写 self.status_updated.emit("嵌入隐写信息...") stego_frames_dir = self.embed_steganography(a_frames_dir, b_frames_dir) self.progress_updated.emit(70) # 处理音频并合成最终视频 self.status_updated.emit("处理音频并合成最终视频...") self.process_audio_and_assemble(stego_frames_dir, audio_path, output_path) self.progress_updated.emit(90) # 添加随机元数据 self.status_updated.emit("添加元数据...") self.add_random_metadata(output_path) self.progress_updated.emit(100) return True, "处理完成" except Exception as e: import traceback error_details = traceback.format_exc() return False, f"处理过程中出现错误: {str(e)}\n详细信息:\n{error_details}" def extract_audio(self, video_path): """提取音频并转换为单声道""" audio_path = os.path.join(self.temp_dir, "audio.wav") # 使用ffmpeg提取音频并转换为单声道 cmd = [ 'ffmpeg', '-i', video_path, '-vn', '-ac', '1', '-ar', '44100', '-y', audio_path ] try: subprocess.run(cmd, check=True, capture_output=True) return audio_path except subprocess.CalledProcessError as e: # 如果提取失败,创建一个默认的音频文件 self.status_updated.emit("警告: 无法提取音频,将创建默认音频") try: # 创建一个1秒的静音音频 sample_rate = 44100 duration = 10 # 默认10秒 # 获取视频时长 probe_cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', video_path] result = subprocess.run(probe_cmd, capture_output=True, text=True) if result.returncode == 0: duration = float(result.stdout.strip()) # 生成静音 t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False) silence = np.zeros_like(t) # 保存为WAV文件 if SCIPY_AVAILABLE: wavfile.write(audio_path, sample_rate, silence.astype(np.float32)) else: # 使用wave模块创建WAV文件 with wave.open(audio_path, 'wb') as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) wav_file.setframerate(sample_rate) # 将静音数据转换为字节 max_amplitude = 32767 # 16位有符号整数的最大值 for sample in silence: data = struct.pack('<h', int(sample * max_amplitude)) wav_file.writeframesraw(data) return audio_path except Exception as e2: self.status_updated.emit(f"创建默认音频失败: {str(e2)}") # 最后的手段,创建一个空的音频文件 open(audio_path, 'a').close() return audio_path def process_video_a(self, video_path): """处理视频A""" # 创建输出目录 output_dir = os.path.join(self.temp_dir, "video_a_frames") os.makedirs(output_dir, exist_ok=True) # 获取视频信息 cap = cv2.VideoCapture(video_path) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) fps = cap.get(cv2.CAP_PROP_FPS) width, height = 1080, 2336 # 生成不可见黑色扰动背景 for i in range(total_frames): # 创建带有轻微扰动的黑色背景 background = np.random.randint(0, 3, (height, width, 3), dtype=np.uint8) # 读取原始帧 ret, frame = cap.read() if not ret: break # 调整原始帧大小并居中放置 h, w = frame.shape[:2] scale = min((width-10)/w, (height-10)/h) new_w, new_h = int(w * scale), int(h * scale) resized_frame = cv2.resize(frame, (new_w, new_h)) # 将调整后的帧放置在背景上,设置不透明度为98% x_offset = (width - new_w) // 2 y_offset = (height - new_h) // 2 # 创建叠加层 overlay = background.copy() roi = overlay[y_offset:y_offset+new_h, x_offset:x_offset+new_w] # 使用加权叠加实现98%不透明度 cv2.addWeighted(resized_frame, 0.98, roi, 0.02, 0, roi) # 保存帧 cv2.imwrite(os.path.join(output_dir, f"frame_{i:06d}.png"), overlay) # 更新进度 if i % 10 == 0: self.status_updated.emit(f"处理视频A帧: {i}/{total_frames}") cap.release() return output_dir def process_video_b(self, video_path, total_frames_needed): """处理视频B""" # 创建输出目录 output_dir = os.path.join(self.temp_dir, "video_b_frames") os.makedirs(output_dir, exist_ok=True) # 获取视频B的信息 cap_b = cv2.VideoCapture(video_path) total_frames_b = int(cap_b.get(cv2.CAP_PROP_FRAME_COUNT)) fps_b = cap_b.get(cv2.CAP_PROP_FPS) width, height = 1080, 2336 # 计算需要从视频B中提取的帧 start_frame = 0 if total_frames_b > total_frames_needed: start_frame = random.randint(0, total_frames_b - total_frames_needed) # 生成不可见黑色扰动背景并处理视频B for i in range(total_frames_needed): # 创建带有轻微扰动的黑色背景 background = np.random.randint(0, 3, (height, width, 3), dtype=np.uint8) # 读取原始帧(从适当的位置) frame_idx = min(start_frame + i, total_frames_b - 1) cap_b.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) ret, frame = cap_b.read() if not ret: break # 调整原始帧大小并居中放置 h, w = frame.shape[:2] scale = min((width-10)/w, (height-10)/h) new_w, new_h = int(w * scale), int(h * scale) resized_frame = cv2.resize(frame, (new_w, new_h)) # 将调整后的帧放置在背景上 x_offset = (width - new_w) // 2 y_offset = (height - new_h) // 2 # 创建叠加层 overlay = background.copy() roi = overlay[y_offset:y_offset+new_h, x_offset:x_offset+new_w] # 叠加帧 cv2.addWeighted(resized_frame, 1.0, roi, 0.0, 0, roi) # 保存帧 cv2.imwrite(os.path.join(output_dir, f"frame_{i:06d}.png"), overlay) # 更新进度 if i % 10 == 0: self.status_updated.emit(f"处理视频B帧: {i}/{total_frames_needed}") cap_b.release() return output_dir def dct_embed_gpu(self, carrier, secret): """使用GPU加速的DCT隐写""" if self.gpu_type == "cuda" and CUDA_AVAILABLE and self.use_gpu: # 使用CuPy进行GPU加速 carrier_gpu = cp.asarray(carrier) secret_gpu = cp.asarray(secret) # 转换为YUV颜色空间 carrier_yuv = cp.zeros_like(carrier_gpu) secret_yuv = cp.zeros_like(secret_gpu) # RGBYUV转换矩阵 transform = cp.array([[0.299, 0.587, 0.114], [-0.14713, -0.28886, 0.436], [0.615, -0.51499, -0.10001]]) # 应用转换矩阵 for i in range(carrier_gpu.shape[0]): for j in range(carrier_gpu.shape[1]): carrier_yuv[i, j] = cp.dot(transform, carrier_gpu[i, j]) secret_yuv[i, j] = cp.dot(transform, secret_gpu[i, j]) # 只使用Y通道进行DCT变换 carrier_y = carrier_yuv[:,:,0].astype(cp.float32) secret_y = secret_yuv[:,:,0].astype(cp.float32) # 对载体和秘密图像进行DCT变换 carrier_dct = cp.fft.dct(cp.fft.dct(carrier_y, axis=0), axis=1) secret_dct = cp.fft.dct(cp.fft.dct(secret_y, axis=0), axis=1) # 嵌入强度因子 alpha = 0.03 # 在DCT域中嵌入秘密图像 stego_dct = carrier_dct + alpha * secret_dct # 进行逆DCT变换 stego_y = cp.fft.idct(cp.fft.idct(stego_dct, axis=1), axis=0) # 将结果放回YUV图像中 stego_yuv = carrier_yuv.copy() stego_yuv[:,:,0] = stego_y # YUVRGB转换矩阵 inv_transform = cp.linalg.inv(transform) # 转换回RGB颜色空间 stego_bgr = cp.zeros_like(stego_yuv) for i in range(stego_yuv.shape[0]): for j in range(stego_yuv.shape[1]): stego_bgr[i, j] = cp.dot(inv_transform, stego_yuv[i, j]) return cp.clip(stego_bgr, 0, 255).astype(cp.uint8).get() elif self.gpu_type == "opencl" and OPENCL_AVAILABLE and self.use_gpu and self.gpu_context: # 使用OpenCL进行GPU加速,特别优化Intel Arc显卡 return self.dct_embed_opencl(carrier, secret) else: # 使用CPU版本 return self.dct_embed_cpu(carrier, secret) def dct_embed_opencl(self, carrier, secret): """使用OpenCL进行DCT隐写,特别优化Intel Arc显卡""" try: # 将图像转换为YUV颜色空间 carrier_yuv = cv2.cvtColor(carrier, cv2.COLOR_BGR2YUV) secret_yuv = cv2.cvtColor(secret, cv2.COLOR_BGR2YUV) # 只使用Y通道进行DCT变换 carrier_y = carrier_yuv[:,:,0].astype(np.float32) secret_y = secret_yuv[:,:,0].ast(np.float32) # 创建OpenCL缓冲区 carrier_buffer = cl.Buffer(self.gpu_context, cl.mem_flags.READ_ONLY | cl.mem_flags.COPY_HOST_PTR, hostbuf=carrier_y) secret_buffer = cl.Buffer(self.gpu_context, cl.mem_flags.READ_ONLY | cl.mem_flags.COPY_HOST_PTR, hostbuf=secret_y) # 创建输出缓冲区 stego_dct_buffer = cl.Buffer(self.gpu_context, cl.mem_flags.WRITE_ONLY, carrier_y.nbytes) stego_y_buffer = cl.Buffer(self.gpu_context, cl.mem_flags.WRITE_ONLY, carrier_y.nbytes) # 编译OpenCL程序 - 修复了内核代码中的语法错误 dct_program = cl.Program(self.gpu_context, """ __kernel void dct_embed(__global float* carrier, __global float* secret, __global float* stego_dct, __global float* stego_y, float alpha, int width, int height) { int x = get_global_id(0); int y = get_global_id(1); int idx = y * width + x; // DCT变换 (简化实现) // 在实际应用中,这里应该实现完整的2D DCT算法 // 这里使用简化的DCT近似 float dct_carrier = carrier[idx] * cos((2*x+1)*y*M_PI/(2*width)); float dct_secret = secret[idx] * cos((2*x+1)*y*M_PI/(2*width)); // 嵌入秘密图像 stego_dct[idx] = dct_carrier + alpha * dct_secret; // 逆DCT变换 stego_y[idx] = stego_dct[idx] * cos((2*x+1)*y*M_PI/(2*width)); } """).build() # 设置内核参数 width, height = carrier_y.shape[1], carrier_y.shape[0] alpha = np.float32(0.03) # 执行内核 dct_program.dct_embed(self.gpu_queue, carrier_y.shape, None, carrier_buffer, secret_buffer, stego_dct_buffer, stego_y_buffer, alpha, np.int32(width), np.int32(height)) # 读取结果 stego_y = np.empty_like(carrier_y) cl.enqueue_copy(self.gpu_queue, stego_y, stego_y_buffer) # 将结果放回YUV图像中 stego_yuv = carrier_yuv.copy() stego_yuv[:,:,0] = stego_y # 转换回BGR颜色空间 stego_bgr = cv2.cvtColor(stego_yuv, cv2.COLOR_YUV2BGR) return np.clip(stego_bgr, 0, 255).astype(np.uint8) except Exception as e: self.status_updated.emit(f"OpenCL处理失败: {str(e)},将使用CPU") return self.dct_embed_cpu(carrier, secret) def dct_embed_cpu(self, carrier, secret): """在DCT域中嵌入秘密图像(CPU版本)""" # 将图像转换为YUV颜色空间 carrier_yuv = cv2.cvtColor(carrier, cv2.COLOR_BGR2YUV) secret_yuv = cv2.cvtColor(secret, cv2.COLOR_BGR2YUV) # 只使用Y通道进行DCT变换 carrier_y = carrier_yuv[:,:,0].astype(np.float32) secret_y = secret_yuv[:,:,0].astype(np.float32) # 对载体和秘密图像进行DCT变换 carrier_dct = cv2.dct(carrier_y) secret_dct = cv2.dct(secret_y) # 嵌入强度因子 alpha = 0.03 # 在DCT域中嵌入秘密图像 stego_dct = carrier_dct + alpha * secret_dct # 进行逆DCT变换 stego_y = cv2.idct(stego_dct) # 将结果放回YUV图像中 stego_yuv = carrier_yuv.copy() stego_yuv[:,:,0] = stego_y # 转换回BGR颜色空间 stego_bgr = cv2.cvtColor(stego_yuv, cv2.COLOR_YUV2BGR) return np.clip(stego_bgr, 0, 255).ast(np.uint8) def embed_steganography(self, a_frames_dir, b_frames_dir): """嵌入隐写信息""" # 创建输出目录 output_dir = os.path.join(self.temp_dir, "stego_frames") os.makedirs(output_dir, exist_ok=True) # 获取帧列表 a_frames = sorted([f for f in os.listdir(a_frames_dir) if f.endswith('.png')]) total_frames = len(a_frames) for i, frame_name in enumerate(a_frames): # 读取A视频帧 carrier_frame = cv2.imread(os.path.join(a_frames_dir, frame_name)) # 读取B视频帧 secret_frame = cv2.imread(os.path.join(b_frames_dir, frame_name)) # 调整秘密图像大小以匹配载体图像 secret_frame = cv2.resize(secret_frame, (carrier_frame.shape[1], carrier_frame.shape[0])) # 在DCT域中嵌入秘密图像 if self.use_gpu: stego_frame = self.dct_embed_gpu(carrier_frame, secret_frame) else: stego_frame = self.dct_embed_cpu(carrier_frame, secret_frame) # 添加数字水印(版权保护)- 透明度极低,人眼几乎不可见 stego_frame = self.add_watermark(stego_frame) # 保存处理后的帧 cv2.imwrite(os.path.join(output_dir, frame_name), stego_frame) # 更新进度 if i % 10 == 0: self.status_updated.emit(f"嵌入隐写帧: {i}/{total_frames}") return output_dir def add_watermark(self, image): """添加数字水印 - 透明度极低,人眼几乎不可见""" # 将OpenCV图像转换为PIL图像 pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) # 创建一个绘图对象 draw = ImageDraw.Draw(pil_image, 'RGBA') # 使用默认字体 try: font = ImageFont.load_default() # 尝试加载系统字体 try: font = ImageFont.truetype("arial.ttf", 20) except: pass except: pass # 添加水印文本 - 使用极低的透明度 (约0.5%) watermark_text = "Copyright Protected" # 获取文本尺寸 try: # 对于较新版本的Pillow bbox = draw.textbbox((0, 0), watermark_text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] except: # 对于较旧版本的Pillow try: text_width, text_height = draw.textsize(watermark_text, font=font) except: # 如果所有方法都失败,使用估计值 text_width, text_height = 150, 20 # 在多个位置添加水印 positions = [ (10, 10), # 左上角 (image.shape[1] - text_width - 10, 10), # 右上角 (10, image.shape[0] - text_height - 10), # 左下角 (image.shape[1] - text_width - 10, image.shape[0] - text_height - 10) # 右下角 ] for position in positions: # 添加文本 - 使用极低的透明度 (约0.5%) draw.text(position, watermark_text, (255, 255, 255, 2), font=font) # 透明度从5降低到2 # 转换回OpenCV图像 return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) def process_audio_and_assemble(self, frames_dir, audio_path, output_path): """处理音频并合成最终视频""" # 生成随机噪声音频 noise_audio_path = os.path.join(self.temp_dir, "noise_audio.wav") # 获取音频信息 try: cmd = ['ffprobe', '-i', audio_path, '-show_entries', 'format=duration', '-v', 'quiet', '-of', 'csv=p=0'] result = subprocess.run(cmd, capture_output=True, text=True) duration = float(result.stdout.strip()) # 生成随机噪声(极低音量) sample_rate = 44100 t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False) noise = 0.0005 * np.random.randn(len(t)) # 极低音量噪声 # 保存噪声音频 if SCIPY_AVAILABLE: wavfile.write(noise_audio_path, sample_rate, noise.astype(np.float32)) else: # 使用wave模块创建WAV文件 with wave.open(noise_audio_path, 'wb') as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) wav_file.setframerate(sample_rate) # 将噪声数据转换为字节 max_amplitude = 32767 # 16位有签名整数的最大值 for sample in noise: data = struct.pack('<h', int(sample * max_amplitude)) wav_file.writeframesraw(data) except: # 如果生成噪声失败,创建一个空的音频文件 open(noise_audio_path, 'a').close() # 在音频中嵌入隐写信息 stego_audio_path = os.path.join(self.temp_dir, "stego_audio.wav") message = "HiddenSteganoMessage2023" success, msg = AudioSteganography.embed_message(audio_path, message, stego_audio_path) if not success: self.status_updated.emit(f"音频隐写警告: {msg}") stego_audio_path = audio_path # 使用原始音频 # 合并音频(左声道为隐写音频,右声道为噪声) mixed_audio_path = os.path.join(self.temp_dir, "mixed_audio.wav") cmd = [ 'ffmpeg', '-y', '-i', stego_audio_path, '-i', noise_audio_path, '-filter_complex', '[0:a][1:a]amerge=inputs=2,pan=stereo|c0<c0+c1|c1<c2+c3[aout]', '-map', '[aout]', mixed_audio_path ] try: subprocess.run(cmd, check=True, capture_output=True) except: # 如果合并失败,使用隐写音频 mixed_audio_path = stego_audio_path # 使用ffmpeg从帧序列创建视频 frame_pattern = os.path.join(frames_dir, "frame_%06d.png") # 生成随机比特率 (5000-7000kbps) bitrate = random.randint(5000, 7000) self.status_updated.emit(f"使用比特率: {bitrate}kbps") # 使用H.264编码和指定的参数 cmd = [ 'ffmpeg', '-y', '-framerate', '30', '-i', frame_pattern, '-i', mixed_audio_path, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', # 较低CRF值以获得更高质量 '-preset', 'fast' if not self.use_gpu else 'medium', '-b:v', f'{bitrate}k', '-maxrate', f'{bitrate + 1000}k', '-bufsize', f'{bitrate * 2}k', '-s', '1080x2336', '-c:a', 'aac', '-b:a', '128k', '-metadata', 'title=Processed Video', output_path ] try: subprocess.run(cmd, check=True, capture_output=True) except subprocess.CalledProcessError as e: # 如果添加音频失败,尝试创建没有音频的视频 cmd_no_audio = [ 'ffmpeg', '-y', '-framerate', '30', '-i', frame_pattern, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', '-preset', 'fast' if not self.use_gpu else 'medium', '-b:v', f'{bitrate}k', '-maxrate', f'{bitrate + 1000}k', '-bufsize', f'{bitrate * 2}k', '-s', '1080x2336', '-metadata', 'title=Processed Video', output_path ] subprocess.run(cmd_no_audio, check=True, capture_output=True) # 添加声纹去重处理(简化实现) self.add_voiceprint_processing(output_path) def add_voiceprint_processing(self, video_path): """添加声纹去重处理(简化实现)""" # 在实际应用中,这里应该实现复杂的声纹处理算法 # 这里只是一个简单的示例,添加一些元数据标记 temp_output = video_path + ".temp.mp4" cmd = [ 'ffmpeg', '-i', video_path, '-metadata', 'voiceprint_processed=true', '-metadata', 'voiceprint_hash=' + ''.join(random.choices('0123456789abcdef', k=32)), '-codec', 'copy', '-y', temp_output ] try: subprocess.run(cmd, check=True, capture_output=True) # 替换原文件 os.replace(temp_output, video_path) except: # 如果添加元数据失败,保持原文件不变 if os.path.exists(temp_output): os.remove(temp_output) def add_random_metadata(self, video_path): """添加随机元数据到视频文件""" metadata_options = { 'creation_time': ['2023-01-15T10:30:00', '2023-02-20T14:45:30', '2023-03-10T09:15:45'], 'location': ['New York, USA', 'London, UK', 'Tokyo, Japan', 'Paris, France'], 'device': ['iPhone 14 Pro', 'Samsung Galaxy S23', 'Canon EOS R5', 'Sony A7IV'], 'description': ['Beautiful landscape', 'Urban exploration', 'Nature documentary', 'Travel vlog'], 'encoder': ['H.264', 'HEVC', 'AV1', 'VP9'] } # 随机选择元数据 selected_metadata = { 'creation_time': random.choice(metadata_options['creation_time']), 'location': random.choice(metadata_options['location']), 'device': random.choice(metadata_options['device']), 'description': random.choice(metadata_options['description']), 'encoder': random.choice(metadata_options['encoder']) } # 创建临时输出文件 temp_output = video_path + ".temp.mp4" # 使用ffmpeg添加元数据 cmd = [ 'ffmpeg', '-i', video_path, '-metadata', f'creation_time={selected_metadata["creation_time"]}', '-metadata', f'location={selected_metadata["location"]}', '-metadata', f'device={selected_metadata["device"]}', '-metadata', f'description={selected_metadata["description"]}', '-metadata', f'encoder={selected_metadata["encoder"]}', '-codec', 'copy', '-y', temp_output ] try: subprocess.run(cmd, check=True, capture_output=True) # 替换原文件 os.replace(temp_output, video_path) except: # 如果添加元数据失败,保持原文件不变 if os.path.exists(temp_output): os.remove(temp_output) class VideoSteganographyApp(QMainWindow): def __init__(self): super().__init__() self.video_a_path = "" self.video_b_paths = [] self.output_dir = "" self.use_gpu = False self.gpu_type = "auto" self.initUI() def initUI(self): self.setWindowTitle('视频隐写处理工具 - 最终版') self.setGeometry(100, 100, 900, 800) # 设置应用程序图标 if hasattr(sys, '_MEIPASS'): # 打包后的路径 icon_path = os.path.join(sys._MEIPASS, 'app_icon.ico') else: # 开发时的路径 icon_path = 'app_icon.ico' if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) # 设置暗色主题样式 self.setStyleSheet(""" QMainWindow { background-color: #2b2b2b; color: #cccccc; } QGroupBox { font-weight: bold; border: 2px solid #444444; border-radius: 5px; margin-top: 1ex; padding-top: 10px; background-color: #3c3c3c; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #ffffff; } QPushButton { background-color: #4CAF50; border: none; color: white; padding: 10px 20px; text-align: center; text-decoration: none; font-size: 16px; margin: 4px 2px; border-radius: 5px; } QPushButton:hover { background-color: #45a049; } QPushButton:disabled { background-color: #555555; } QPushButton:checked { background-color: #2196F3; } QLabel { padding: 5px; color: #cccccc; } QProgressBar { border: 2px solid #444444; border-radius: 5px; text-align: center; background-color: #3c3c3c; } QProgressBar::chunk { background-color: #4CAF50; width: 10px; } QCheckBox { padding: 5px; color: #cccccc; } QCheckBox::indicator { width: 15px; height: 15px; } QCheckBox::indicator:unchecked { border: 1px solid #555555; background-color: #3c3c3c; } QCheckBox::indicator:checked { border: 1px solid #555555; background-color: #4CAF50; } QListWidget { border: 1px solid #444444; border-radius: 3px; background-color: #3c3c3c; color: #cccccc; } QComboBox { border: 1px solid #444444; border-radius: 3px; padding: 5px; background-color: #3c3c3c; color: #cccccc; } QComboBox QAbstractItemView { border: 1px solid #444444; background-color: #3c3c3c; color: #cccccc; selection-background-color: #4CAF50; } QTextEdit { background-color: #3c3c3c; color: #cccccc; border: 1px solid #444444; border-radius: 3px; } """) central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) # 标题 title_label = QLabel("视频隐写处理工具 - 最终版") title_label.setAlignment(Qt.AlignCenter) title_font = QFont() title_font.setPointSize(20) title_font.setBold(True) title_label.setFont(title_font) layout.addWidget(title_label) # 加速选项 acceleration_group = QGroupBox("加速选项") acceleration_layout = QHBoxLayout() self.gpu_checkbox = QCheckBox("使用GPU加速") self.gpu_checkbox.setChecked(False) self.gpu_checkbox.stateChanged.connect(self.toggle_gpu_acceleration) acceleration_layout.addWidget(self.gpu_checkbox) self.gpu_type_combo = QComboBox() self.gpu_type_combo.addItems(["自动检测", "NVIDIA CUDA", "OpenCL (Intel/AMD)"]) self.gpu_type_combo.currentIndexChanged.connect(self.change_gpu_type) acceleration_layout.addWidget(QLabel("GPU类型:")) acceleration_layout.addWidget(self.gpu_type_combo) acceleration_group.setLayout(acceleration_layout) layout.addWidget(acceleration_group) # 视频A选择区域 video_a_group = QGroupBox("视频A (主视频)") video_a_layout = QVBoxLayout() self.video_a_label = QLabel("未选择文件") video_a_layout.addWidget(self.video_a_label) video_a_btn = QPushButton("选择视频A") video_a_btn.clicked.connect(self.select_video_a) video_a_layout.addWidget(video_a_btn) video_a_group.setLayout(video_a_layout) layout.addWidget(video_a_group) # 视频B选择区域 video_b_group = QGroupBox("视频B (隐写视频 - 可多选)") video_b_layout = QVBoxLayout() self.video_b_list = QListWidget() video_b_layout.addWidget(self.video_b_list) video_b_btn_layout = QHBoxLayout() add_video_b_btn = QPushButton("添加视频B") add_video_b_btn.clicked.connect(self.add_video_b) video_b_btn_layout.addWidget(add_video_b_btn) remove_video_b_btn = QPushButton("移除选中") remove_video_b_btn.clicked.connect(self.remove_video_b) video_b_btn_layout.addWidget(remove_video_b_btn) clear_video_b_btn = QPushButton("清空列表") clear_video_b_btn.clicked.connect(self.clear_video_b) video_b_btn_layout.addWidget(clear_video_b_btn) video_b_layout.addLayout(video_b_btn_layout) video_b_group.setLayout(video_b_layout) layout.addWidget(video_b_group) # 输出选择区域 output_group = QGroupBox("输出设置") output_layout = QVBoxLayout() self.output_label = QLabel("未选择输出目录") output_layout.addWidget(self.output_label) output_btn = QPushButton("选择输出目录") output_btn.clicked.connect(self.select_output) output_layout.addWidget(output_btn) output_group.setLayout(output_layout) layout.addWidget(output_group) # 进度区域 progress_group = QGroupBox("处理进度") progress_layout = QVBoxLayout() self.batch_label = QLabel("准备就绪") progress_layout.addWidget(self.batch_label) self.status_label = QLabel("等待开始...") progress_layout.addWidget(self.status_label) self.progress_bar = QProgressBar() self.progress_bar.setValue(0) progress_layout.addWidget(self.progress_bar) progress_group.setLayout(progress_layout) layout.addWidget(progress_group) # 处理按钮 self.process_btn = QPushButton("开始批量处理") self.process_btn.clicked.connect(self.process_videos) self.process_btn.setEnabled(False) layout.addWidget(self.process_btn) # 日志区域 log_group = QGroupBox("处理日志") log_layout = QVBoxLayout() self.log_text = QTextEdit() self.log_text.setReadOnly(True) log_layout.addWidget(self.log_text) log_group.setLayout(log_layout) layout.addWidget(log_group) # 初始化日志 self.log_text.append("应用程序已启动") self.log_text.append(f"CUDA可用: {CUDA_AVAILABLE}") self.log_text.append(f"OpenCL可用: {OPENCL_AVAILABLE}") self.log_text.append(f"Scipy可用: {SCIPY_AVAILABLE}") def toggle_gpu_acceleration(self, state): self.use_gpu = (state == Qt.Checked) if self.use_gpu: self.log_text.append("已启用GPU加速") else: self.log_text.append("已禁用GPU加速,使用CPU处理") def change_gpu_type(self, index): if index == 0: self.gpu_type = "auto" self.log_text.append("GPU类型: 自动检测") elif index == 1: self.gpu_type = "cuda" self.log_text.append("GPU类型: NVIDIA CUDA") elif index == 2: self.gpu_type = "opencl" self.log_text.append("GPU类型: OpenCL (Intel/AMD)") def select_video_a(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择视频A文件", "", "视频文件 (*.mp4 *.avi *.mov *.mkv)" ) if file_path: self.video_a_path = file_path self.video_a_label.setText(f"已选择: {os.path.basename(file_path)}") self.log_text.append(f"已选择视频A: {file_path}") self.check_ready() def add_video_b(self): file_paths, _ = QFileDialog.getOpenFileNames( self, "选择视频B文件", "", "视频文件 (*.mp4 *.avi *.mov *.mkv)" ) if file_paths: for file_path in file_paths: if file_path not in self.video_b_paths: self.video_b_paths.append(file_path) self.video_b_list.addItem(os.path.basename(file_path)) self.log_text.append(f"已添加视频B: {file_path}") self.check_ready() def remove_video_b(self): selected_items = self.video_b_list.selectedItems() for item in selected_items: index = self.video_b_list.row(item) removed_path = self.video_b_paths.pop(index) self.video_b_list.takeItem(index) self.log_text.append(f"已移除视频B: {removed_path}") self.check_ready() def clear_video_b(self): self.video_b_paths.clear() self.video_b_list.clear() self.log_text.append("已清空视频B列表") self.check_ready() def select_output(self): dir_path = QFileDialog.getExistingDirectory( self, "选择输出目录" ) if dir_path: self.output_dir = dir_path self.output_label.setText(f"输出目录: {dir_path}") self.log_text.append(f"已选择输出目录: {dir_path}") self.check_ready() def check_ready(self): if self.video_a_path and self.video_b_paths and self.output_dir: self.process_btn.setEnabled(True) else: self.process_btn.setEnabled(False) def process_videos(self): self.process_btn.setEnabled(False) self.log_text.append("开始批量处理视频...") self.processor = VideoProcessor( self.video_a_path, self.video_b_paths, self.output_dir, self.use_gpu, self.gpu_type ) self.processor.progress_updated.connect(self.update_progress) self.processor.status_updated.connect(self.update_status) self.processor.finished.connect(self.processing_finished) self.processor.batch_progress.connect(self.update_batch_progress) self.processor.start() def update_progress(self, value): self.progress_bar.setValue(value) def update_status(self, message): self.status_label.setText(message) self.log_text.append(message) def update_batch_progress(self, current, total): self.batch_label.setText(f"处理进度: {current}/{total}") self.log_text.append(f"开始处理第 {current} 个视频,共 {total} 个") def processing_finished(self, success, message): self.process_btn.setEnabled(True) self.status_label.setText("处理完成" if success else "处理失败") self.log_text.append(message) if success: QMessageBox.information(self, "成功", message) else: QMessageBox.warning(self, "错误", message) def main(): app = QApplication(sys.argv) # 设置应用程序样式为Fusion,支持暗色主题 app.setStyle('Fusion') # 设置调色板为暗色主题 palette = QPalette() palette.setColor(QPalette.Window, QColor(43, 43, 43)) palette.setColor(QPalette.WindowText, Qt.white) palette.setColor(QPalette.Base, QColor(25, 25, 25)) palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) palette.setColor(QPalette.ToolTipBase, Qt.white) palette.setColor(QPalette.ToolTipText, Qt.white) palette.setColor(QPalette.Text, Qt.white) palette.setColor(QPalette.Button, QColor(53, 53, 53)) palette.setColor(QPalette.ButtonText, Qt.white) palette.setColor(QPalette.BrightText, Qt.red) palette.setColor(QPalette.Link, QColor(42, 130, 218)) palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) palette.setColor(QPalette.HighlightedText, Qt.black) app.setPalette(palette) window = VideoSteganographyApp() window.show() sys.exit(app.exec_()) if __name__ == '__main__': main() 生成的软件提示: 处理失败: 处理过程中出现错误: 'numpy.ndarray' object has no attribute 'ast' 详细信息: Traceback (most recent call last): File "main.py", line 278, in process_single_video File "main.py", line 642, in embed_steganography File "main.py", line 616, in dct_embed_cpu AttributeError: 'numpy.ndarray' object has no attribute 'ast' 请修正一下
最新发布
08-22
import sys import os import tkinter as tk from tkinter import ttk, filedialog, messagebox import cv2 import numpy as np import random from datetime import datetime, timedelta import threading import subprocess import shutil # 设置DPI感知,确保在高分辨率屏幕上显示正常 try: from ctypes import windll windll.shcore.SetProcessDpiAwareness(1) except: pass # 尝试导入可选依赖 MOVIEPY_AVAILABLE = False PSUTIL_AVAILABLE = False # 处理打包后的导入问题 if getattr(sys, 'frozen', False): # 运行在打包环境中 base_path = sys._MEIPASS # 添加可能的包路径 for package in ['moviepy', 'imageio', 'imageio_ffmpeg', 'psutil']: package_path = os.path.join(base_path, package) if os.path.exists(package_path): sys.path.insert(0, package_path) else: # 正常运行时 base_path = os.path.dirname(__file__) try: from moviepy.editor import VideoFileClip, AudioFileClip MOVIEPY_AVAILABLE = True except ImportError as e: print(f"MoviePy import error: {e}") try: import psutil PSUTIL_AVAILABLE = True except ImportError: pass # 全局变量 stop_processing = False # 定义核心处理函数 def add_invisible_overlay(frame, strength): """核心功能:添加全透明扰动层(对抗哈希检测)""" # 将强度从0-100映射到更合理的扰动范围 (1-5) overlay_strength = strength / 100.0 * 4 + 1 # 1 to 5 # 1. 创建一个和帧大小一样的随机噪声图像 noise = np.random.randn(*frame.shape).astype(np.float32) * overlay_strength # 2. 将噪声加到原帧上 new_frame = frame.astype(np.float32) + noise # 3. 确保像素值在0-255之间 new_frame = np.clip(new_frame, 0, 255).astype(np.uint8) return new_frame def resize_with_padding(frame, target_width=720, target_height=1560): """将帧调整为目标分辨率,保持宽高比,不足部分用黑色填充""" # 获取原始尺寸 h, w = frame.shape[:2] # 计算缩放比例 scale = target_width / w new_h = int(h * scale) # 如果缩放后的高度超过目标高度,则按高度缩放 if new_h > target_height: scale = target_height / h new_w = int(w * scale) resized = cv2.resize(frame, (new_w, target_height)) else: resized = cv2.resize(frame, (target_width, new_h)) # 创建目标画布(黑色) canvas = np.zeros((target_height, target_width, 3), dtype=np.uint8) # 计算放置位置(居中) y_offset = (target_height - resized.shape[0]) // 2 x_offset = (target_width - resized.shape[1]) // 2 # 将缩放后的图像放到画布上 canvas[y_offset:y_offset+resized.shape[0], x_offset:x_offset+resized.shape[1]] = resized # 在黑色区域添加不可见的随机噪声(亮度值0-5) black_areas = np.where(canvas == 0) if len(black_areas[0]) > 0: # 只对黑色区域添加噪声 noise = np.random.randint(0, 6, size=black_areas[0].shape, dtype=np.uint8) for i in range(3): # 对RGB三个通道 canvas[black_areas[0], black_areas[1], i] = noise return canvas def generate_random_metadata(): """生成随机的元数据""" # 随机设备型号列表 devices = [ "iPhone15,3", "iPhone15,2", "iPhone14,2", "iPhone14,1", "SM-G998B", "SM-G996B", "SM-G781B", "Mi 11 Ultra", "Mi 10", "Redmi Note 10 Pro" ] # 随机应用程序列表 apps = [ "Wxmm_9020230808", "Wxmm_9020230701", "Wxmm_9020230605", "LemonCamera_5.2.1", "CapCut_9.5.0", "VivaVideo_9.15.5" ] # 随机生成创建时间(最近30天内) now = datetime.now() random_days = random.randint(0, 30) random_hours = random.randint(0, 23) random_minutes = random.randint(0, 59) random_seconds = random.randint(0, 59) creation_time = now - timedelta(days=random_days, hours=random_hours, minutes=random_minutes, seconds=random_seconds) return { "device_model": random.choice(devices), "writing_application": random.choice(apps), "creation_time": creation_time.strftime("%Y-%m-%dT%H:%M:%S"), "title": f"Video_{random.randint(10000, 99999)}", "artist": "Mobile User", "compatible_brands": "isom,iso2,avc1,mp41", "major_brand": "isom" } def corrupt_metadata(input_path, output_path, custom_metadata=None, gpu_type="cpu"): """使用FFmpeg深度修改元数据""" if custom_metadata is None: custom_metadata = generate_random_metadata() # 根据GPU类型设置编码器 if gpu_type == "nvidia": video_encoder = "h264_nvenc" elif gpu_type == "amd": video_encoder = "h264_amf" elif gpu_type == "intel": video_encoder = "h264_qsv" else: video_encoder = "libx264" # 构造FFmpeg命令 command = [ 'ffmpeg', '-i', input_path, '-map_metadata', '-1', # 丢弃所有元数据 '-metadata', f'title={custom_metadata["title"]}', '-metadata', f'artist={custom_metadata["artist"]}', '-metadata', f'creation_time={custom_metadata["creation_time"]}', '-metadata', f'compatible_brands={custom_metadata["compatible_brands"]}', '-metadata', f'major_brand={custom_metadata["major_brand"]}', '-metadata', f'handler_name={custom_metadata["writing_application"]}', '-movflags', 'use_metadata_tags', '-c:v', video_encoder, '-preset', 'medium', '-crf', str(random.randint(18, 23)), # 随机CRF值 '-profile:v', 'high', '-level', '4.0', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '96k', '-ar', '44100', '-y', output_path ] # 添加设备特定元数据 if 'iPhone' in custom_metadata["device_model"]: command.extend([ '-metadata', f'com.apple.quicktime.model={custom_metadata["device_model"]}', '-metadata', f'com.apple.quicktime.software=16.0' ]) try: subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return True except subprocess.CalledProcessError as e: print(f"FFmpeg error: {e}") return False except FileNotFoundError: messagebox.showerror('致命错误', '错误:未找到FFmpeg!\n请确保ffmpeg.exe在程序同一目录下。') return False def create_background_video(output_path, duration, width=720, height=1560, fps=30): """创建带有扰动的黑色背景视频""" # 使用FFmpeg创建带有随机噪声的黑色背景视频 cmd = [ 'ffmpeg', '-f', 'lavfi', '-i', f'nullsrc=s={width}x{height}:d={duration}:r={fps}', '-vf', 'noise=alls=20:allf=t', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-y', output_path ] try: subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return True except subprocess.CalledProcessError as e: print(f"创建背景视频失败: {e}") return False def detect_gpu(): """检测可用的GPU类型""" try: # 尝试使用nvidia-smi检测NVIDIA GPU try: result = subprocess.run(['nvidia-smi'], capture_output=True, text=True, check=True) if result.returncode == 0: return "nvidia" except (subprocess.CalledProcessError, FileNotFoundError): pass # 尝试使用Windows Management Instrumentation检测AMD和Intel GPU try: import wmi w = wmi.WMI() for gpu in w.Win32_VideoController(): name = gpu.Name.lower() if "amd" in name or "radeon" in name: return "amd" elif "intel" in name: return "intel" except ImportError: pass # 尝试使用dxdiag检测GPU try: result = subprocess.run(['dxdiag', '/t', 'dxdiag.txt'], capture_output=True, text=True, check=True) if result.returncode == 0: with open('dxdiag.txt', 'r', encoding='utf-16') as f: content = f.read().lower() if "nvidia" in content: return "nvidia" elif "amd" in content or "radeon" in content: return "amd" elif "intel" in content: return "intel" except (subprocess.CalledProcessError, FileNotFoundError): pass except Exception as e: print(f"GPU检测失败: {e}") return "cpu" def add_audio_watermark(audio_clip, strength): """给音频添加水印(简化版本)""" # 在实际应用中,这里应该实现音频扰动算法 # 这里只是一个示例,返回原始音频 return audio_clip def process_video(): """主处理流程控制器""" global stop_processing if not MOVIEPY_AVAILABLE: messagebox.showerror("错误", "MoviePy库未安装!请运行: pip install moviepy") return False input_path = input_entry.get() output_path = output_entry.get() if not input_path or not output_path: messagebox.showerror('错误', '请先选择输入和输出文件!') return False # 解析用户选择的强度和功能 strength = strength_scale.get() use_video_perturb = video_var.get() use_audio_perturb = audio_var.get() use_metadata_corrupt = metadata_var.get() use_gan = gan_var.get() use_resize = resize_var.get() use_pip = pip_var.get() pip_opacity = pip_opacity_scale.get() if use_pip else 2 num_pip_videos = int(pip_num_combo.get()) if use_pip else 0 gpu_type = gpu_combo.get() # 临时文件路径 temp_video_path = "temp_processed.mp4" temp_audio_path = "temp_audio.aac" pip_temp_path = "temp_pip.mp4" if use_pip else None background_path = "temp_background.mp4" final_output_path = output_path # 获取原始视频时长和帧率 try: original_clip = VideoFileClip(input_path) original_duration = original_clip.duration original_fps = original_clip.fps original_clip.close() except Exception as e: messagebox.showerror('错误', f'无法打开视频文件: {str(e)}') return False try: # 第一步:创建背景视频 if use_resize: if not create_background_video(background_path, original_duration, 720, 1560, original_fps): messagebox.showerror('错误', '创建背景视频失败!') return False # 第二步:处理视频和音频 if use_video_perturb or use_resize: # 使用OpenCV打开视频 cap = cv2.VideoCapture(input_path) # 获取视频属性 fps = int(cap.get(cv2.CAP_PROP_FPS)) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 设置目标分辨率 target_width, target_height = 720, 1560 # 创建VideoWriter来写入处理后的视频 fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter(temp_video_path, fourcc, fps, (target_width, target_height)) processed_frames = 0 # 主循环:逐帧处理 while True: ret, frame = cap.read() if not ret: break # 读到结尾就退出 # 如果勾选了"调整分辨率",先调整分辨率 if use_resize: frame = resize_with_padding(frame, target_width, target_height) # 如果勾选了"视频扰动",就对当前帧进行处理 if use_video_perturb: frame = add_invisible_overlay(frame, strength) # 写入处理后的帧 out.write(frame) processed_frames += 1 # 更新进度条 progress_var.set(processed_frames / total_frames * 100) root.update_idletasks() # 检查是否取消 if stop_processing: break # 释放资源 cap.release() out.release() if stop_processing: messagebox.showinfo('信息', '处理已取消!') return False # 第三步:处理音频 if use_audio_perturb: # 从原视频提取音频 original_video = VideoFileClip(input_path) original_audio = original_video.audio if original_audio is not None: # 给音频添加水印 processed_audio = add_audio_watermark(original_audio, strength) # 保存处理后的音频到临时文件 processed_audio.write_audiofile(temp_audio_path, logger=None) processed_audio.close() original_video.close() else: # 如果没有勾选音频处理,直接提取原音频 original_video = VideoFileClip(input_path) original_audio = original_video.audio if original_audio is not None: original_audio.write_audiofile(temp_audio_path, logger=None) original_video.close() # 第四步:合并视频和音频 # 如果处理了视频或调整了分辨率,使用处理后的视频,否则使用原视频 video_source = temp_video_path if (use_video_perturb or use_resize) else input_path # 如果有音频文件,合并音频 if os.path.exists(temp_audio_path): # 使用FFmpeg合并音视频 merge_cmd = [ 'ffmpeg', '-i', video_source, '-i', temp_audio_path, '-c:v', 'copy', '-c:a', 'aac', '-map', '0:v:0', '-map', '1:a:0', '-shortest', '-y', final_output_path ] subprocess.run(merge_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: # 如果没有音频,直接复制视频 shutil.copy2(video_source, final_output_path) # 第五步:处理元数据(无论是否处理视频音频,只要勾选了就执行) if use_metadata_corrupt: custom_meta = generate_random_metadata() temp_final_path = final_output_path + "_temp.mp4" success = corrupt_metadata(final_output_path, temp_final_path, custom_meta, gpu_type) if success: # 用处理完元数据的文件替换最终文件 if os.path.exists(final_output_path): os.remove(final_output_path) os.rename(temp_final_path, final_output_path) else: return False # 第六步:GAN处理(预留功能) if use_gan: messagebox.showinfo('信息', 'GAN功能是预留选项,在当前版本中未实际生效。') messagebox.showinfo('完成', f'处理完成!\n输出文件已保存至: {final_output_path}') return True except Exception as e: messagebox.showerror('错误', f'处理过程中出现错误: {str(e)}') return False finally: # 清理可能的临时文件 for temp_file in [temp_video_path, temp_audio_path, pip_temp_path, background_path]: if temp_file and os.path.exists(temp_file): try: os.remove(temp_file) except: pass # 重置进度条 progress_var.set(0) # 启用开始按钮 start_button.config(state=tk.NORMAL) stop_processing = False def start_processing(): """开始处理视频""" global stop_processing stop_processing = False start_button.config(state=tk.DISABLED) # 在新线程中处理视频 thread = threading.Thread(target=process_video, daemon=True) thread.start() def stop_processing_func(): """停止处理""" global stop_processing stop_processing = True def browse_input(): """浏览输入文件""" filename = filedialog.askopenfilename( filetypes=[("Video Files", "*.mp4 *.mov *.avi *.mkv"), ("All Files", "*.*")] ) if filename: input_entry.delete(0, tk.END) input_entry.insert(0, filename) def browse_output(): """浏览输出文件""" filename = filedialog.asksaveasfilename( defaultextension=".mp4", filetypes=[("MP4 Files", "*.mp4"), ("All Files", "*.*")] ) if filename: output_entry.delete(0, tk.END) output_entry.insert(0, filename) def toggle_pip_widgets(): """切换画中画相关控件的状态""" state = tk.NORMAL if pip_var.get() else tk.DISABLED pip_num_combo.config(state=state) pip_opacity_scale.config(state=state) def show_gan_info(): """显示GAN功能信息""" if gan_var.get(): messagebox.showinfo('功能说明', '请注意:GAN功能是高级预留功能。\n在当前版本中,它会被一个高级扰动算法模拟,但并非真正的GAN。\n效果依然强大。') # 检测可用GPU detected_gpu = detect_gpu() gpu_options = ["自动检测", "cpu", "nvidia", "amd", "intel"] default_gpu = detected_gpu if detected_gpu != "cpu" else "自动检测" # 创建主窗口 root = tk.Tk() root.title("视频号专版防检测处理工具 v3.0") root.geometry("800x600") # 创建变量 video_var = tk.BooleanVar(value=True) audio_var = tk.BooleanVar(value=True) resize_var = tk.BooleanVar(value=True) metadata_var = tk.BooleanVar(value=True) pip_var = tk.BooleanVar(value=False) gan_var = tk.BooleanVar(value=False) progress_var = tk.DoubleVar(value=0) # 创建界面组件 main_frame = ttk.Frame(root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 输入文件选择 ttk.Label(main_frame, text="输入视频文件:").grid(row=0, column=0, sticky=tk.W, pady=5) input_entry = ttk.Entry(main_frame, width=50) input_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Button(main_frame, text="浏览", command=browse_input).grid(row=0, column=2, padx=5, pady=5) # 输出文件选择 ttk.Label(main_frame, text="输出视频文件:").grid(row=1, column=0, sticky=tk.W, pady=5) output_entry = ttk.Entry(main_frame, width=50) output_entry.grid(row=1, column=1, padx=5, pady=5) ttk.Button(main_frame, text="浏览", command=browse_output).grid(row=1, column=2, padx=5, pady=5) # 分隔线 ttk.Separator(main_frame, orient=tk.HORIZONTAL).grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) # 处理强度 ttk.Label(main_frame, text="处理强度:").grid(row=3, column=0, sticky=tk.W, pady=5) strength_scale = tk.Scale(main_frame, from_=1, to=100, orient=tk.HORIZONTAL, length=400) strength_scale.set(50) strength_scale.grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=5) # 分隔线 ttk.Separator(main_frame, orient=tk.HORIZONTAL).grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) # 处理选项 ttk.Checkbutton(main_frame, text="时空域微扰动 (抗视频指纹 - 核心推荐)", variable=video_var).grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=2) ttk.Checkbutton(main_frame, text="音频指纹污染 (抗音频指纹 - 核心推荐)", variable=audio_var).grid(row=6, column=0, columnspan=3, sticky=tk.W, pady=2) ttk.Checkbutton(main_frame, text="标准化分辨率 (720x1560) + 黑边扰动", variable=resize_var).grid(row=7, column=0, columnspan=3, sticky=tk.W, pady=2) ttk.Checkbutton(main_frame, text="元数据彻底清理与伪造", variable=metadata_var).grid(row=8, column=0, columnspan=3, sticky=tk.W, pady=2) # 画中画选项 pip_frame = ttk.Frame(main_frame) pip_frame.grid(row=9, column=0, columnspan=3, sticky=tk.W, pady=2) ttk.Checkbutton(pip_frame, text="画中画干扰 (从P文件夹随机选择视频)", variable=pip_var, command=toggle_pip_widgets).grid(row=0, column=0, sticky=tk.W) pip_options_frame = ttk.Frame(main_frame) pip_options_frame.grid(row=10, column=0, columnspan=3, sticky=tk.W, pady=2) ttk.Label(pip_options_frame, text="画中画数量:").grid(row=0, column=0, sticky=tk.W, padx=5) pip_num_combo = ttk.Combobox(pip_options_frame, values=[1, 2, 3, 4, 5], state="readonly", width=5) pip_num_combo.set(3) pip_num_combo.grid(row=0, column=1, padx=5) ttk.Label(pip_options_frame, text="透明度 (1-100):").grid(row=0, column=2, sticky=tk.W, padx=5) pip_opacity_scale = tk.Scale(pip_options_frame, from_=1, to=100, orient=tk.HORIZONTAL, length=150) pip_opacity_scale.set(2) pip_opacity_scale.grid(row=0, column=3, padx=5) # 禁用画中画选项 pip_num_combo.config(state=tk.DISABLED) pip_opacity_scale.config(state=tk.DISABLED) # GAN选项 ttk.Checkbutton(main_frame, text="动态GAN对抗性扰动 (预留功能)", variable=gan_var, command=show_gan_info).grid(row=11, column=0, columnspan=3, sticky=tk.W, pady=2) # GPU加速选项 gpu_frame = ttk.Frame(main_frame) gpu_frame.grid(row=12, column=0, columnspan=3, sticky=tk.W, pady=5) ttk.Label(gpu_frame, text="GPU加速:").grid(row=0, column=0, sticky=tk.W) gpu_combo = ttk.Combobox(gpu_frame, values=gpu_options, state="readonly", width=10) gpu_combo.set(default_gpu) gpu_combo.grid(row=0, column=1, padx=5) # 分隔线 ttk.Separator(main_frame, orient=tk.HORIZONTAL).grid(row=13, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) # 进度条 ttk.Label(main_frame, text="进度:").grid(row=14, column=0, sticky=tk.W, pady=5) progress_bar = ttk.Progressbar(main_frame, variable=progress_var, maximum=100, length=400) progress_bar.grid(row=14, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=5) # 按钮 button_frame = ttk.Frame(main_frame) button_frame.grid(row=15, column=0, columnspan=3, pady=10) start_button = ttk.Button(button_frame, text="开始处理", command=start_processing) start_button.pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="停止", command=stop_processing_func).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="退出", command=root.quit).pack(side=tk.LEFT, padx=5) # 配置网格权重 root.columnconfigure(0, weight=1) root.rowconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) # 运行主循环 root.mainloop() 以上代码在打包时出现问题,请使用Tkinter替代PySimpleGUI代码,通过指定路径解决 MoviePy 导入问题,C:\Users\Administrator\AppData\Local\Programs\Python\Python312\Lib\site-packages\moviepy_init_.py 来修改代码,这是moviepy路径,并检查整体流程有没有错误项, 然后将代码展示成无需命令行运行的模式,而是鼠标点击就可以运行的软件,只要Windows版本的详细操作。每一步怎么打包需要用到什么软件怎么操作都详细的发给我,我是小白,请讲的详细点
08-21
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值