用Rose构建需要OpenCV的跨平台app

对OpenCV的dnn模块,考虑到Rose已完全内置了TensorFlow Lite,dnn模块只包含微信二维码引擎需要的caffe。

虽然OpenCV在机器学习、运动识别方面不如其它sdk,但处理图像几乎已成标准。既然决定内置,目标就不能局限于用OpenCV研究算法、验证论文,而是要直接面向产品,要把OpenCV无缝融入工具链,从而简化移动设备编程,确保移动设备app的执行效率。

一、SDL_Surface和Mat之间几近零成本互换

app要融合OpenCV,首先要解决如何让Mat和操作系统用于显示的单元之间的相互转换。Rose基于SDL,是一个叫SDL_Surface的结构表示内存中图像。SDL_Surface是个C结构,不方便引用计数编程,Rose于是引入surface封装SDL_Surface,问题就具体化到如何在surface和Mat间互换。由于图像数据量大,互换必须避免复制像素数据。下图概述了互换涉及到操作。

转换1:Mat转为surface

Mat内容包括两部分,一是矩阵头,二是像素数据(data),使用引用计数机制管理data。沿袭引用计数方案,让在surface维持一个Mat,使得这个mat有一个额外引用计数!这样让看两种情况,1)Mat方面已清空所有mat,由于surface那边还有一个,只有那个surface被释放才清空这mat。2)surface方面先到0,这个额外计数就没了,问题就回到传统只由Mat管理引用计数,等这个mat没对象了释放data。

struct surface {
	surface(const cv::Mat& mat)
		: surface_(SDL_CreateRGBSurfaceFrom(mat.data, mat.cols, mat.rows, mat.channels() * 8, mat.channels() * mat.cols, 0xFF0000, 0xFF00, 0xFF, 0xFF000000)) {
		mat_ = new cv::Mat;
		*mat_ = mat;
	}
	......
	SDL_Surface* surface_;
	cv::Mat* mat_;
};

上面代码是具体实现,surface提供一个输入参数是Mat的构造函数,一旦是用这函数构造的surface时,mat_成员变量将有效。mat_是new操作生成的,既不是直接指针赋值(这不会增加引用计数),也不是mat.clone(这是另建data了)。注意,要转换的mat的像素格式必须是ARGB,而且当两侧都有计数时,不要改变图像尺寸。

转换2:surface转为Mat。

为避免复制数据,需要使用带data的Mat构造函数。

Mat(int rows, int cols, int type, void* data, size_t step);

有人或许认为把surface的像数数据传入data,然后用上面说的额外引用计数就行了。——不行的,因为Mat不会释放此时的data,即使引用计算已是0!这里抄下data参数语义。

"data。指向用户数据的指针。构造函数传入data和step参数不分配矩阵数据。相反,它们只是初始化矩阵头指向指定的数据,这意味着没有数据复制。此操作很高效,可以用来处理使用OpenCV函数的外部数据。外部数据不会自动释放,所以你应该小心处理它。"

由于这个不释放,为确保转换出的mat中数据有效,就要求surf的生命期要超过mat!要同时做到生命期不超过和编程简洁,可使用class的构造、析构机制。

struct tsurface_2_mat_lock {
	tsurface_2_mat_lock(surface& surf)
		: surface_(surf)
	{ mat = cv::Mat(surf->h, surf->w, CV_8UC4, surf->pixels); }
	cv::Mat mat;
};

  tsurface_2_mat_lock(surf)构造出一个对象,该对象的mat成员存储着转换出的mat,后续代码就用lock.mat作为OpenCV函数的输入参数,以及存放处理结果。一旦不需要了,lock的析构函数就自动把mat释放。注意,当是存储处理结果时,不要改变mat中的尺寸和像素格式。

接下让说说像素格式。Rose内部统一使用SDL_PIXELFORMAT_ARGB8888作为中间格式,ARGB是作为uint32_t时说的,具体到字节序(小端先存)是B、G、R、A,这正是OpenCV的默认分量排序。由于这个ARGB8888,转到灰度图像的函数调用是以下语句。

cv::cvtColor(lock.mat, gray, cv::COLOR_BGRA2GRAY);

二、highgui、imgcodec和videoio

OpenCV中代码可分为两类,一是算法相关代码、二是平台辅助代码。对算法代码,内置OpenCV时遵循一条原则:算法代码要保持兼容,即算法代码要一字不改的就能移植进基于Rose的app。对平台辅助代码,具体是highgui、imgcodec、videoio,内置时把这三个模块去掉了,改为用Rose实现。

highgui。highgui包括imshow、Trackbar控件、鼠标事件。对imshow,替换方法是用上面说的surface(mat)转为surf,后续代码就可自由渲染这surf了。Rose中的Slider控件可替换Trackbar。至于鼠标,Rose提供了完整GUI,当中有个是track控件,这控件默认提供了left_button_down、motion、leave框架。

imgcodec。imgcodec包括imread、imwrite。imread可以读多种图像格式,像png、jpeg、tiff等等,在这方面,Rose支持的图像就一种,png。能支持多种自然要比只支持png好,但这个好到底有多少意义?1)推向市场的产品不是实验室产品,为稳定等考虑往往会限于一种图像格式,而png无疑是最流行的。2)只限于一种png,可极大减少编译OpenCV需要的依赖库。Rose对imread提供的替代方法是image::get_image("misc/ok.png"),它会返回一个surface,然后用tsurface_2_mat_lock转到mat。不论什么平台,app会把图像文件放在两类地方,一是资源包,二是用户数据,为此get_image对文件路径做了规范化处理,简化路径参数。Rose替换imwrite方法是先把mat用surface(mat)生成surf,然后用save_surface_to_file存成png文件。

videoio。videoio包括实时采集视频和播放本地视频文件。对采集视频,Rose内置了Webrtc,自然要用Webrtc去处理本地、远程视频了。Rose处理视频分两个步骤,一是启动/停止采集,二是采集到帧后怎么处理。对本地和远程视频,第二步是一样的,不同的只是第一步。启动本地采集是构造一个tavcapture对象,析构这对象就结束采集,结合这些,启动/停止本地采集方法就是定义个std::unique_ptr<tavcapture>类型的tavcapture对象。至于启动、停止远程视频,则是按着标准Webrtc信令服务器流程。

对第二步,Rose建议用一个track控件显示视频,如何显示就是自写挂向该track控件的did_draw方法。

paper_->set_did_draw(boost::bind(&topencv::did_draw_paper, this, _1, _2, _3));
paper_->set_timer_interval(30);

void topencv::did_draw_paper(ttrack& widget, const SDL_Rect& draw_rect, const bool bg_drawn)
{
	......
	tavcapture::VideoRenderer* local_renderer = avcapture_->vrenderer(false);
	texture& local_tex = avcapture_->get_texture(false);
	bool require_render_local = local_renderer && local_renderer->dirty();
	if (require_render_local) {
		threading::lock lock(avcapture_->get_mutex(false));
		ttexture_2_mat_lock lock2(local_tex);
		dst = app_did_draw_frame(false, lock2.mat, draw_rect);
		SDL_UnlockTexture(local_tex.get());
	}
	....
}

为便于同步,did_draw_paper肯定是在主线程执行。set_timer_interval(30)让主线程每隔30毫秒调用一次did_draw_paper。帧接收线程会把新收到的帧数据放在local_texture,并把local_renderer置脏。did_draw_paper决定要不要更新显示就判断local_renderer是不是脏了,是脏的话用threading::lock申请到帧锁(为避免和帧接收线程同时写帧数据),然后用ttexture_2_mat_lock把帧数据转成mat,OpenCV就可处理处理这mat。处理完后,结果写回mat。后面的SDL_UnlockTexture把帧数据从内存上传到纹理,同时用OpenGL等方法实现硬件渲染。ttexture_2_mat_lock一个类似tsurface_2_mat_lock的类,只不过像素数据一个是在surface,一个是texture,由于转换时需要像素数据在内存,能支持的texture必须是SDL_TEXTUREACCESS_STREAMING类型。

对播放本地视频,当前Rose还没实现替代方案。一来真正到实用时,移动端很少会有这种使用场景,二是毕竟个人精力有限,先处理更急迫的。由于内置了Webrtc,编/解码视频至少会支持h264、vpx,也就是说,播放这两种格式的本地视频,技术上不存在障碍,就是得花时间。

三、快速编程

Rose提供了个叫studio的app,app主页有个“OpenCV”入口,进入就可查看调用了OpenCV API的几个示例。通过看示例源码(https://github.com/freeors/Rose/blob/master/apps-src/apps/studio/gui/dialogs/opencv.cpp),知道surface、mat间如何转换,操作鼠标、Slider控件、get_image、操作本地视频,等等。首页还有个“聊天”入口,用它可就可实验OpenCV处理远程视频。

Studio执行“新建app”命令后,生成的app就有OpenCV算法测试窗口,只要把算法写入相关函数就能看到执行结果。一旦Windows通过调试,借用Rose跨平台特性,就可编译出iOS、Android,看那些平台的执行效率。

Rose在内置OpenCV时,Windows端已新建Visual Studio工程,iOS则基于XCode,开发者很方便就能通过断点进入OpenCV库代码。

全部评论: 0

    写评论: