Linux系统下海康工业相机MVS二次开发-Python
环境:树莓派 Ubuntu系统
编程环境:Python3.7 Node(忘了版本了,都可以,最好稳定版本)
需要安装的模块:Python端:cv2 websockets fastapi等;Node端:主要是ws(用来传输视频流)
安装可以看网上的文章,很多有写,不过树莓派这里避坑:树莓派进入是pi
用户,需要sudo su
才能切换为root
用户,安装Python模块时如果在pi
用户下安装,用root
用户运行是是没有的,会报no module name...
错误。另外,安装cv2
模块时似乎不能用pip
安装,需要用它自带的apt-get
安装,不过不知道是不是只有我这里才是这样的。
以前上课做过树莓派的实验,很简单,但是真正实践时还是遇到了许多问题和困难,做了接近一周,因为树莓派只有一个网口得和相机换来换去,记录一下工程吧。
先整体介绍一下功能,主要是前端写了一个页面调用python后端相机传送的视频帧,并实现点击按钮拍照功能。
首先,需要在系统安装MVS,这里引用一下其他博主写的文章,可参考:https://blog.csdn.net/cugyzy/article/details/120974745
以下是我写的工程目录:
写代码时参考了许多博主以及MVS官方安装完成后在/opt/MVS/Samples/armhf/Python/GarbImage
下的代码以及官方的对于相机的调用方法文档。
先将各个方法封装在类HKCamera
中,如下:
import sys
from ctypes import *
import os
import numpy as np
import time
import cv2
import random
import copy
sys.path.append("/opt/MVS/Samples/armhf/Python/MvImport") #打开MVS中的MvImport文件,对于不同系统打开的文件路径跟随实际文件路径变化即可
from MvCameraControl_class import * #调用了MvCameraControl_class.py文件
class HKCamera():
def __init__(self, CameraIdx=0, log_path=None):
# enumerate all the camera devices
deviceList = self.enum_devices()
# generate a camera instance
self.camera = self.open_camera(deviceList, CameraIdx, log_path)
self.start_camera()
def __del__(self):
if self.camera is None:
return
# 停止取流
ret = self.camera.MV_CC_StopGrabbing()
if ret != 0:
raise Exception("stop grabbing fail! ret[0x%x]" % ret)
# 关闭设备
ret = self.camera.MV_CC_CloseDevice()
if ret != 0:
raise Exception("close deivce fail! ret[0x%x]" % ret)
# 销毁句柄
ret = self.camera.MV_CC_DestroyHandle()
if ret != 0:
raise Exception("destroy handle fail! ret[0x%x]" % ret)
@staticmethod
def enum_devices(device=0, device_way=False):
"""
device = 0 枚举网口、USB口、未知设备、cameralink 设备
device = 1 枚举GenTL设备
"""
if device_way == False:
if device == 0:
cameraType = MV_GIGE_DEVICE | MV_USB_DEVICE | MV_UNKNOW_DEVICE | MV_1394_DEVICE | MV_CAMERALINK_DEVICE
deviceList = MV_CC_DEVICE_INFO_LIST()
# 枚举设备
ret = MvCamera.MV_CC_EnumDevices(cameraType, deviceList)
if ret != 0:
raise Exception("enum devices fail! ret[0x%x]" % ret)
return deviceList
else:
pass
elif device_way == True:
pass
def open_camera(self, deviceList, CameraIdx, log_path):
# generate a camera instance
camera = MvCamera()
# 选择设备并创建句柄
stDeviceList = cast(deviceList.pDeviceInfo[CameraIdx], POINTER(MV_CC_DEVICE_INFO)).contents
if log_path is not None:
ret = self.camera.MV_CC_SetSDKLogPath(log_path)
if ret != 0:
raise Exception("set Log path fail! ret[0x%x]" % ret)
# 创建句柄,生成日志
ret = camera.MV_CC_CreateHandle(stDeviceList)
if ret != 0:
raise Exception("create handle fail! ret[0x%x]" % ret)
else:
# 创建句柄,不生成日志
ret = camera.MV_CC_CreateHandleWithoutLog(stDeviceList)
if ret != 0:
raise Exception("create handle fail! ret[0x%x]" % ret)
# 打开相机
ret = camera.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0)
if ret != 0:
raise Exception("open device fail! ret[0x%x]" % ret)
return camera
def start_camera(self):
stParam = MVCC_INTVALUE()
memset(byref(stParam), 0, sizeof(MVCC_INTVALUE))
ret = self.camera.MV_CC_GetIntValue("PayloadSize", stParam)
if ret != 0:
raise Exception("get payload size fail! ret[0x%x]" % ret)
self.nDataSize = stParam.nCurValue
self.pData = (c_ubyte * self.nDataSize)()
self.stFrameInfo = MV_FRAME_OUT_INFO_EX()
memset(byref(self.stFrameInfo), 0, sizeof(self.stFrameInfo))
self.camera.MV_CC_StartGrabbing()
def get_Value(self, param_type, node_name):
"""
:param cam: 相机实例
:param_type: 获取节点值得类型
:param node_name: 节点名 可选 int 、float 、enum 、bool 、string 型节点
:return: 节点值
"""
if param_type == "int_value":
stParam = MVCC_INTVALUE_EX()
memset(byref(stParam), 0, sizeof(MVCC_INTVALUE_EX))
ret = self.camera.MV_CC_GetIntValueEx(node_name, stParam)
if ret != 0:
raise Exception("获取 int 型数据 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
return stParam.nCurValue
elif param_type == "float_value":
stFloatValue = MVCC_FLOATVALUE()
memset(byref(stFloatValue), 0, sizeof(MVCC_FLOATVALUE))
ret = self.camera.MV_CC_GetFloatValue(node_name, stFloatValue)
if ret != 0:
raise Exception("获取 float 型数据 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
return stFloatValue.fCurValue
elif param_type == "enum_value":
stEnumValue = MVCC_ENUMVALUE()
memset(byref(stEnumValue), 0, sizeof(MVCC_ENUMVALUE))
ret = self.camera.MV_CC_GetEnumValue(node_name, stEnumValue)
if ret != 0:
raise Exception("获取 enum 型数据 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
return stEnumValue.nCurValue
elif param_type == "bool_value":
stBool = c_bool(False)
ret = self.camera.MV_CC_GetBoolValue(node_name, stBool)
if ret != 0:
raise Exception("获取 bool 型数据 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
return stBool.value
elif param_type == "string_value":
stStringValue = MVCC_STRINGVALUE()
memset(byref(stStringValue), 0, sizeof(MVCC_STRINGVALUE))
ret = self.camera.MV_CC_GetStringValue(node_name, stStringValue)
if ret != 0:
raise Exception("获取 string 型数据 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
return stStringValue.chCurValue
else:
return None
def set_Value(self, param_type, node_name, node_value):
"""
:param cam: 相机实例
:param param_type: 需要设置的节点值得类型
int:
float:
enum: 参考于客户端中该选项的 Enum Entry Value 值即可
bool: 对应 0 为关,1 为开
string: 输入值为数字或者英文字符,不能为汉字
:param node_name: 需要设置的节点名
:param node_value: 设置给节点的值
:return:
"""
if param_type == "int_value":
ret = self.camera.MV_CC_SetIntValueEx(node_name, int(node_value))
if ret != 0:
raise Exception("设置 int 型数据节点 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
elif param_type == "float_value":
ret = self.camera.MV_CC_SetFloatValue(node_name, float(node_value))
if ret != 0:
raise Exception("设置 float 型数据节点 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
elif param_type == "enum_value":
ret = self.camera.MV_CC_SetEnumValue(node_name, node_value)
if ret != 0:
raise Exception("设置 enum 型数据节点 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
elif param_type == "bool_value":
ret = self.camera.MV_CC_SetBoolValue(node_name, node_value)
if ret != 0:
raise Exception("设置 bool 型数据节点 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
elif param_type == "string_value":
ret = self.camera.MV_CC_SetStringValue(node_name, str(node_value))
if ret != 0:
raise Exception("设置 string 型数据节点 %s 失败 ! 报错码 ret[0x%x]" % (node_name, ret))
def set_exposure_time(self, exp_time):
self.set_Value(param_type="float_value", node_name="ExposureTime", node_value=exp_time)
def get_exposure_time(self):
return self.get_Value(param_type="float_value", node_name="ExposureTime")
def get_image(self, width=None):
"""
:param cam: 相机实例
:active_way:主动取流方式的不同方法 分别是(getImagebuffer)(getoneframetimeout)
:return:
"""
ret = self.camera.MV_CC_GetOneFrameTimeout(self.pData, self.nDataSize, self.stFrameInfo, 1000)
if ret == 0:
#image = np.asarray(self.pData).reshape((self.stFrameInfo.nHeight, self.stFrameInfo.nWidth))
image = np.asarray(self.pData)
if width is not None:
image = cv2.resize(image, (width, int(self.stFrameInfo.nHeight * width / self.stFrameInfo.nWidth)))
num = random.randint(1,10)
cv2.putText(image, str(num), (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.5, 255, 1)
pass
return image
else:
return None
#实时展示
def show_runtime_info(self, image):
# exp_time = self.get_exposure_time()
#cv2.putText(image, ("exposure time = %1.1fms" % (exp_time * 0.001)), (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.5, 255, 1)
num = random.randint(1,10)
cv2.putText(image, str(num), (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.5, 255, 1)
#拍照存储照片
def take_picture(self,image):
#print("take picture!")
path = "/home/pi/Downloads"
if not os.path.exists(path):
os.mkdir(path)
random_str = ""
base_str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
length = len(base_str)-1
for i in range(8):
random_str += base_str[random.randint(0,length)]
file_path = path+"/"+random_str+".jpg"
cv2.imwrite(file_path,image)
return file_path
"""
bmpsize = self.stFrameInfo.nWidth * self.stFrameInfo.nHeight * 3 + 54
bmp_buf = (c_ubyte * bmpsize)()
path = "/home/pi/Downloads"
if not os.path.exists(path):
os.mkdir(path)
random_str = ""
base_str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
length = len(base_str)-1
for i in range(8):
random_str += base_str[random.randint(0,length)]
file_path = path+"/"+random_str+".bmp"
print(file_path)
file_open = open(file_path.encode('ascii'), 'wb+')
try:
img_buff = copy.deepcopy(bmp_buf)
file_open.write(img_buff, )
res = True
except:
raise Exception("save file executed failed")
finally:
file_open.close()
"""
再写一个Server做为后端服务,并实时传送视频帧:
# websocket
import sys
import cv2
import numpy as np
import asyncio
from threading import Thread
import websockets
import base64
sys.path.append("/home/pi/Desktop/HKtest/HKProject/modules") #打开MVS中的MvImport文件,对于不同系统打开的文件路径跟随实际文件路径变化即可
from HKCamera import *
from fastapi import FastAPI
import uvicorn
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
#设置允许访问的域名
origins = ["*"] #也可以设置为"*",即为所有。
#设置跨域传参
app.add_middleware(
CORSMiddleware,
allow_origins=origins, #设置允许的origins来源00
allow_credentials=True,
allow_methods=["*"], # 设置允许跨域的http方法,比如 get、post、put等。
allow_headers=["*"]) #允许跨域的headers,可以用来鉴别来源等作用。
#isTakePicture = False
#isDownTake = False
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),95]
camera = HKCamera()
# 向服务器端实时发送视频帧
async def send_msg(websocket):
#global isTakePicture,isDownTake
try:
while True:
image = camera.get_image(width=800)
if image is not None:
# camera.show_runtime_info(image)
# cv2.imshow("", image)
# 图像编码
result, imgencode = cv2.imencode('.jpg', image, encode_param)
data = np.array(imgencode)
img = data.tostring()
# base64编码传输
img = base64.b64encode(img).decode()
await websocket.send("data:image/jpeg;base64," + img)
except Exception as e:
print(e)
# 客户端主逻辑
async def main_logic():
async with websockets.connect('ws://127.0.0.1:3000') as websocket:
await send_msg(websocket)
#loop = asyncio.get_event_loop()
#result = loop.run_until_complete(main_logic())
def start_thread_loop(loop):
asyncio.set_event_loop(loop)
loop.run_until_complete(main_logic())
#=================next is web server========================
@app.get("/server")
def takePicture():
image = camera.get_image(width=800)
if image is not None:
file_path = camera.take_picture(image)
msg = ""
if file_path != None:
msg = "Picture has been saved!"
else:
msg = "saved fail!"
return {"msg":msg,"file_path":file_path}
if __name__ == '__main__':
new_loop = asyncio.new_event_loop()
t = Thread(target=start_thread_loop,args=(new_loop,))
t.start()
uvicorn.run(app, host="127.0.0.1", port=8000)
写这部分代码时还是遇到了许多问题,因为要实时传输视频帧图片,所以又要同时监听前端发送的请求是不能同时进行的,因此需要用到多线程的知识,将视频帧图片传送作为子线程,后端服务作为主线程,因此可以同时进行且互不干扰。这里学到了一个新东西–协程
,Python有一个包是asyncio
,里面可以进行多协程的开发,这里也是遇到问题后通过这个思想受到启发后才解决了问题,后面可以再多研究研究。
前端部分:
首先是node服务,需要开启websocket服务并在收到视频帧后将内容发送到各个客户端:
let WebSocketServer = require('ws').Server,
wss = new WebSocketServer({ port: 3000 });
wss.on('connection', function (ws) {
console.log('客户端已连接');
// console.log(ws);
// ws.on('message', function (message) {
// wss.clients.forEach(function each(client) {
// let data = [{msgtest:"sfsdfuisdj=====",message:message}];
// let data = [{msgtest:"sfsdfuisdj====="}];
// client.send(data);
// console.log(data);
// });
// console.log(message.length);
// console.log(message);
// });
ws.onmessage = function(evt){
wss.clients.forEach(function each(client){
client.send(evt.data);
});
console.log(evt.data.length);
}
});
之前看了一些博客是用的上面注释的写法,但我在运行时却显示不出图片,因为页面接收到的是二进制文件,读不出来,改为下面这种写法就好了,直接传送的是base64。
页面代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<img id="resImg" src="" />
</div>
<div id="test">
<button id="take">点击拍照</button>
</div>
<script src="jquery.min.js"></script>
<script>
let ws = new WebSocket("ws://127.0.0.1:3000/");
var image = document.getElementById("resImg");
//var divtest = document.getElementById("test");
ws.onopen = function (evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function (evt) {
//console.log(evt)
//console.log("==="+evt.data);
//console.log("==="+JSON.stringify(evt.data));
//divtest.innerHTML=evt;
image.setAttribute("src", evt.data);
//$("#resImg").attr("src", evt.data);
// console.log("Received Message: " + JSON.stringify(evt.data));
// ws.close();
};
ws.onclose = function (evt) {
console.log("Connection closed.");
};
let btn = document.getElementById('take');
btn.onclick = function () {
//创建对象
const xhr = new XMLHttpRequest()
//设置请求方法和url
xhr.open('GET', 'http://localhost:8000/server')
//发送
xhr.send()
//事件绑定 处理服务端返回的结果
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { //服务端返回了所有结果
if (xhr.status >= 200 && xhr.readyState <= 300) { //2开头的都表示成功
//处理结果
console.log(xhr.status) //状态码
console.log(xhr.statusText) //状态字符串
console.log(xhr.getAllResponseHeaders) //所有响应头
console.log(xhr.response) //响应体
alert(xhr.response)
}
}
}
}
</script>
</body>
</html>
启动node服务命令:node websocket.js
运行python文件命令:python HKCameraServer.py
【参考】https://blog.csdn.net/weixin_42613125/article/details/121089120
【参考】https://blog.csdn.net/qq_39570716/article/details/114066097?spm=1001.2014.3001.5501
【参考】https://www.jianshu.com/p/e3933a56285f
【参考】https://blog.csdn.net/qq_23107577/article/details/113984935?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-4-113984935-blog-123371414.pc_relevant_antiscanv3&spm=1001.2101.3001.4242.3&utm_relevant_index=7
【参考】https://blog.csdn.net/zhuzheqing/article/details/109819702
【参考】https://blog.csdn.net/qq_36917144/article/details/117292871?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2-117292871-blog-109819702.pc_relevant_downloadblacklistv1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2-117292871-blog-109819702.pc_relevant_downloadblacklistv1&utm_relevant_index=4