@ -0,0 +1,691 @@
import sys
import os
from dataclasses import dataclass
from typing import Optional , Tuple
import numpy as np
from PyQt5 import QtCore , QtGui , QtWidgets
# -----------------------------
# 数据模型与枚举
# -----------------------------
class PixelFormat :
# Packed YUV422
YUYV = " YUYV " # Y0 U0 Y1 V0
YVYU = " YVYU " # Y0 V0 Y1 U0
UYVY = " UYVY " # U0 Y0 V0 Y1
VYUY = " VYUY " # V0 Y0 U0 Y1
# Semi-planar / planar variants
NV12 = " NV12 " # Y + interleaved UV (4:2:0), UV order = U,V
NV21 = " NV21 " # Y + interleaved VU (4:2:0), UV order = V,U
NV16 = " NV16 " # Y + interleaved UV (4:2:2), UV order = U,V
NV61 = " NV61 " # Y + interleaved VU (4:2:2), UV order = V,U
GRAY = " GRAY " # Only Y
# YUV444
YUV444_PLANAR = " YUV444_PLANAR " # YYYY... UUUU... VVVV...
YUV444_INTERLEAVED_UV = " YUV444_INTERLEAVED_UV " # Y U V Y U V ... (UV顺序=U,V)
YUV444_INTERLEAVED_VU = " YUV444_INTERLEAVED_VU " # Y V U Y V U ... (UV顺序=V,U)
# RGB/BGR formats
RGB565 = " RGB565 " # 16-bit RGB: RRRRR GGGGGG BBBBB
BGR565 = " BGR565 " # 16-bit BGR: BBBBB GGGGGG RRRRR
RGB888 = " RGB888 " # 24-bit RGB: R G B
BGR888 = " BGR888 " # 24-bit BGR: B G R
RGB8888 = " RGB8888 " # 32-bit RGB: R G B (alpha ignored)
BGR8888 = " BGR8888 " # 32-bit BGR: B G R (alpha ignored)
NRGB8888 = " NRGB8888 " # 32-bit native RGB (no alpha)
NBGR8888 = " NBGR8888 " # 32-bit native BGR (no alpha)
SUPPORTED_FORMATS = [
PixelFormat . YUYV , PixelFormat . YVYU , PixelFormat . UYVY , PixelFormat . VYUY ,
PixelFormat . NV12 , PixelFormat . NV21 , PixelFormat . NV16 , PixelFormat . NV61 ,
PixelFormat . GRAY ,
PixelFormat . YUV444_PLANAR , PixelFormat . YUV444_INTERLEAVED_UV , PixelFormat . YUV444_INTERLEAVED_VU ,
PixelFormat . RGB565 , PixelFormat . BGR565 ,
PixelFormat . RGB888 , PixelFormat . BGR888 ,
PixelFormat . RGB8888 , PixelFormat . BGR8888 ,
PixelFormat . NRGB8888 , PixelFormat . NBGR8888
]
# 预定义的尺寸选项
PREDEFINED_SIZES = [
" 640x480 (VGA) " ,
" 800x600 (SVGA) " ,
" 1024x768 (XGA) " ,
" 1280x720 (HD) " ,
" 1920x1080 (Full HD) " ,
" 2560x1440 (2K) " ,
" 3840x2160 (4K) " ,
" 自定义... "
]
@dataclass
class ImageSpec :
width : int
height : int
fmt : str
# -----------------------------
# YUV -> RGB 转换工具
# -----------------------------
def yuv_to_rgb_numpy ( Y , U , V ) :
"""
Y , U , V : float32 ndarray in [ 0. .255 ] , same shape .
Returns RGB uint8 ndarray with shape ( . . . , 3 ) .
BT .601 full - range近似 :
R = Y + 1.402 * ( V - 128 )
G = Y - 0.344136 * ( U - 128 ) - 0.714136 * ( V - 128 )
B = Y + 1.772 * ( U - 128 )
"""
Yf = Y . astype ( np . float32 )
Uf = U . astype ( np . float32 ) - 128.0
Vf = V . astype ( np . float32 ) - 128.0
R = Yf + 1.402 * Vf
G = Yf - 0.344136 * Uf - 0.714136 * Vf
B = Yf + 1.772 * Uf
RGB = np . stack ( [ R , G , B ] , axis = - 1 )
np . clip ( RGB , 0 , 255 , out = RGB )
return RGB . astype ( np . uint8 )
def unpack_packed_422_to_planes ( data : np . ndarray , width : int , height : int , order : str ) - > Tuple [ np . ndarray , np . ndarray , np . ndarray ] :
"""
将打包的YUV422 ( 每2像素4字节 ) 解成等大Y平面和下采样的U / V再上采样到全分辨率 。
order : " YUYV " , " YVYU " , " UYVY " , " VYUY "
返回 Y , U_full , V_full
"""
# 每行应有 width*2 字节
expected = width * height * 2
if data . size != expected :
raise ValueError ( f " 数据长度不匹配,期望 { expected } 字节,实际 { data . size } 字节 " )
# 先 reshape 成 (H, W*2)
row = data . reshape ( ( height , width * 2 ) )
# 两像素为一组的4字节块
blocks = row . reshape ( ( height , width / / 2 , 4 ) ) # shape (H, W/2, 4)
if order == PixelFormat . YUYV :
Y0 = blocks [ : , : , 0 ]
U0 = blocks [ : , : , 1 ]
Y1 = blocks [ : , : , 2 ]
V0 = blocks [ : , : , 3 ]
elif order == PixelFormat . YVYU :
Y0 = blocks [ : , : , 0 ]
V0 = blocks [ : , : , 1 ]
Y1 = blocks [ : , : , 2 ]
U0 = blocks [ : , : , 3 ]
elif order == PixelFormat . UYVY :
U0 = blocks [ : , : , 0 ]
Y0 = blocks [ : , : , 1 ]
V0 = blocks [ : , : , 2 ]
Y1 = blocks [ : , : , 3 ]
elif order == PixelFormat . VYUY :
V0 = blocks [ : , : , 0 ]
Y0 = blocks [ : , : , 1 ]
U0 = blocks [ : , : , 2 ]
Y1 = blocks [ : , : , 3 ]
else :
raise ValueError ( " 不支持的打包422顺序 " )
# 还原Y为 (H, W)
Y = np . empty ( ( height , width ) , dtype = np . uint8 )
Y [ : , 0 : : 2 ] = Y0
Y [ : , 1 : : 2 ] = Y1
# U/V 为每2像素共享, 横向采样因子2, 纵向不变 -> 需要水平上采样到全分辨率
U = np . repeat ( U0 , 2 , axis = 1 )
V = np . repeat ( V0 , 2 , axis = 1 )
return Y , U , V
def parse_nvxx ( data : np . ndarray , width : int , height : int , chroma_order : str , is_420 : bool ) - > Tuple [ np . ndarray , np . ndarray , np . ndarray ] :
"""
NV12 / NV21 ( 420 ) 或 NV16 / NV61 ( 422 ) 半平面解析 。
chroma_order : " UV " or " VU "
is_420 : True for 4 : 2 : 0 , False for 4 : 2 : 2
返回 Y , U_full , V_full
"""
if is_420 :
y_size = width * height
c_h = height / / 2
c_size = width * c_h # interleaved 2 channels across width
expected = y_size + c_size
if data . size != expected :
raise ValueError ( f " 数据长度不匹配(420),期望 { expected } 字节,实际 { data . size } 字节 " )
Y = data [ : y_size ] . reshape ( ( height , width ) )
UV = data [ y_size : ] . reshape ( ( c_h , width ) )
# UV 行内是交错: U,V,U,V,...
U_sub = UV [ : , 0 : : 2 ]
V_sub = UV [ : , 1 : : 2 ]
if chroma_order == " VU " :
U_sub , V_sub = V_sub , U_sub
# 上采样到全分辨率(双线性可选,这里用最近邻)
U = np . repeat ( np . repeat ( U_sub , 2 , axis = 0 ) , 2 , axis = 1 )
V = np . repeat ( np . repeat ( V_sub , 2 , axis = 0 ) , 2 , axis = 1 )
else :
# 422: 色度高方向与亮度同高, 水平方向为1/2
y_size = width * height
c_h = height
c_size = width * c_h / / 1 / / 1 / / 2 # 每行交错UV对, 宽度一样但每2像素一组 → 列数等于宽
expected = y_size + ( width * c_h ) # 因为UV交错, 每行宽度与Y一致
if data . size != expected :
raise ValueError ( f " 数据长度不匹配(422),期望 { expected } 字节,实际 { data . size } 字节 " )
Y = data [ : y_size ] . reshape ( ( height , width ) )
UV = data [ y_size : ] . reshape ( ( c_h , width ) )
U_sub = UV [ : , 0 : : 2 ]
V_sub = UV [ : , 1 : : 2 ]
if chroma_order == " VU " :
U_sub , V_sub = V_sub , U_sub
U = np . repeat ( U_sub , 2 , axis = 1 )
V = np . repeat ( V_sub , 2 , axis = 1 )
return Y , U , V
def parse_gray ( data : np . ndarray , width : int , height : int ) - > Tuple [ np . ndarray , np . ndarray , np . ndarray ] :
expected = width * height
if data . size != expected :
raise ValueError ( f " 数据长度不匹配(GRAY),期望 { expected } 字节,实际 { data . size } 字节 " )
Y = data . reshape ( ( height , width ) )
U = np . full_like ( Y , 128 , dtype = np . uint8 )
V = np . full_like ( Y , 128 , dtype = np . uint8 )
return Y , U , V
def parse_yuv444_planar ( data : np . ndarray , width : int , height : int ) - > Tuple [ np . ndarray , np . ndarray , np . ndarray ] :
plane_size = width * height
expected = plane_size * 3
if data . size != expected :
raise ValueError ( f " 数据长度不匹配(YUV444 planar),期望 { expected } 字节,实际 { data . size } 字节 " )
Y = data [ 0 : plane_size ] . reshape ( ( height , width ) )
U = data [ plane_size : 2 * plane_size ] . reshape ( ( height , width ) )
V = data [ 2 * plane_size : 3 * plane_size ] . reshape ( ( height , width ) )
return Y , U , V
def parse_yuv444_interleaved ( data : np . ndarray , width : int , height : int , uv_order : str ) - > Tuple [ np . ndarray , np . ndarray , np . ndarray ] :
"""
交错式 : 按像素 Y U V 或 Y V U 排列 ( 总步长3字节 ) 。
uv_order : " UV " 或 " VU "
"""
expected = width * height * 3
if data . size != expected :
raise ValueError ( f " 数据长度不匹配(YUV444 interleaved),期望 { expected } 字节,实际 { data . size } 字节 " )
pix = data . reshape ( ( height , width , 3 ) )
Y = pix [ : , : , 0 ]
if uv_order == " UV " :
U = pix [ : , : , 1 ]
V = pix [ : , : , 2 ]
else :
V = pix [ : , : , 1 ]
U = pix [ : , : , 2 ]
return Y , U , V
# -----------------------------
# RGB/BGR 格式解析函数
# -----------------------------
def parse_rgb565 ( data : np . ndarray , width : int , height : int , is_bgr : bool = False ) - > np . ndarray :
"""
解析RGB565或BGR565格式
每个像素2字节 : RRRRR GGGGGG BBBBB ( 16 位 )
返回RGB uint8数组 ( H , W , 3 )
"""
expected = width * height * 2
if data . size != expected :
raise ValueError ( f " 数据长度不匹配(RGB565/BGR565),期望 { expected } 字节,实际 { data . size } 字节 " )
# 将数据重新解释为16位整数( 小端序)
pixels = data . view ( dtype = np . uint16 ) . reshape ( ( height , width ) )
# 提取RGB分量
if is_bgr :
# BGR565: BBBBB GGGGGG RRRRR
b = ( ( pixels >> 11 ) & 0x1F ) << 3 # 5-bit to 8-bit
g = ( ( pixels >> 5 ) & 0x3F ) << 2 # 6-bit to 8-bit
r = ( pixels & 0x1F ) << 3 # 5-bit to 8-bit
else :
# RGB565: RRRRR GGGGGG BBBBB
r = ( ( pixels >> 11 ) & 0x1F ) << 3
g = ( ( pixels >> 5 ) & 0x3F ) << 2
b = ( pixels & 0x1F ) << 3
# 组合成RGB图像
rgb = np . stack ( [ r , g , b ] , axis = - 1 ) . astype ( np . uint8 )
return rgb
def parse_rgb888 ( data : np . ndarray , width : int , height : int , is_bgr : bool = False ) - > np . ndarray :
"""
解析RGB888或BGR888格式
每个像素3字节 : R G B 或 B G R
返回RGB uint8数组 ( H , W , 3 )
"""
expected = width * height * 3
if data . size != expected :
raise ValueError ( f " 数据长度不匹配(RGB888/BGR888),期望 { expected } 字节,实际 { data . size } 字节 " )
# 重塑为(H, W, 3)
pixels = data . reshape ( ( height , width , 3 ) )
if is_bgr :
# BGR -> RGB
rgb = np . zeros_like ( pixels )
rgb [ : , : , 0 ] = pixels [ : , : , 2 ] # R
rgb [ : , : , 1 ] = pixels [ : , : , 1 ] # G
rgb [ : , : , 2 ] = pixels [ : , : , 0 ] # B
return rgb
else :
# 已经是RGB格式
return pixels
def parse_rgb8888 ( data : np . ndarray , width : int , height : int , is_bgr : bool = False , is_native : bool = False ) - > np . ndarray :
"""
解析32位RGB / BGR格式
每个像素4字节 : R G B ( alpha ignored ) 或 B G R ( alpha ignored )
或NRGB8888 / NBGR8888 ( native格式 , alpha通道可能位于不同位置 )
返回RGB uint8数组 ( H , W , 3 )
"""
expected = width * height * 4
if data . size != expected :
raise ValueError ( f " 数据长度不匹配(32-bit RGB/BGR),期望 { expected } 字节,实际 { data . size } 字节 " )
# 重塑为(H, W, 4)
pixels = data . reshape ( ( height , width , 4 ) )
if is_native :
# 对于NRGB8888/NBGR8888, 格式可能为 A R G B 或 R G B A, 取决于系统字节序
# 简单起见, 我们假设前三个字节是RGB/BGR分量( 忽略alpha)
if is_bgr :
# NBGR8888: B G R (alpha)
rgb = np . zeros ( ( height , width , 3 ) , dtype = np . uint8 )
rgb [ : , : , 0 ] = pixels [ : , : , 2 ] # R
rgb [ : , : , 1 ] = pixels [ : , : , 1 ] # G
rgb [ : , : , 2 ] = pixels [ : , : , 0 ] # B
else :
# NRGB8888: R G B (alpha)
rgb = pixels [ : , : , : 3 ] . copy ( )
else :
# RGB8888或BGR8888
if is_bgr :
# BGR8888: B G R (alpha)
rgb = np . zeros ( ( height , width , 3 ) , dtype = np . uint8 )
rgb [ : , : , 0 ] = pixels [ : , : , 2 ] # R
rgb [ : , : , 1 ] = pixels [ : , : , 1 ] # G
rgb [ : , : , 2 ] = pixels [ : , : , 0 ] # B
else :
# RGB8888: R G B (alpha)
rgb = pixels [ : , : , : 3 ] . copy ( )
return rgb
def decode_raw_frame ( raw : bytes , spec : ImageSpec , yuv444_uv_order : str = " UV " ) - > np . ndarray :
"""
将原始帧数据按 spec 解码为 RGB np . ndarray ( H , W , 3 ) , uint8
yuv444_uv_order : 针对 YUV444_INTERLEAVED 时 UV顺序选择 " UV " 或 " VU "
"""
width , height , fmt = spec . width , spec . height , spec . fmt
arr = np . frombuffer ( raw , dtype = np . uint8 )
# YUV格式解析
if fmt in ( PixelFormat . YUYV , PixelFormat . YVYU , PixelFormat . UYVY , PixelFormat . VYUY ) :
Y , U , V = unpack_packed_422_to_planes ( arr , width , height , fmt )
rgb = yuv_to_rgb_numpy ( Y , U , V )
elif fmt == PixelFormat . NV12 :
Y , U , V = parse_nvxx ( arr , width , height , " UV " , is_420 = True )
rgb = yuv_to_rgb_numpy ( Y , U , V )
elif fmt == PixelFormat . NV21 :
Y , U , V = parse_nvxx ( arr , width , height , " VU " , is_420 = True )
rgb = yuv_to_rgb_numpy ( Y , U , V )
elif fmt == PixelFormat . NV16 :
Y , U , V = parse_nvxx ( arr , width , height , " UV " , is_420 = False )
rgb = yuv_to_rgb_numpy ( Y , U , V )
elif fmt == PixelFormat . NV61 :
Y , U , V = parse_nvxx ( arr , width , height , " VU " , is_420 = False )
rgb = yuv_to_rgb_numpy ( Y , U , V )
elif fmt == PixelFormat . GRAY :
Y , U , V = parse_gray ( arr , width , height )
rgb = yuv_to_rgb_numpy ( Y , U , V )
elif fmt == PixelFormat . YUV444_PLANAR :
Y , U , V = parse_yuv444_planar ( arr , width , height )
rgb = yuv_to_rgb_numpy ( Y , U , V )
elif fmt == PixelFormat . YUV444_INTERLEAVED_UV :
Y , U , V = parse_yuv444_interleaved ( arr , width , height , " UV " )
rgb = yuv_to_rgb_numpy ( Y , U , V )
elif fmt == PixelFormat . YUV444_INTERLEAVED_VU :
Y , U , V = parse_yuv444_interleaved ( arr , width , height , " VU " )
rgb = yuv_to_rgb_numpy ( Y , U , V )
# RGB/BGR格式解析
elif fmt == PixelFormat . RGB565 :
rgb = parse_rgb565 ( arr , width , height , is_bgr = False )
elif fmt == PixelFormat . BGR565 :
rgb = parse_rgb565 ( arr , width , height , is_bgr = True )
elif fmt == PixelFormat . RGB888 :
rgb = parse_rgb888 ( arr , width , height , is_bgr = False )
elif fmt == PixelFormat . BGR888 :
rgb = parse_rgb888 ( arr , width , height , is_bgr = True )
elif fmt == PixelFormat . RGB8888 :
rgb = parse_rgb8888 ( arr , width , height , is_bgr = False , is_native = False )
elif fmt == PixelFormat . BGR8888 :
rgb = parse_rgb8888 ( arr , width , height , is_bgr = True , is_native = False )
elif fmt == PixelFormat . NRGB8888 :
rgb = parse_rgb8888 ( arr , width , height , is_bgr = False , is_native = True )
elif fmt == PixelFormat . NBGR8888 :
rgb = parse_rgb8888 ( arr , width , height , is_bgr = True , is_native = True )
else :
raise ValueError ( f " 不支持的像素格式: { fmt } " )
return rgb
# -----------------------------
# QImage 显示与栅格绘制
# -----------------------------
class GridPixmap ( QtGui . QPixmap ) :
def __init__ ( self , base_image : QtGui . QImage , show_grid : bool , grid_step : int , grid_color : QtGui . QColor ) :
super ( ) . __init__ ( QtGui . QPixmap . fromImage ( base_image ) )
if show_grid and grid_step > 0 :
painter = QtGui . QPainter ( self )
pen = QtGui . QPen ( grid_color )
pen . setWidth ( 1 )
painter . setPen ( pen )
w = self . width ( )
h = self . height ( )
# 画垂直线
x = 0
while x < w :
painter . drawLine ( x , 0 , x , h )
x + = grid_step
# 画水平线
y = 0
while y < h :
painter . drawLine ( 0 , y , w , y )
y + = grid_step
painter . end ( )
def numpy_to_qimage ( rgb : np . ndarray ) - > QtGui . QImage :
h , w , _ = rgb . shape
bytes_per_line = 3 * w
# 注意:必须保证连续内存
if not rgb . flags [ ' C_CONTIGUOUS ' ] :
rgb = np . ascontiguousarray ( rgb )
return QtGui . QImage ( rgb . data , w , h , bytes_per_line , QtGui . QImage . Format_RGB888 ) . copy ( )
# -----------------------------
# 主窗口与UI
# -----------------------------
class ImageViewer ( QtWidgets . QLabel ) :
def __init__ ( self ) :
super ( ) . __init__ ( )
self . setAlignment ( QtCore . Qt . AlignCenter )
self . setBackgroundRole ( QtGui . QPalette . Base )
self . setSizePolicy ( QtWidgets . QSizePolicy . Ignored , QtWidgets . QSizePolicy . Ignored )
# 修改:设置为不缩放,保持原始大小
self . setScaledContents ( False )
def set_pixmap ( self , pixmap : QtGui . QPixmap ) :
self . setPixmap ( pixmap )
# 调整窗口以适应图片大小(可选)
self . resize ( pixmap . size ( ) )
class MainWindow ( QtWidgets . QMainWindow ) :
def __init__ ( self ) :
super ( ) . __init__ ( )
self . setWindowTitle ( " Raw YUV Viewer " )
self . resize ( 1000 , 700 )
# 创建一个滚动区域来容纳图像查看器(当图像太大时)
self . scroll_area = QtWidgets . QScrollArea ( )
self . scroll_area . setWidgetResizable ( True )
self . scroll_area . setAlignment ( QtCore . Qt . AlignCenter )
self . viewer = ImageViewer ( )
self . scroll_area . setWidget ( self . viewer )
self . setCentralWidget ( self . scroll_area )
# 当前状态
self . image_spec = ImageSpec ( 640 , 480 , PixelFormat . NV12 )
self . yuv444_uv_order = " UV " # 仅对 YUV444 interleaved 使用
self . current_file : Optional [ str ] = None
self . current_rgb : Optional [ np . ndarray ] = None
# 栅格设置
self . show_grid = False
self . grid_step = 8
self . grid_color = QtGui . QColor ( 0 , 255 , 0 , 120 )
self . _create_actions ( )
self . _create_menus ( )
# 启用拖放
self . setAcceptDrops ( True )
# 状态栏
self . statusBar ( ) . showMessage ( " 拖入原始帧文件,或 文件 -> 打开 " )
# ---- 菜单和动作 ----
def _create_actions ( self ) :
self . act_open = QtWidgets . QAction ( " 打开... " , self )
self . act_open . setShortcut ( " Ctrl+O " )
self . act_open . triggered . connect ( self . open_file_dialog )
self . act_quit = QtWidgets . QAction ( " 退出 " , self )
self . act_quit . setShortcut ( " Ctrl+Q " )
self . act_quit . triggered . connect ( self . close )
self . act_toggle_grid = QtWidgets . QAction ( " 显示栅格 " , self , checkable = True )
self . act_toggle_grid . triggered . connect ( self . toggle_grid )
self . act_set_grid_step = QtWidgets . QAction ( " 设置栅格步长... " , self )
self . act_set_grid_step . triggered . connect ( self . set_grid_step )
self . act_rerender = QtWidgets . QAction ( " 重新渲染 " , self )
self . act_rerender . setShortcut ( " F5 " )
self . act_rerender . triggered . connect ( self . rerender )
self . act_fit_to_window = QtWidgets . QAction ( " 适应窗口大小 " , self , checkable = True )
self . act_fit_to_window . triggered . connect ( self . toggle_fit_to_window )
self . act_actual_size = QtWidgets . QAction ( " 实际大小 " , self )
self . act_actual_size . triggered . connect ( self . show_actual_size )
def _create_menus ( self ) :
m_file = self . menuBar ( ) . addMenu ( " 文件 " )
m_file . addAction ( self . act_open )
m_file . addSeparator ( )
m_file . addAction ( self . act_quit )
m_settings = self . menuBar ( ) . addMenu ( " 设置 " )
# 创建尺寸子菜单
m_size = m_settings . addMenu ( " 图像尺寸 " )
for size_text in PREDEFINED_SIZES :
if size_text == " 自定义... " :
m_size . addSeparator ( )
act_custom = QtWidgets . QAction ( " 自定义... " , self )
act_custom . triggered . connect ( self . set_custom_size )
m_size . addAction ( act_custom )
else :
act = QtWidgets . QAction ( size_text , self )
act . triggered . connect ( lambda checked , s = size_text : self . set_predefined_size ( s ) )
m_size . addAction ( act )
# 创建格式子菜单
m_format = m_settings . addMenu ( " 像素格式 " )
for fmt in SUPPORTED_FORMATS :
act = QtWidgets . QAction ( fmt , self , checkable = True )
act . setChecked ( fmt == self . image_spec . fmt )
act . triggered . connect ( lambda checked , f = fmt : self . set_pixel_format ( f ) )
m_format . addAction ( act )
m_settings . addSeparator ( )
m_settings . addAction ( self . act_rerender )
m_view = self . menuBar ( ) . addMenu ( " 视图 " )
m_view . addAction ( self . act_toggle_grid )
m_view . addAction ( self . act_set_grid_step )
m_view . addSeparator ( )
m_view . addAction ( self . act_fit_to_window )
m_view . addAction ( self . act_actual_size )
# ---- 事件:拖放 ----
def dragEnterEvent ( self , event : QtGui . QDragEnterEvent ) :
if event . mimeData ( ) . hasUrls ( ) :
event . acceptProposedAction ( )
def dropEvent ( self , event : QtGui . QDropEvent ) :
urls = event . mimeData ( ) . urls ( )
if not urls :
return
path = urls [ 0 ] . toLocalFile ( )
if path :
self . load_file ( path )
# ---- 打开/加载/渲染 ----
def open_file_dialog ( self ) :
path , _ = QtWidgets . QFileDialog . getOpenFileName ( self , " 选择原始帧文件 " , " " , " All Files (*) " )
if path :
self . load_file ( path )
def load_file ( self , path : str ) :
try :
with open ( path , " rb " ) as f :
raw = f . read ( )
self . current_file = path
self . statusBar ( ) . showMessage ( f " 已加载: { os . path . basename ( path ) } ( { len ( raw ) } bytes) " )
self . current_rgb = self . decode_with_current_spec ( raw )
self . update_view ( )
except Exception as e :
QtWidgets . QMessageBox . critical ( self , " 错误 " , str ( e ) )
def decode_with_current_spec ( self , raw : bytes ) - > np . ndarray :
# 对 YUV444_INTERLEAVED_*, uv 顺序已在格式名中决定;也保留一个变量用于未来扩展
return decode_raw_frame ( raw , self . image_spec , self . yuv444_uv_order )
def update_view ( self ) :
if self . current_rgb is None :
self . viewer . setPixmap ( QtGui . QPixmap ( ) )
return
qimg = numpy_to_qimage ( self . current_rgb )
pm = GridPixmap ( qimg , self . show_grid , self . grid_step , self . grid_color )
self . viewer . set_pixmap ( pm )
# 更新窗口标题显示当前信息
self . setWindowTitle ( f " Raw YUV Viewer - { self . image_spec . width } x { self . image_spec . height } { self . image_spec . fmt } " )
def rerender ( self ) :
if not self . current_file :
return
try :
with open ( self . current_file , " rb " ) as f :
raw = f . read ( )
self . current_rgb = self . decode_with_current_spec ( raw )
self . update_view ( )
except Exception as e :
QtWidgets . QMessageBox . critical ( self , " 错误 " , str ( e ) )
# ---- 设置项 ----
def set_predefined_size ( self , size_text : str ) :
""" 设置预定义的尺寸 """
size_part = size_text . split ( ) [ 0 ]
w , h = map ( int , size_part . split ( ' x ' ) )
self . image_spec = ImageSpec ( w , h , self . image_spec . fmt )
self . statusBar ( ) . showMessage ( f " 已设置尺寸: { w } x { h } " )
self . rerender ( )
# 更新格式菜单的选中状态
self . update_format_menu_checked ( )
def set_custom_size ( self ) :
""" 设置自定义尺寸 """
w , ok1 = QtWidgets . QInputDialog . getInt ( self , " 图像宽度 " , " 宽 (pixels): " , self . image_spec . width , 1 , 16384 , 1 )
if not ok1 :
return
h , ok2 = QtWidgets . QInputDialog . getInt ( self , " 图像高度 " , " 高 (pixels): " , self . image_spec . height , 1 , 16384 , 1 )
if not ok2 :
return
self . image_spec = ImageSpec ( w , h , self . image_spec . fmt )
self . statusBar ( ) . showMessage ( f " 已设置尺寸: { w } x { h } " )
self . rerender ( )
def set_pixel_format ( self , fmt : str ) :
""" 设置像素格式 """
self . image_spec = ImageSpec ( self . image_spec . width , self . image_spec . height , fmt )
self . statusBar ( ) . showMessage ( f " 已设置像素格式: { fmt } " )
self . rerender ( )
# 更新格式菜单的选中状态
self . update_format_menu_checked ( )
def update_format_menu_checked ( self ) :
""" 更新格式菜单的选中状态 """
format_menu = self . menuBar ( ) . actions ( ) [ 1 ] . menu ( ) . actions ( ) [ 1 ] . menu ( ) # 获取格式子菜单
for action in format_menu . actions ( ) :
if action . text ( ) == self . image_spec . fmt :
action . setChecked ( True )
else :
action . setChecked ( False )
def toggle_grid ( self , checked : bool ) :
self . show_grid = checked
self . update_view ( )
def set_grid_step ( self ) :
step , ok = QtWidgets . QInputDialog . getInt ( self , " 栅格步长 " , " 像素步长: " , self . grid_step , 1 , 512 , 1 )
if not ok :
return
self . grid_step = step
self . update_view ( )
def toggle_fit_to_window ( self , checked : bool ) :
""" 切换是否适应窗口大小 """
if checked :
self . scroll_area . setWidgetResizable ( True )
self . viewer . setScaledContents ( True )
else :
self . scroll_area . setWidgetResizable ( False )
self . viewer . setScaledContents ( False )
if self . viewer . pixmap ( ) :
self . viewer . resize ( self . viewer . pixmap ( ) . size ( ) )
def show_actual_size ( self ) :
""" 显示实际大小(取消适应窗口) """
self . act_fit_to_window . setChecked ( False )
self . scroll_area . setWidgetResizable ( False )
self . viewer . setScaledContents ( False )
if self . viewer . pixmap ( ) :
self . viewer . resize ( self . viewer . pixmap ( ) . size ( ) )
def main ( ) :
app = QtWidgets . QApplication ( sys . argv )
w = MainWindow ( )
w . show ( )
sys . exit ( app . exec_ ( ) )
if __name__ == " __main__ " :
main ( )