# -*- coding: utf-8 -*-
import threading
import time
import sys
import inspect
import ctypes
import os
import cv2
import numpy as np
import logging
from .MvCameraControl_class import *
from datetime import datetime
from ctypes import *
from enum import Enum
from ctypes import byref, cast, POINTER, c_ubyte
from .MvCameraControl_class import MvCamera
from .Cam_eraConstants import *
from .Camera_Params_header import MV_FRAME_OUT_INFO_EX, PixelType_Gvsp_BGR8_Packed
# 配置日志系统
def setup_logging(log_level=logging.INFO):
"""配置全局日志系统"""
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("camera_operation.log"),
logging.StreamHandler()
]
)
logging.info("日志系统初始化完成")
setup_logging(logging.INFO)
# 像素格式兼容处理
try:
# 尝试导入像素类型定义
from .PixelType_header import *
logging.info("成功从 PixelType_header 导入像素类型定义")
except ImportError:
try:
# 尝试从父目录导入
from .PixelType_header import *
logging.info("成功从全局 PixelType_header 导入像素类型定义")
except ImportError:
logging.warning("警告: 无法导入 PixelType_header,创建虚拟定义")
# 创建必要的虚拟定义
PixelType_Gvsp_Undefined = -1
PixelType_Gvsp_Mono8 = 0x01000008
PixelType_Gvsp_BayerGR8 = 0x01080008
PixelType_Gvsp_RGB8_Packed = 0x02180014
# 定义像素格式映射表
PIXEL_FORMATS = {
"MONO8": PixelType_Gvsp_Mono8 if 'PixelType_Gvsp_Mono8' in globals() else 0x01080001,
"MONO10": PixelType_Gvsp_Mono10 if 'PixelType_Gvsp_Mono10' in globals() else 0x01100003,
"MONO12": PixelType_Gvsp_Mono12 if 'PixelType_Gvsp_Mono12' in globals() else 0x01100005,
"BAYER_BG8": PixelType_Gvsp_BayerBG8 if 'PixelType_Gvsp_BayerBG8' in globals() else 0x0108000B,
"BAYER_GB8": PixelType_Gvsp_BayerGB8 if 'PixelType_Gvsp_BayerGB8' in globals() else 0x0108000A,
"BAYER_GR8": PixelType_Gvsp_BayerGR8 if 'PixelType_Gvsp_BayerGR8' in globals() else 0x01080008,
"BAYER_RG8": PixelType_Gvsp_BayerRG8 if 'PixelType_Gvsp_BayerRG8' in globals() else 0x01080009,
"RGB8": PixelType_Gvsp_RGB8_Packed if 'PixelType_Gvsp_RGB8_Packed' in globals() else 0x02180014,
"BGR8": PixelType_Gvsp_BGR8_Packed,
"YUV422": PixelType_Gvsp_YUV422_Packed if 'PixelType_Gvsp_YUV422_Packed' in globals() else 0x02100032,
"YUV422_YUYV": PixelType_Gvsp_YUV422_YUYV_Packed if 'PixelType_Gvsp_YUV422_YUYV_Packed' in globals() else 0x0210001F
}
# 像素格式枚举
class PixelFormat(Enum):
MONO8 = PIXEL_FORMATS["MONO8"]
MONO10 = PIXEL_FORMATS["MONO10"]
MONO12 = PIXEL_FORMATS["MONO12"]
BAYER_BG8 = PIXEL_FORMATS["BAYER_BG8"]
BAYER_GB8 = PIXEL_FORMATS["BAYER_GB8"]
BAYER_GR8 = PIXEL_FORMATS["BAYER_GR8"]
BAYER_RG8 = PIXEL_FORMATS["BAYER_RG8"]
RGB8 = PIXEL_FORMATS["RGB8"]
YUV422 = PIXEL_FORMATS["YUV422"]
YUV422_YUYV = PIXEL_FORMATS["YUV422_YUYV"]
def is_mono_data(enGvspPixelType):
"""判断是否为单色图像格式"""
mono_formats = [
PIXEL_FORMATS["MONO8"],
PIXEL_FORMATS["MONO10"],
PIXEL_FORMATS["MONO12"],
0x010C0004, # Mono10 Packed
0x010C0006, # Mono12 Packed
0x01080002, # Mono8 Signed
0x0110000C # Mono16
]
return enGvspPixelType in mono_formats
def is_color_data(enGvspPixelType):
"""判断是否为彩色图像格式"""
color_formats = [
# Bayer格式
PIXEL_FORMATS["BAYER_BG8"],
PIXEL_FORMATS["BAYER_GB8"],
PIXEL_FORMATS["BAYER_GR8"],
PIXEL_FORMATS["BAYER_RG8"],
0x01100011, # BayerBG10
0x01100010, # BayerGB10
0x0110000E, # BayerGR10
0x0110000F, # BayerRG10
0x01100017, # BayerBG12
0x01100016, # BayerGB12
0x01100014, # BayerGR12
0x01100015, # BayerRG12
# YUV格式
PIXEL_FORMATS["YUV422"],
PIXEL_FORMATS["YUV422_YUYV"],
# RGB格式
PIXEL_FORMATS["RGB8"],
0x0220001E, # RGB10_Packed
0x02300020, # RGB12_Packed
0x02400021, # RGB16_Packed
0x02180032 # BGR8_Packed
]
return enGvspPixelType in color_formats
def get_pixel_format_name(pixel_value):
"""根据像素值获取可读的名称"""
for name, value in PIXEL_FORMATS.items():
if value == pixel_value:
return name
return f"未知格式: 0x{pixel_value:08X}"
# 强制关闭线程
def async_raise(tid, exctype):
"""安全终止线程"""
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def stop_thread(thread):
"""停止指定线程"""
async_raise(thread.ident, SystemExit)
# 相机操作类
class CameraOperation:
__public_methods__ = [
'open_device', 'close_device', 'start_grabbing', 'stop_grabbing',
'set_trigger_mode', 'set_continue_mode', 'trigger_once',
'get_parameters', 'set_param', 'save_image', 'capture_frame',
'get_current_frame', 'is_frame_available'
]
# 状态码定义
MV_OK = 0
MV_E_CALLORDER = -2147483647
MV_E_PARAMETER = -2147483646
MV_E_NO_DATA = -2147483645
MV_E_SAVE_IMAGE = -2147483644
MV_E_STATE = -2147483643
MV_E_HANDLE = -2147483642
MV_E_SUPPORT = -2147483641
def __init__(self, obj_cam, st_device_list, n_connect_num=0):
"""
初始化相机操作对象
:param obj_cam: 相机对象
:param st_device_list: 设备列表
:param n_connect_num: 连接编号
"""
self.device_handle = None
self.is_open = False
# 确保cam属性存在
self.cam = obj_cam # 使用cam作为相机对象的统一引用
self.obj_cam = obj_cam # 保持旧代码兼容性
self.st_device_list = st_device_list
self.n_connect_num = n_connect_num
# 状态标志 - 确保所有属性初始化
self.b_open_device = False
self.b_start_grabbing = False
self.b_thread_running = False
self.b_exit = False
self.connected = False # 连接状态标志
self.b_frame_received = False # 帧接收状态
# 线程相关
self.h_thread_handle = None
# 图像数据
self.st_frame_info = None
self.buf_save_image = None
self.n_save_image_size = 0
self.current_frame = None
# 参数
self.frame_rate = 0.0
self.exposure_time = 0.0
self.gain = 0.0
# 线程安全锁
self.buf_lock = threading.Lock() # 图像缓冲区锁
self.frame_lock = threading.Lock() # 当前帧锁
self.param_lock = threading.Lock() # 参数锁
self.frame_count = 0 # 帧计数
self.last_frame_time = None # 最后帧时间
self.is_streaming = False # 取流状态
logging.info("相机操作对象初始化完成")
def is_frame_available(self):
"""检查是否有可用帧图像"""
try:
# 检查是否有有效帧图像
with self.frame_lock:
return self.current_frame is not None and self.current_frame.size > 0
except Exception as e:
logging.error(f"检查帧可用性失败: {str(e)}")
return False
def capture_frame(self):
"""捕获当前帧"""
try:
with self.frame_lock:
if self.current_frame is not None:
return self.current_frame.copy()
return None
except Exception as e:
logging.error(f"捕获帧失败: {str(e)}")
return None
def get_current_frame(self):
"""获取当前帧图像"""
try:
with self.frame_lock:
if self.current_frame is not None:
return self.current_frame.copy()
return None
except Exception as e:
logging.error(f"获取图像失败: {str(e)}")
return None
def get_frame_info(self):
"""
获取当前帧的详细信息
:return: 帧信息字典
"""
info = {
"available": False,
"width": 0,
"height": 0,
"format": "未知",
"size": 0
}
try:
with self.frame_lock:
if self.current_frame is not None:
info["available"] = True
info["width"] = self.current_frame.shape[1]
info["height"] = self.current_frame.shape[0]
info["format"] = f"{self.current_frame.dtype}"
info["size"] = self.current_frame.size
if self.st_frame_info:
info["sdk_width"] = self.st_frame_info.nWidth
info["sdk_height"] = self.st_frame_info.nHeight
info["sdk_format"] = get_pixel_format_name(self.st_frame_info.enPixelType)
info["sdk_size"] = self.st_frame_info.nFrameLen
except Exception as e:
logging.exception(f"获取帧信息异常: {str(e)}")
return info
def set_pixel_format(self, format_name):
if format_name in PIXEL_FORMATS:
format_value = PIXEL_FORMATS[format_name]
ret = self.obj_cam.MV_CC_SetPixelType(format_value)
if ret != self.MV_OK:
logging.error(f"设置像素格式失败: {hex(ret)}")
return ret
else:
logging.error(f"不支持的像素格式: {format_name}")
return self.MV_E_PARAMETER
def save_frame_to_file(self, frame, file_path, save_format="bmp", quality=95):
"""
将帧保存到文件
:param frame: 要保存的帧 (numpy数组)
:param file_path: 文件路径
:param save_format: 保存格式 (bmp, jpg, png, tiff)
:param quality: 保存质量 (仅对jpg有效)
:return: 保存结果 (MV_OK 或 错误码)
"""
if frame is None or frame.size == 0:
logging.error("无法保存无效帧: 帧为空")
return self.MV_E_NO_DATA
try:
# 确保目录存在
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
os.makedirs(directory, exist_ok=True)
# 根据格式保存图像
save_format = save_format.lower()
if save_format == "bmp":
cv2.imwrite(file_path, frame)
elif save_format in ["jpg", "jpeg"]:
cv2.imwrite(file_path, frame, [cv2.IMWRITE_JPEG_QUALITY, quality])
elif save_format == "png":
cv2.imwrite(file_path, frame, [cv2.IMWRITE_PNG_COMPRESSION, 9])
elif save_format in ["tiff", "tif"]:
cv2.imwrite(file_path, frame, [cv2.IMWRITE_TIFF_COMPRESSION, 1])
else:
logging.error(f"不支持的图像格式: {save_format}")
return self.MV_E_PARAMETER
# 验证保存结果
if not os.path.exists(file_path):
logging.error(f"文件保存失败: {file_path}")
return self.MV_E_SAVE_IMAGE
file_size = os.path.getsize(file_path)
if file_size < 1024: # 小于1KB视为无效
logging.error(f"文件大小异常: {file_path} ({file_size} 字节)")
os.remove(file_path) # 删除无效文件
return self.MV_E_SAVE_IMAGE
logging.info(f"图像已保存: {file_path} ({file_size} 字节)")
return self.MV_OK
except Exception as e:
logging.exception(f"保存图像异常: {str(e)}")
return self.MV_E_SAVE_IMAGE
def start_grabbing(self, hwnd=None):
"""开始取流"""
ret = self.cam.start_grabbing(hwnd)
if ret == MV_OK:
self.is_grabbing = True
return ret
def stop_grabbing(self):
"""停止取流"""
# 关键修复:添加状态检查
if not hasattr(self, 'cam') or self.cam is None:
logging.error("无法停止采集:相机对象不存在")
return self.MV_E_STATE
if not self.connected:
logging.error("无法停止采集:相机未连接")
return self.MV_E_STATE
try:
ret = self.cam.stop_grabbing()
if ret == MV_OK:
self.is_grabbing = False
self.last_frame = None # 清除最后帧
self.image_buffer = [] # 清空缓冲区
return ret
except Exception as e:
logging.exception(f"停止采集时发生异常: {e}")
return self.MV_E_STATE
def is_ready(self):
"""检查设备是否就绪"""
return self.b_open_device and not self.b_exit
def open_device(self, device_index=0, access_mode=0):
"""打开相机设备"""
if self.b_open_device:
logging.warning("设备已打开,无需重复操作")
return self.MV_OK
if self.n_connect_num < 0:
logging.error("无效的连接编号")
return self.MV_E_CALLORDER
try:
# 选择设备并创建句柄
nConnectionNum = int(self.n_connect_num)
stDeviceList = cast(self.st_device_list.pDeviceInfo[int(nConnectionNum)],
POINTER(MV_CC_DEVICE_INFO)).contents
# 确保相机对象存在
if self.obj_cam is None:
self.obj_cam = MvCamera()
self.cam = self.obj_cam # 确保cam属性存在
# 创建相机句柄
ret = self.obj_cam.MV_CC_CreateHandle(stDeviceList)
if ret != self.MV_OK:
self.obj_cam.MV_CC_DestroyHandle()
logging.error(f"创建句柄失败: {hex(ret)}")
return ret
# 打开设备
ret = self.obj_cam.MV_CC_openDevice()
if ret != self.MV_OK:
logging.error(f"打开设备失败: {hex(ret)}")
return ret
# 设备已成功打开
self.b_open_device = True
self.connected = True # 设置连接状态
self.b_exit = False
logging.info("设备打开成功")
# 探测网络最佳包大小(仅对GigE相机有效)
if stDeviceList.nTLayerType in [MV_GIGE_DEVICE, MV_GENTL_GIGE_DEVICE]:
nPacketSize = self.obj_cam.MV_CC_GetOptimalPacketSize()
if int(nPacketSize) > 0:
ret = self.obj_cam.MV_CC_SetIntValue("GevSCPSPacketSize", nPacketSize)
if ret != self.MV_OK:
logging.warning(f"设置包大小失败: {hex(ret)}")
else:
logging.warning(f"获取最佳包大小失败: {hex(nPacketSize)}")
# 获取采集帧率使能状态
stBool = c_bool(False)
ret = self.obj_cam.MV_CC_GetBoolValue("AcquisitionFrameRateEnable", stBool)
if ret != self.MV_OK:
logging.warning(f"获取帧率使能状态失败: {hex(ret)}")
# 现在设备已打开,可以安全设置触发模式
ret = self.set_continue_mode()
if ret != self.MV_OK:
logging.warning(f"设置连续模式失败: {hex(ret)}")
return self.MV_OK
except Exception as e:
logging.exception(f"打开设备异常: {str(e)}")
# 尝试清理资源
try:
if hasattr(self, 'obj_cam'):
self.obj_cam.MV_CC_CloseDevice()
self.obj_cam.MV_CC_DestroyHandle()
except:
pass
self.b_open_device = False
self.connected = False
return self.MV_E_STATE
def close_device(self):
"""关闭相机设备"""
if not self.b_open_device:
logging.warning("设备未打开,无需关闭")
return self.MV_OK
try:
# 停止采集(如果正在采集)
if self.b_start_grabbing:
self.stop_grabbing()
# 关闭设备
ret = self.obj_cam.MV_CC_CloseDevice()
if ret != self.MV_OK:
logging.error(f"关闭设备失败: {hex(ret)}")
# 销毁句柄
ret = self.obj_cam.MV_CC_DestroyHandle()
if ret != self.MV_OK:
logging.error(f"销毁句柄失败: {hex(ret)}")
self.b_open_device = False
self.connected = False # 重置连接状态
self.b_exit = True
logging.info("设备关闭成功")
return self.MV_OK
except Exception as e:
logging.exception(f"关闭设备异常: {str(e)}")
return self.MV_E_STATE
def start_grabbing(self, winHandle=None):
"""开始图像采集"""
if not self.b_open_device:
logging.error("设备未打开,无法开始采集")
return self.MV_E_CALLORDER
if self.b_start_grabbing:
logging.warning("采集已在进行中")
return self.MV_OK
try:
# 开始采集
ret = self.obj_cam.MV_CC_StartGrabbing()
if ret != self.MV_OK:
logging.error(f"开始采集失败: {hex(ret)}")
return ret
self.b_start_grabbing = True
self.b_exit = False
# 启动采集线程
self.h_thread_handle = threading.Thread(
target=self.work_thread,
args=(winHandle,),
daemon=True
)
self.h_thread_handle.start()
self.b_thread_running = True
logging.info("图像采集已开始")
return self.MV_OK
except Exception as e:
logging.exception(f"开始采集异常: {str(e)}")
return self.MV_E_STATE
def set_trigger_mode(self, enable=True, source=MV_TRIGGER_SOURCE_SOFTWARE):
"""
设置触发模式
:param enable: 是否启用触发模式
:param source: 触发源 (默认软件触发)
:return: 操作结果
"""
if not self.b_open_device:
logging.error("设备未打开,无法设置触发模式")
return self.MV_E_CALLORDER
try:
mode = MV_TRIGGER_MODE_ON if enable else MV_TRIGGER_MODE_OFF
ret = self.obj_cam.MV_CC_SetEnumValue("TriggerMode", mode)
if ret != self.MV_OK:
logging.error(f"设置触发模式失败: {hex(ret)}")
return ret
if enable:
ret = self.obj_cam.MV_CC_SetEnumValue("TriggerSource", source)
if ret != self.MV_OK:
logging.error(f"设置触发源失败: {hex(ret)}")
return ret
logging.info(f"触发模式设置成功: {'启用' if enable else '禁用'}")
return self.MV_OK
except Exception as e:
logging.exception(f"设置触发模式异常: {str(e)}")
return self.MV_E_STATE
def set_continue_mode(self):
"""设置连续采集模式"""
# 添加设备状态检查
if not self.b_open_device:
logging.error("设备未打开,无法设置连续模式")
return self.MV_E_CALLORDER
logging.info("尝试设置连续采集模式")
try:
# 禁用触发模式
ret = self.obj_cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF)
if ret != self.MV_OK:
logging.error(f"禁用触发模式失败: {hex(ret)}")
return ret
# 设置采集模式为连续
ret = self.obj_cam.MV_CC_SetEnumValue("AcquisitionMode", 2) # 2表示连续采集
if ret != self.MV_OK:
logging.error(f"设置连续采集模式失败: {hex(ret)}")
return ret
logging.info("连续采集模式设置成功")
return self.MV_OK
except Exception as e:
logging.exception(f"设置连续模式异常: {str(e)}")
return self.MV_E_STATE
def trigger_once(self):
"""执行一次软触发"""
if not self.b_open_device:
logging.error("设备未打开,无法触发")
return self.MV_E_CALLORDER
try:
ret = self.obj_cam.MV_CC_SetCommandValue("TriggerSoftware")
if ret != self.MV_OK:
logging.error(f"软触发失败: {hex(ret)}")
return ret
logging.info("软触发成功")
return self.MV_OK
except Exception as e:
logging.exception(f"软触发异常: {str(e)}")
return self.MV_E_STATE
def get_parameters(self):
"""获取相机参数"""
if not self.b_open_device:
logging.error("设备未打开,无法获取参数")
return self.MV_E_CALLORDER
try:
# 使用锁保护参数访问
with self.param_lock:
# 初始化返回值为整数错误码
return_code = self.MV_OK
# 帧率
stFrameRate = MVCC_FLOATVALUE()
memset(byref(stFrameRate), 0, sizeof(MVCC_FLOATVALUE))
ret = self.obj_cam.MV_CC_GetFloatValue("AcquisitionFrameRate", stFrameRate)
if ret == self.MV_OK:
self.frame_rate = stFrameRate.fCurValue
logging.debug(f"获取帧率成功: {self.frame_rate}")
else:
logging.warning(f"获取帧率失败: {hex(ret)}")
if return_code == self.MV_OK: # 保留第一个错误码
return_code = ret
# 曝光时间
stExposure = MVCC_FLOATVALUE()
memset(byref(stExposure), 0, sizeof(MVCC_FLOATVALUE))
ret = self.obj_cam.MV_CC_GetFloatValue("ExposureTime", stExposure)
if ret == self.MV_OK:
self.exposure_time = stExposure.fCurValue
logging.debug(f"获取曝光时间成功: {self.exposure_time}")
else:
logging.warning(f"获取曝光时间失败: {hex(ret)}")
if return_code == self.MV_OK:
return_code = ret
# 增益
stGain = MVCC_FLOATVALUE()
memset(byref(stGain), 0, sizeof(MVCC_FLOATVALUE))
ret = self.obj_cam.MV_CC_GetFloatValue("Gain", stGain)
if ret == self.MV_OK:
self.gain = stGain.fCurValue
logging.debug(f"获取增益成功: {self.gain}")
else:
logging.warning(f"获取增益失败: {hex(ret)}")
if return_code == self.MV_OK:
return_code = ret
# 返回整数错误码
return return_code
except Exception as e:
logging.exception(f"获取参数异常: {str(e)}")
return self.MV_E_STATE
# 添加兼容拼写错误的方法
def get_parame(self):
"""兼容拼写错误的方法名:get_parame"""
logging.warning("调用get_parame方法 - 可能是拼写错误,建议使用get_parameters()")
return self.get_parameters()
# 添加动态属性处理
def __getattr__(self, name):
# 处理保存方法
if name == "Save_Image":
logging.warning("动态处理Save_Image调用 - 映射到save_image")
return self.save_image
if name == "Save_Bmp":
logging.warning("动态处理Save_Bmp调用 - 映射到save_bmp")
return self.save_bmp
# 处理其他可能的拼写错误
method_map = {
"get_parame": self.get_parame,
"get_parm": self.get_parameters,
"get_parmeter": self.get_parameters,
"get_parma": self.get_parameters,
"GetParameter": self.get_parameters,
"GetParam": self.get_parameters
}
if name in method_map:
logging.warning(f"动态处理{name}调用 - 可能是拼写错误")
return method_map[name]
# 关键修复:处理b_frame_received属性
if name == "b_frame_received":
logging.warning("动态访问b_frame_received属性 - 提供默认值")
return False
# 关键修复:处理cam属性
if name == "cam":
if not hasattr(self, 'obj_cam') or self.obj_cam is None:
raise AttributeError(
f"'{type(self).__name__}' 对象没有 'cam' 属性。"
"可能原因: 1) 相机未初始化 2) 连接未建立"
)
return self.obj_cam
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
def set_param(self, frame_rate=None, exposure_time=None, gain=None):
"""
设置相机参数
:param frame_rate: 帧率 (None表示不修改)
:param exposure_time: 曝光时间 (None表示不修改)
:param gain: 增益 (None表示不修改)
:return: 操作结果
"""
if not self.b_open_device:
logging.error("设备未打开,无法设置参数")
return self.MV_E_CALLORDER
try:
# 使用锁保护参数修改
with self.param_lock:
# 禁用自动曝光
ret = self.obj_cam.MV_CC_SetEnumValue("ExposureAuto", 0)
if ret != self.MV_OK:
logging.warning(f"禁用自动曝光失败: {hex(ret)}")
# 设置帧率
if frame_rate is not None:
ret = self.obj_cam.MV_CC_SetFloatValue("AcquisitionFrameRate", float(frame_rate))
if ret != self.MV_OK:
logging.error(f"设置帧率失败: {hex(ret)}")
return ret
self.frame_rate = float(frame_rate)
# 设置曝光时间
if exposure_time is not None:
ret = self.obj_cam.MV_CC_SetFloatValue("ExposureTime", float(exposure_time))
if ret != self.MV_OK:
logging.error(f"设置曝光时间失败: {hex(ret)}")
return ret
self.exposure_time = float(exposure_time)
# 设置增益
if gain is not None:
ret = self.obj_cam.MV_CC_SetFloatValue("Gain", float(gain))
if ret != self.MV_OK:
logging.error(f"设置增益失败: {hex(ret)}")
return ret
self.gain = float(gain)
logging.info(f"参数设置成功: 帧率={self.frame_rate}, 曝光={self.exposure_time}, 增益={self.gain}")
return self.MV_OK
except Exception as e:
logging.exception(f"设置参数异常: {str(e)}")
return self.MV_E_STATE
def work_thread(self, winHandle=None):
"""图像采集工作线程"""
stOutFrame = MV_FRAME_OUT()
memset(byref(stOutFrame), 0, sizeof(stOutFrame))
logging.info("采集线程启动")
while not self.b_exit:
try:
# 获取图像缓冲区
ret = self.obj_cam.MV_CC_GetImageBuffer(stOutFrame, 1000)
if ret != self.MV_OK:
if ret != self.MV_E_NO_DATA: # 忽略无数据错误
logging.warning(f"获取图像缓冲区失败: {hex(ret)}")
time.sleep(0.01)
continue
# 更新帧信息
self.st_frame_info = stOutFrame.stFrameInfo
# 关键修复:标记帧已接收
self.b_frame_received = True
# 记录像素格式信息
pixel_format = get_pixel_format_name(self.st_frame_info.enPixelType)
logging.debug(f"获取到图像: {self.st_frame_info.nWidth}x{self.st_frame_info.nHeight}, "
f"格式: {pixel_format}, 大小: {self.st_frame_info.nFrameLen}字节")
# 分配/更新图像缓冲区
frame_size = stOutFrame.stFrameInfo.nFrameLen
with self.buf_lock:
if self.buf_save_image is None or self.n_save_image_size < frame_size:
self.buf_save_image = (c_ubyte * frame_size)()
self.n_save_image_size = frame_size
# 复制图像数据
cdll.msvcrt.memcpy(byref(self.buf_save_image), stOutFrame.pBufAddr, frame_size)
# 更新当前帧
self.update_current_frame()
# 显示图像(如果指定了窗口句柄)
if winHandle is not None:
stDisplayParam = MV_DISPLAY_FRAME_INFO()
memset(byref(stDisplayParam), 0, sizeof(stDisplayParam))
stDisplayParam.hWnd = int(winHandle)
stDisplayParam.nWidth = self.st_frame_info.nWidth
stDisplayParam.nHeight = self.st_frame_info.nHeight
stDisplayParam.enPixelType = self.st_frame_info.enPixelType
stDisplayParam.pData = self.buf_save_image
stDisplayParam.nDataLen = frame_size
self.obj_cam.MV_CC_DisplayOneFrame(stDisplayParam)
# 释放图像缓冲区
self.obj_cam.MV_CC_FreeImageBuffer(stOutFrame)
except Exception as e:
logging.exception(f"采集线程异常: {str(e)}")
time.sleep(0.1)
# 清理资源
with self.buf_lock:
if self.buf_save_image is not None:
del self.buf_save_image
self.buf_save_image = None
self.n_save_image_size = 0
logging.info("采集线程退出")
def update_current_frame(self):
"""将原始图像数据转换为OpenCV格式并存储"""
if not self.st_frame_info or not self.buf_save_image:
logging.warning("更新当前帧时缺少帧信息或缓冲区")
return
try:
# 获取图像信息
width = self.st_frame_info.nWidth
height = self.st_frame_info.nHeight
pixel_type = self.st_frame_info.enPixelType
# 复制缓冲区数据
with self.buf_lock:
buffer_copy = bytearray(self.buf_save_image)
# 转换为numpy数组
np_buffer = np.frombuffer(buffer_copy, dtype=np.uint8)
# 根据像素类型处理图像
frame = None
if is_mono_data(pixel_type):
# 单色图像
frame = np_buffer.reshape(height, width)
elif pixel_type == PIXEL_FORMATS["BAYER_BG8"]:
# Bayer BG格式
frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerBG2RGB)
elif pixel_type == PIXEL_FORMATS["BAYER_GB8"]:
# Bayer GB格式
frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerGB2RGB)
elif pixel_type == PIXEL_FORMATS["BAYER_GR8"]:
# Bayer GR格式
frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerGR2RGB)
elif pixel_type == PIXEL_FORMATS["BAYER_RG8"]:
# Bayer RG格式
frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerRG2RGB)
elif pixel_type == PIXEL_FORMATS["RGB8"]:
# RGB格式
frame = np_buffer.reshape(height, width, 3)
elif pixel_type in [PIXEL_FORMATS["YUV422"], PIXEL_FORMATS["YUV422_YUYV"]]:
# YUV格式
frame = cv2.cvtColor(np_buffer.reshape(height, width, 2), cv2.COLOR_YUV2RGB_YUYV)
else:
# 尝试自动处理其他格式
if pixel_type in [PIXEL_FORMATS["MONO10"], PIXEL_FORMATS["MONO12"]]:
# 10位或12位单色图像需要特殊处理
frame = self.process_high_bit_depth(np_buffer, width, height, pixel_type)
else:
logging.warning(f"不支持的像素格式: {get_pixel_format_name(pixel_type)}")
return
# 更新当前帧 - 使用线程安全锁
with self.frame_lock:
self.current_frame = frame
logging.debug(f"当前帧更新成功: {frame.shape if frame is not None else '无数据'}")
except Exception as e:
logging.exception(f"更新当前帧异常: {str(e)}")
# 调试:保存原始数据用于分析
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
debug_path = f"frame_debug_{timestamp}.bin"
with open(debug_path, "wb") as f:
f.write(buffer_copy)
logging.info(f"已保存原始帧数据到: {debug_path}")
except:
logging.error("保存调试帧数据失败")
def process_high_bit_depth(self, buffer, width, height, pixel_type):
"""处理高位深度图像格式"""
try:
# 10位或12位图像处理
if pixel_type == PIXEL_FORMATS["MONO10"]:
# 将10位数据转换为16位
data_16bit = np.frombuffer(buffer, dtype=np.uint16)
# 10位数据存储方式:每个像素占用2字节,但只有10位有效
data_16bit = (data_16bit >> 6) # 右移6位使10位数据对齐到低10位
frame = data_16bit.reshape(height, width).astype(np.uint16)
elif pixel_type == PIXEL_FORMATS["MONO12"]:
# 将12位数据转换为16位
data_16bit = np.frombuffer(buffer, dtype=np.uint16)
# 12位数据存储方式:每个像素占用2字节,但只有12位有效
data_16bit = (data_16bit >> 4) # 右移4位使12位数据对齐到低12位
frame = data_16bit.reshape(height, width).astype(np.uint16)
else:
logging.warning(f"不支持的高位深度格式: {get_pixel_format_name(pixel_type)}")
return None
# 归一化到8位用于显示(如果需要)
frame_8bit = cv2.convertScaleAbs(frame, alpha=(255.0/4095.0))
return frame_8bit
except Exception as e:
logging.exception(f"处理高位深度图像异常: {str(e)}")
return None
def save_image(self, file_path, save_format="bmp", quality=95):
"""
安全保存当前帧到文件 - 使用原始缓冲区数据
"""
if not self.b_open_device or not self.b_start_grabbing:
logging.error("设备未就绪,无法保存图像")
return self.MV_E_CALLORDER
# 使用缓冲区锁确保数据一致性
with self.buf_lock:
if not self.buf_save_image or not self.st_frame_info:
logging.error("无可用图像数据")
return self.MV_E_NO_DATA
# 获取图像信息
width = self.st_frame_info.nWidth
height = self.st_frame_info.nHeight
pixel_type = self.st_frame_info.enPixelType
frame_size = self.st_frame_info.nFrameLen
# 复制缓冲区数据
buffer_copy = bytearray(self.buf_save_image)
try:
# 确保目录存在
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
os.makedirs(directory, exist_ok=True)
logging.info(f"创建目录: {directory}")
# 根据像素类型处理图像
np_buffer = np.frombuffer(buffer_copy, dtype=np.uint8)
# 根据像素格式转换图像
if is_mono_data(pixel_type):
# 单色图像
frame = np_buffer.reshape(height, width)
elif pixel_type == PIXEL_FORMATS["BAYER_BG8"]:
# Bayer BG格式
frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerBG2BGR)
elif pixel_type == PIXEL_FORMATS["BAYER_GB8"]:
# Bayer GB格式
frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerGB2BGR)
elif pixel_type == PIXEL_FORMATS["BAYER_GR8"]:
# Bayer GR格式
frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerGR2BGR)
elif pixel_type == PIXEL_FORMATS["BAYER_RG8"]:
# Bayer RG格式
frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerRG2BGR)
elif pixel_type == PIXEL_FORMATS["RGB8"]:
# RGB格式
frame = np_buffer.reshape(height, width, 3)
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
elif pixel_type in [PIXEL_FORMATS["YUV422"], PIXEL_FORMATS["YUV422_YUYV"]]:
# YUV格式
frame = cv2.cvtColor(np_buffer.reshape(height, width, 2), cv2.COLOR_YUV2BGR_YUYV)
else:
# 尝试自动处理其他格式
if pixel_type in [PIXEL_FORMATS["MONO10"], PIXEL_FORMATS["MONO12"]]:
# 10位或12位单色图像需要特殊处理
frame = self.process_high_bit_depth(np_buffer, width, height, pixel_type)
else:
logging.error(f"不支持的像素格式: {get_pixel_format_name(pixel_type)}")
return self.MV_E_PARAMETER
# 根据格式保存图像
save_format = save_format.lower()
try:
if save_format == "bmp":
cv2.imwrite(file_path, frame)
elif save_format in ["jpg", "jpeg"]:
cv2.imwrite(file_path, frame, [cv2.IMWRITE_JPEG_QUALITY, quality])
elif save_format == "png":
cv2.imwrite(file_path, frame, [cv2.IMWRITE_PNG_COMPRESSION, 9])
elif save_format in ["tiff", "tif"]:
cv2.imwrite(file_path, frame, [cv2.IMWRITE_TIFF_COMPRESSION, 1])
else:
logging.error(f"不支持的图像格式: {save_format}")
return self.MV_E_PARAMETER
# 验证保存结果
if not os.path.exists(file_path):
logging.error(f"文件保存失败: {file_path}")
return self.MV_E_SAVE_IMAGE
file_size = os.path.getsize(file_path)
if file_size < 1024: # 小于1KB视为无效
logging.error(f"文件大小异常: {file_path} ({file_size} 字节)")
os.remove(file_path) # 删除无效文件
return self.MV_E_SAVE_IMAGE
logging.info(f"图像已保存: {file_path} ({file_size} 字节)")
return self.MV_OK
except Exception as e:
logging.exception(f"保存图像异常: {str(e)}")
return self.MV_E_SAVE_IMAGE
except Exception as e:
logging.exception(f"图像处理异常: {str(e)}")
return self.MV_E_SAVE_IMAGE
# 兼容旧方法的保存接口
def save_jpg(self, file_path=None, quality=95):
"""保存为JPEG格式"""
if file_path is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_path = f"capture_{timestamp}.jpg"
return self.save_image(file_path, "jpg", quality)
def save_bmp(self, file_path=None):
"""保存为BMP格式"""
if file_path is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_path = f"capture_{timestamp}.bmp"
return self.save_image(file_path, "bmp")
def save_png(self, file_path=None):
"""保存为PNG格式"""
if file_path is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_path = f"capture_{timestamp}.png"
return self.save_image(file_path, "png")
def save_tiff(self, file_path=None):
"""保存为TIFF格式"""
if file_path is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_path = f"capture_{timestamp}.tiff"
return self.save_image(file_path, "tiff")
# 新增方法:获取帧状态详细信息
def get_frame_status(self):
"""获取当前帧状态详细信息"""
# 安全访问b_frame_received属性
frame_received = getattr(self, 'b_frame_received', False)
status = {
"camera_open": self.b_open_device,
"grabbing_started": self.b_start_grabbing,
"thread_running": self.b_thread_running,
"frame_received": frame_received, # 使用安全访问
"frame_size": self.n_save_image_size if self.buf_save_image else 0,
"current_frame": self.current_frame is not None,
"connected": self.connected # 新增连接状态
}
if self.st_frame_info:
status.update({
"width": self.st_frame_info.nWidth,
"height": self.st_frame_info.nHeight,
"pixel_format": get_pixel_format_name(self.st_frame_info.enPixelType),
"frame_num": self.st_frame_info.nFrameNum
})
return status
# 析构函数确保资源释放
def __del__(self):
"""确保相机资源被正确释放"""
try:
if self.b_start_grabbing:
self.stop_grabbing()
if self.b_open_device:
self.close_device()
except Exception as e:
logging.error(f"析构函数中释放资源失败: {str(e)}")
按照你的办法找到这个文件中的大学修改方法名为小写,并完整展示修改后的代码