在进行图像相关的算法工作时,通常离不开拉取实时摄像头进行拉流工作。在python中,通常使用OPENCV完成解码工作,但OPENCV解码非常占用CPU资源,对于资源受限的边缘设备,占用大量CPU资源显然是我们不愿意看到的。
好在Rockchip官方针对自家的开发版设计了专门的硬件解码芯片,并通过mpp进行硬件解码。不过官方提供的是基于C++开发的示例,着实令没接触过C++的朋友们头疼。
因此,本文重写了官方demo、加入了Zlmediakit进行拉流并通过pybind11将代码封装成so库,现在,在python中直接通过import的方式,就能完成基础的拉流解码操作!!!
废话不多说,先上一个简单的演示:
output
在上述演示中,我写了一个简单的 test.py 用于调用 mpp_decoder 进行拉流以及解码操作,代码如下:
rtsp_stress = ""
import cv2
import time
import mpp_decoder
# args1: rtsp address, args2: video type (support h264, h265)
mpp_decoder.start_processing_async(rtsp_stress, 264)
frame_idx = 0
while True:
frame = mpp_decoder.get_decoded_frame()
if frame.size > 0:
converted_image = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
cv2.imwrite(f"./output/frame_{frame_idx}.jpg", converted_image)
print(f"Received frame with shape: {converted_image.shape}")
frame_idx += 1
else:
print("No frame available")
time.sleep(1)
time.sleep(0.1)
- 使用pybind11封装c++代码思路:
1. start_processing_async函数接收 rtsp 流地址以及流类型
2. 调用 process_video_rtsp 函数使用 Zlmediakit 进行拉流操做
3. Zlmediakit 在监听事件on_track_frame_out进行解码操作:ctx->decoder->Decode()
4. 解码一帧数据后触发回调函数 mpp_decoder_frame_callback
5. 回调函数 mpp_decoder_frame_callback 负责取出数据、放入大小为50的缓冲区
6. python 通过调用 get_decoded_frame 函数取出数据,get_decoded_frame 函数中负责将数据转换成 python numpy 的数据类型并返回给 python
- 具体代码如下:
void mpp_decoder_frame_callback(void *userdata, int width_stride, int height_stride, int width, int height, int format, int fd, void *data)
{
rga_buffer_t origin;
std::vector<size_t> shape = { (size_t)height, (size_t)width, 3 };
std::vector<size_t> strides = { (size_t)width * 3, (size_t)3, 1 };
// 创建 YUV420SP 格式的缓冲区
origin = wrapbuffer_fd(fd, width, height, RK_FORMAT_YCbCr_420_SP, width_stride, height_stride);
cv::Mat origin_mat = cv::Mat::zeros(height, width, CV_8UC3);
rga_buffer_t rgb_img = wrapbuffer_virtualaddr((void *)origin_mat.data, width, height, RK_FORMAT_RGB_888);
imcopy(origin, rgb_img);
// 将解码后的数据存入共享缓冲区
std::lock_guard<std::mutex> lock(buffer_mutex);
if (frame_buffer.size() >= 50)
{
frame_buffer.pop(); // 丢弃旧帧
}
frame_buffer.push(origin_mat); // 存储新帧
}
// Python 端从缓冲区读取数据的接口
py::array_t<uint8_t> get_decoded_frame()
{
std::lock_guard<std::mutex> lock(buffer_mutex);
if (frame_buffer.empty())
{
return py::array_t<uint8_t>(); // 如果没有数据则返回空数组
}
cv::Mat frame = frame_buffer.front();
frame_buffer.pop();
// 将 cv::Mat 转换为 NumPy 数组
py::array_t<uint8_t> result = py::array_t<uint8_t>(
{frame.rows, frame.cols, 3},
{frame.cols * 3, 3, 1},
frame.data
);
return result;
}
void API_CALL on_track_frame_out(void *user_data, mk_frame frame) {
rknn_zl_app_context_t *ctx = (rknn_zl_app_context_t *) user_data;
// printf("on_track_frame_out ctx=%p\n", ctx);
const char *data = mk_frame_get_data(frame); // 获取帧数据指针
ctx->dts = mk_frame_get_dts(frame); // 获取解码时间戳,单位毫秒
ctx->pts = mk_frame_get_pts(frame); // 获取显示时间戳,单位毫秒
size_t size = mk_frame_get_data_size(frame); // 获取帧数据指针长度
// printf("decoder=%p\n", ctx->decoder);
ctx->decoder->Decode((uint8_t *)data, size, 0); // 调用decoder解码器进行解码
}
void API_CALL on_mk_play_event_func(void *user_data, int err_code, const char *err_msg, mk_track tracks[], int track_count)
{
rknn_zl_app_context_t *ctx = (rknn_zl_app_context_t *) user_data; // 将 user_data 转换成 Zlmediakit Context 结构体指针
if (err_code == 0)
{
log_debug("play success!");
int i;
for (i = 0; i < track_count; ++i)
{
if (mk_track_is_video(tracks[i]))
{
// 监听 frame 的输出事件,参数:当前音视频轨道, 回调函数on_track_frame_out,用户数据指针
mk_track_add_delegate(tracks[i], on_track_frame_out, user_data);
}
}
}
else
{
log_warn("play failed: %d %s", err_code, err_msg);
}
}
int process_video_rtsp(rknn_zl_app_context_t *ctx, const char *url)
{
mk_config config; // zlmediakit config配置,默认即可
memset(&config, 0, sizeof(mk_config));
config.log_mask = LOG_CONSOLE; // 设置日志输出的路径
mk_env_init(&config); // 使用 config 配置初始化 zlmediakit 环境
mk_player player = mk_player_create();
//设置播放器正常播放/播放中断的回调函数
mk_player_set_on_result(player, on_mk_play_event_func, ctx);
mk_player_set_on_shutdown(player, on_mk_play_event_func, ctx);
// 开始播放 url
mk_player_play(player, url);
while(true)
{
std::this_thread::sleep_for(std::chrono::seconds(10));
}
}
void start_processing_async(const std::string& rtsp_address, int video_type)
{
std::thread([rtsp_address, video_type]()
{
rknn_zl_app_context_t app_ctx;
memset(&app_ctx, 0, sizeof(rknn_zl_app_context_t));
if (app_ctx.decoder == NULL)
{
MppDecoder* decoder = new MppDecoder();
decoder->Init(video_type, 30, &app_ctx);
decoder->SetCallback(mpp_decoder_frame_callback);
app_ctx.decoder = decoder;
}
// 使用 std::string 的 c_str(),确保 rtsp_address 在线程内是有效的
process_video_rtsp(&app_ctx, rtsp_address.c_str());
if (app_ctx.decoder != nullptr)
{
delete app_ctx.decoder;
app_ctx.decoder = nullptr;
}
}).detach(); // 分离线程,让它后台执行
}
PYBIND11_MODULE(mpp_decoder, m)
{
// 绑定 start_processing_async 函数
m.def("start_processing_async", [](const std::string &rtsp_address, int video_type){
start_processing_async(rtsp_address, video_type);
}, "Start processing RTSP stream with specified URL and video type");
// 绑定 get_decoded_frame 函数
m.def("get
上述所有代码都运行在docker容器中,在docker容器中只需要export 一下so库的路径即可。
export PYTHONPATH=/mnt/mpp_decoder/build:$PYTHONPATH
目前仅支持单摄像头拉流,如果问题,欢迎评论私信!