相机(tcamera)

  • 摄像头是个独占资源,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会使用摄像头的内置模块

  1. dnn。运行来源是摄像头的物体识别时。
  2. 相机。启动时就会开启摄像头
  3. 临时任务   物体识别
  4. 商店。用摄像头扫描二维码,安装小程序。

 

一、如何使用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_会在两个地方产生影响,这也是它仅有的两个成员函数。

  1. tcamera::start_avcapture()期间,一旦发现depth_slot_!=nullptr,会调用depth_slot_->camera_create_avcapture,得到trtc_client* avcapture。否则调用的是普通相机的new tavcapture。
  2. 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。导致就一直不会收到图像。但不会导致非法。

 

六、物体识别

用以下方法便可使用物体识别。

  1. 在enter_secne前,调用set_tflite,传下tscript、tflite。
  2. enter_secne时,至少包含标记FLAG_USE_TFLITE。
  3. 在主线程时间片,读取识别结果。

上面“读取物体识别结果”代码片断示范了如何读取结果。

读取前,必须先申请掉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时,希望达到三个目标。

  1. 以一个api同时处理本地、远程帧。
  2. 能够用OpenCV高效处理帧。
  3. 能够用硬件渲染高效渲染到后台纹理。

在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_。

全部评论: 0

    写评论: