- 摄像头是个独占资源,app要统一到一个对象进行管理。这个对象是base_instance中的camera_,类型tcamera。
- 为避免摄像头关掉很快又打开,用完摄像头后,任务是结束了,但camera_不会立即关闭摄像头,而是延长一段时间,这个时长是构造tcamera时的一个参数,默认30秒(expired_threshold)。如果在30秒内摄像头又进入任务,之前关闭倒计时会消失。
- 如何检测摄像头坏了。——任务结束后,开始计数之后收到的视频帧,如果2秒后,新收到帧数是0,认为摄像头坏了。此时不再关闭倒计时,立即调用stop_avcapturer停止摄像头。
- 任一时刻,camera_只会使用一个摄像头。如果存在多个摄像头,camera_要用哪个取决于preferences中的“usingcamera”值。如果不存在这个key,那先扫到哪个就用哪个。“usingcamera”值来自摄像头驱动上报的标识,像“LRCR USB2.0”。
launcher/kdesktop会使用摄像头的内置模块
- dnn。运行来源是摄像头的物体识别时。
- 相机。启动时就会开启摄像头
- 临时任务 物体识别
- 商店。用摄像头扫描二维码,安装小程序。
一、如何使用tcamera
以下所有调用都必须发生在主线程。连接指的是调用一个非nullptr的set_slot,断开指的是调用参数是nullptr的set_lost。
1.1 (可选)camera_.set_tflite(script, tflite)
如果需要开启tcamera内置的物体识别功能,在enter_task前调用set_tflite。
1.2 (可选)camera_.set_slot(slot)
向camera_连接自实现的slot,这个slot必须是持久的(temporary=false)。
slot用于自定义此次行为。它和1.4中set_slot中二选一。当然,如果使用tcamera默认行为就够了,那此次可以没有set_slot。
1.3 camera_.enter_task()
调用enter_task,进入任务(tasking_=true)。
1.4 (可选)camera_.set_slot(slot)
向camera_连接自实现的slot,这个slot必须是临时的(temporary=true)。
对何时连接临时slot,可以在enter_task和exit_task之间的任一时刻,像1.5、1.6后。
1.5 camera_.slice(draw_rect, immediately)
在时间片函数调用slice。在这函数,camera_会调用avcapture_->draw_slice(renderer, draw_rect),从而导致did_draw_slice被调用。
对参数immediately,如果能保证有固定间隔调用slice,设置为true。否则设置为false,免得间隔太短、高频率刷新时浪费cpu,详细解释见后面“tcamera::slice”。
既然是时间片,app须要不间断调用,否则从摄像头收到的图像不会交给app。
1.6 读取物体识别结果
如果enter_exit时使用了FLAG_USE_TFLITE标记,那可用下面代码读取识别结果。
threading::lock lock(camera_.get_variable_mutex()); const tcamera::tresult& result = camera_.get_result(); for (std::vector<tcamera::tresult::titem>::const_iterator it = result.items.begin(); it != result.items.end(); ++ it) { const tcamera::tresult::titem& item = *it; if (item.score >= 0.40) { // 0.7 this_object = item.name; break; } }
item示例
score = 0.984313。该图像中含有“name”物体的概率。最大值1.0。
index = 721
name = 药瓶(pill botlle)
1.7 (可选)camera_.set_slot(nullptr)
断开slot和camera_连接,这个slot必须是临时的。对临时slot,可以不调用set_slot(nullptr),后面这个exit_task时,发现设置了临时slot,会自动断开。
因为exit_task会自动断开,如果app会用set_slot(nullptr)断开临时slot,那建议用“get_slot()”判断下,是true再去主动调用set_slot。
1.8 camera_.exit_task()
调用exit_task,退出任务(tasking_=false)。
1.9 (可选)camera_.set_slot(nullptr)
断开slot和camera_连接,这个slot必须是持久的。
断开持久slot必须手动调用set_slot(nullptr),而且必须在exit_task后。
二、tcamera::tslot
tcamera提供了一个叫tslot的类,app通过重载tslot方法,定制自已的摄像头行为。
2.1 tslot api
void camera_post_enter_task();
执行enter_task期间,执行开始任务后,像可能要启动摄像头,会调用camera_post_enter_task()。对临时slot,也会调用,不过调用时机是在参数非nullptr的set_slot。
void camera_pre_exit_task();
执行exit_task期间,在设置tasking_为false前,会调用camera_pre_exit_task()。对临时slot,也会调用,不过时机是在set_slot(nullptr)。
void camera_did_draw_slice(int id, SDL_Renderer* renderer, trtc_client::VideoRenderer** locals, int locals_count, trtc_client::VideoRenderer** remotes, int remotes_count, const SDL_Rect& draw_rect);
收到一帧了,会调用camera_did_draw_slice。在这里,app要做的一般是两件事,一是把图像渲染到界面,二是处理camera_work_frame的结果。对帧的耗时操作,像判断二维码等,放到在BaseCameraThread线程执行的camera_work_frame。放到那里,可让播放界面保持流畅。
void camera_work_frame(const surface& surf, bool& lock_surf);
(BaseCameraThread线程)app在这里处理帧的耗时操作,像识别物体,识别二维码。
参数lock_surf指示是否要锁住surf。一开始总是true,表示不允许外面修改surf。camera_work_frame(...)已确定不会再使用surf,那就可以把lock_surf设置为false。这样tcamera就可以把下一帧放入DoWork的待处理帧缓存。camera_work_frame(...)可以一直不把lock_surf置为false,这意味着一定要等到copy_image退出了,tcamera才能把下一帧交给DoWork。
void camera_post_switch_camera();
存在多个摄像头时,切换摄像头后,会调用camera_post_switch_camera。
void camera_did_button_clicked(int msgid);
点击“取消”或“抓拍”按钮后,会调用camera_did_button_clicked,参数msgid指示是哪个按钮。对“切换相机”,按下它不会调用这个,切换操作由tcamera内置处理,app可重载camera_post_switch_camera,来增加切换之后执行什么操作。
任一时刻,tcamera最多只能存在一个slot。tslot有两种:持久slot和临时slot。
2.2 持久slot
对持久slot,是在没enter_task时就连接,在exit_task才断开,经历了enter_task到exit_task的整个过程,称为持久。
2.3 临时slot
临时slot,连接和断开都是在处在tasking_=true时,即在enter_task和exit_task之间。为什么要存在这么种slot?设想一下命令机器人进入识别物体,只是临时用下,就不开界面,不显示摄像头图像了。识别几秒钟,发现识别结果不对,就想显示摄像头图像,确认下原因。为做到显示,只须要连接个slot到base_camera,而这里是禁止切换任务,这时就可用临时slot。
临时slot不会经历enter_task,但它在连接时,会调用camera_post_enter_task()。断开时,则会调camera_pre_exit_task。如果app一直不调用set_slot(nullptr)主动断开,exist_task会自动去断开,此时也会调用camera_pre_exit_task。
临时slot存在个谁来调base_camera.slice问题。原则是在没有连接前,由使用enter_task的调用slice,像ttmp_task。一旦连接了,就由tslot来调用slice。
三、内置按钮
按钮 | 显示条件 | 位置 | 描述 |
取消 | FLAG_SHOW_CANCEL | 左上角 | 点击后会调用camera_did_button_clicked(POST_MSG_CANCEL_CAMERA) |
切换 | 总是显示 | 右下角 | tcamera内部处理。切换结束会调用camera_post_switch_camera |
每次slice,app调用render_overlay_texs显示这些按钮。
void render_overlay_texs(SDL_Renderer* renderer, const SDL_Rect& video_dst);
参数video_dst视区矩形。tcamera会以这个矩形参考,计算各按钮位置。
tcamera也会调用render_overlay_texs。start_avcapture成功、但摄像头有问题或格式不对,使得tcamera一直没收到帧,就会让显示这些按钮。有了这些按钮,由用户决定是切换摄像头还是取消。
总之,对显示内置按钮,如果摄像头一直没收到帧,包括没有摄像头,那由tcamera内部显示。否则由app的camera_did_draw_slice负责显示,调用都是render_overlay_texs。
3.1 render_overlay_texs_ticks_和tcamera::slice
enter_task后,按钮不是立即显示,而是要过了render_overlay_texs_ticks_后再显示,目前是2秒。
void tcamera::slice(const SDL_Rect& draw_rect, bool immediately) { SDL_Renderer* renderer = get_renderer(); bool no_frame = true; if (avcapture_.get() != nullptr && avcapture_->using_vidcap_cout() > 0) {
还没调用过start_avcapture,或摄像头没收到帧。
只要调用过start_avcapture,即使查找不到摄像头,avcapture_还是会非nullptr,此时using_vidcap_count返回0
trtc_client::VideoRenderer* sink = avcapture_->vrenderer(false, 0); if (sink->frame_thread_frames != 0) { uint32_t now = SDL_GetTicks(); if (immediately) { // some scene, it has gui-timer, for example ttrack. avcapture_->draw_slice(renderer, draw_rect); } else if (now >= next_draw_ticks_) { // use tbacamera's timer. avcapture_->draw_slice(renderer, draw_rect); next_draw_ticks_ += draw_interval_; } no_frame = false; } } if (no_frame) { if (!SDL_RectEmpty(&draw_rect)) { render_overlay_texs(renderer, draw_rect); } } }
刚进入did_draw_paper,摄像头没收到帧,frame_thread_frames是0,即no_frame=true,tbase_cameera计算出的按钮叠加位置以整个track控件(draw_rect)作为基础。当有图像,no_frame=false时,是以图像区域(video_dst)作为基础。draw_rect和video_dst这个矩形位置往往不一样,于是界面上看去,会有按钮移动。免得让用户看到这个移动,就让初始的2秒内不显示按钮。
只要调用过start_avcapture,即使查找不到摄像头,avcapture_还是会非nullptr,此时using_vidcap_count返回0。
解释下参数immediately。当连接了slot,要把视频画到界面时,会用ttrack控件,并会设置定时器,像30毫秒。这时是在定时器内调用camera_.slice,app已保证这个slice按间隔调用,那可以给immediately设置为true,表示在此次slice就立即调用draw_slice进行刷新。
如果没有连接slot,这里app可能在某个时间点调用camera_.slice,这个间隔时长可能就没规律,像可能很快,30毫秒内就可能多次。如果每次slice都刷新,那就会空费cpu。可以把immediately设置为false,看到false后,camera_可能不会这此次slice就刷新,而是按着内置的一个30毫秒定时器去刷新。
五、深度相机
tcamera同时可用于控制深度相机。要使用是封装了深度相机的tcamera,app在调用enter_task前,须额外调用一个叫set_depth_slot的api
void tcamera::set_depth_slot(tdepth_slot* depth_slot);
作是把参数depth_slot赋值给tcamera成员变量depth_slot_。depth_slot_会在两个地方产生影响,这也是它仅有的两个成员函数。
- tcamera::start_avcapture()期间,一旦发现depth_slot_!=nullptr,会调用depth_slot_->camera_create_avcapture,得到trtc_client* avcapture。否则调用的是普通相机的new tavcapture。
- tcamera::app_create_video_renderer期间,一旦发现depth_slot_!=nullptr,会调用depth_slot_->camera_create_video_renderer,得到trtc_client::VideoRenderer*。否则调用的普通相机的new trtc_client::VideoRenderer。
launcher是何时调用set_depth_slot?——须要安装深度相机驱动,就会调用camera_.set_depth_slot(this)。这确保了app在调用enter_task前,tcamera已有一个非nullptr的depth_slot_。
疑问:硬件本身是一个普通相机,却以着set_depth_slot去用,会产生什么后果?——在depth_slot_->camera_create_avcapture,会调用dcamera_driver的main_start方法,由于无法识别普通相机,main_start将返回false。导致就一直不会收到图像。但不会导致非法。
六、物体识别
用以下方法便可使用物体识别。
- 在enter_secne前,调用set_tflite,传下tscript、tflite。
- enter_secne时,至少包含标记FLAG_USE_TFLITE。
- 在主线程时间片,读取识别结果。
上面“读取物体识别结果”代码片断示范了如何读取结果。
读取前,必须先申请掉camera_.variable_mutex_锁,这里要读result_,相应地,DoWorker线程会对它写。
camera_.result_存储着最近识别出的结果。因为新到来一帧就会判断,产生一个新的camera_.result_,主线程必须担心camera_.result_一直不变。
七、did_draw_slice、camera_did_draw_slice
如果有收到过帧,每次slice()在调用avcapture_->draw_slice时,便会调用did_draw_slice。发现连接着slot,就会调用slot_->camera_did_draw_slice。这两个函数的参数一模一样。
void did_draw_slice(int id, SDL_Renderer* renderer, trtc_client::VideoRenderer** locals, int locals_count, trtc_client::VideoRenderer** remotes, int remotes_count, const SDL_Rect& draw_rect)
- locals。接收本地摄像头帧的VideoRenderer数组。对tcamera,这个指针数组长度固定是1。
- locals_count。同时在接收视频帧的本地摄像头个数。对tcamera,这个值固定是1。
- remotes。接收远程摄像头帧的VideoRenderer数组。远程摄像头指像用rtsp协议接收的客户端,trtc_client支持这种摄像头。对tcamera,它不支持远程,这个指针数组值固定是nullptr。
- remotes_count。同时在接收视频帧的远程摄像头个数。对tcamera,这个值固定是0。
- draw_rect。渲染区域。app调用draw_slice时传下的draw_rect参数。
在设计did_draw_slice时,希望达到三个目标。
- 以一个api同时处理本地、远程帧。
- 能够用OpenCV高效处理帧。
- 能够用硬件渲染高效渲染到后台纹理。
在did_draw_slice,app要做的一般是两件事,一是把图像渲染到界面,二是处理camera_work_frame的结果。渲染出的效果包含三个方面,一是本地帧,二是远程帧,来自其它素材的图像。
local_tex_、remote_tex_。收到的本地、远程帧数据后存放到这个纹理。纹理类型是SDL_TEXTUREACCESS_STREAMING。收到的数据被放在纹理关联的内存块。什么时候内存和gpu同步?rtc_client::draw_slice在调用app注册的处理函数前进行同步,即对app来说,这两个纹理中的内存、gpu总是同步的。
local_pixels_、remote_pixels_。local_tex_、remote_rex_关联的内存块的指针。该指针只在rtc_client内部使用,OnFrame用它存储新近收到的帧数据。
local_cv_tex_、remote_cv_tex_。cv是opencv缩写,rtc_client创建的、提供给app进行opencv操作的缓存,rtc_client只负责创建、销毁,不关心内中是什么数据。纹理类型是SDL_TEXTUREACCESS_STREAMING。为什么是这类型?opencv进行的是内存操作,内存操作的结果是放在该纹理关联的内存块。
local_app_tex_、remote_app_tex_。提供给app自定义功能的纹理。rtc_client只是提供个变量,析构时销毁,为什么要增加它?app可能经常有这么需要,要在图像上显示个纹理,这纹理面积可能很小,要是用cv_tex_就即浪费又耗cpu了。举个例子,每个摄像头有名称,要实时在左上角显示这名称,于是可以用名称创建app_tex_。cv_tex_、app_tex的功能都是给app自定义。它们主要区别:cv_tex_是rtc_client创建,尺寸固定是视频帧尺寸,app_tex_则由app自个创建,尺寸自定义。如果对原视频改动非常大,像要进行整图像变化,建议用cv_tex_,只是显示原视频加简单一处叠加,建议用tex_+app_tex_。
八、多线程
tcamera会涉及到两个线程,主线程和BaseCameraThread线程。start_avcapture时会创建、并运行BaseCameraThread线程,stop_avcapture时销毁。不在任务中时(tasking_=false),BaseCameraThread线程低消耗cpu。BaseCameraThread的线程函数是DoWorker,有时也把它称为DoWorker线程。
tcamera提供了一个叫variable_mutex_的互斥锁,app可用它在两个线程间同步读、写变量。至于另一个锁setting_mutex_,只限于tcamera内部使用,app不要用它。
未解疑问:在enter_task、exit_task,要申请setting_mutex_锁,一个threading::lock就够了,为什么还要tsetting_lock?
tsetting_lock setting_lock(*this); threading::lock lock(setting_mutex_);
主线程enter_task/exit_task已经在申请setting_mutex_,接下DoWorker用同样threading::lock申请setting_mutex_,反而是DoWorker先申请到!有时还是不一次,——这是我没有理解地方。这导致在enter_task/exit_task,光申请setting_mutex_就可能光费较长时间,像3秒,还出现过10多秒。所以在主线程在enter_task/exit_task前,先把settings_置为true,DoWorker发现settings_是true,就不去申请setting_mutex_,使得enter_task/exit_task能快速申请到setting_mutex_。