相机(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”。
  • 一个tcamera::tslot就应一个要使用相机的对象,称tcamera::tslot为相机执行体。
  • tcamera::tviewer称为查看器,用于封装tcamera中和显示视频相关api。使用时,把它放进执行体tslot,做到执行体只是代理,真正执行操作的是查看器派生的对象,像gui2::tdcamera、gui2::tcenter。

launcher/kdesktop会使用摄像头的执行体。

  1. DNN(taskid_dnn)。运行来源是摄像头的物体识别时。
  2. 后台任务(taskid_bgtask)。相机类型的小程序单任务。
  3. 底盘子任务(taskid_base_subtask)。目前底盘子任务只是能是相机类型的小程序单任务。
  4. 扫描二维码(taskid_rcamera)。打开相机,扫描二维码。
  5. 深度相机窗口(taskid_dcamera)。在深度相机窗口,是nposm情部时,会用这类型打开相机。
  6. 小程序(taskid_aplt)。目前没使用,计划是分配给在小程序中打开相机。

即使设备安装了多个相机,tcamera任一时刻只能使用一个相机,这意味着任一时刻最多在运行一个执行体。正是这原因,可以解释正在运行使用相机后台单任务时,是否可以操作底盘场景,包括挂起、恢复运行、切换下一场景?——不行。相机单任务和场景操作都要使用相机,在场景操作调用tcamera::set_slot,会抛出非法。

举个例子,正在运行后台任务抓拍,现在要执行恢复运行当前场景。在恢复场景时,要以底盘场景执行体为slot参数调用tcamera::set_slot。在set_slot,要求slot_是nullptr,此时slot_却指向后台任务(抓拍)执行体,导致抛出非法。

 

一、如何使用tcamera

以下所有调用都必须发生在主线程。连接指的是调用一个非nullptr的set_slot,断开指的是调用参数是nullptr的set_slot。

 

1.1 (可选)camera_.set_tflite(script, tflite)

如果需要开启tcamera内置的物体识别功能,在enter_task前调用set_tflite。

 

1.2 camera_.set_slot(slot)

向camera_连接自实现的slot。

slot用于自定义此次行为。即使此次用tcamera默认行为就够了,也必须调用set_slot。

 

1.3 camera_.enter_task(taskid, flags)

调用enter_task,进入任务(tasking_=true)。

对tcamera,taskid只要不是nposm,不关心具体是何值。后面必须以这个taskid值为参数调用exit_task。

 

1.4 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.5 (可选)读取物体识别结果

如果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_.exit_task(taskid)

调用exit_task,退出任务(tasking_=false)。

 

1.8 camera_.set_slot(nullptr)

断开slot和camera_连接。

 

二、tcamera::tslot

tcamera提供了一个叫tslot的类,app通过重载tslot方法,定制自已的摄像头行为。一个tslot就应一个要使用相机的对象,称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_。

最终到renderer的纹理可由三部分组成,一是tex_,它是没改动数据的帧纹理,二是cv_tex_,尺寸和帧纹理一样,进入did_draw_slice时,内容未知,由使用者自个负责生成,三是和帧内容无关的素材,像图像、文字。

tex_。Rose会保证app收到该参数时,当中已是最新帧。frame是frame_tex关联的cv::Mat对象。

cv_tex_:Rose只负责创建、销毁,不会读写当中内容。app要用它,一般分四步,第一步用ttexture_2_mat_lock转出个cv:Mat对象lock.mat。第二步按自个需要修改lock.mat中内容,通常和新图像有关,因而是使用opencv函数操作frame,结果放到lock.mat。第三步调用SDL_UnlockTexture(cv_tex.get())把修改后的内容提交到cv_tex。第四步把cv_tex用SDL_RenderCopy块移到renderer。

 

八、多线程

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

 

九、fix_VideoCapturer_Stop

在~VideoRenderer()

trtc_client::VideoRenderer::~VideoRenderer()
{
  if (rendered_track_.get() != nullptr) {
    rendered_track_->RemoveSink(this);
    rendered_track_ = nullptr; 
  }
  encoder_.reset();
}

rendered_track_类型是rtc::scoped_refptr<webrtc::VideoTrackInterface>,析构过程中会发生复杂逻辑。当中有个是发生在OnFrame线程、worker_thread_线程之间的争抢。

(worker_thread_线程)执行WebRtcVideoCapturer::Stop,
1. 申请VideoCaptureImpl::_apiCs,通过。
2. 申请CaptureSinkFilter::m_crtRecv,但由于被OnFrame线程占着,死锁。

(OnFrame线程)执行CaptureSinkFilter::ProcessCapturedFrame
1. 申请CaptureSinkFilter::m_crtRecv,通过。
2. 在VideoCaptureImpl::IncomingFrame,申请VideoCaptureImpl::_apiCs,但此锁被WebRtcVideoCapturer::Stop占着,死锁。

这两个函数的执行次序是随机的,为避免死锁,解决办法是在VideoCaptureImpl::IncomingFrame,发现正处在~VideoRenderer期间,即“fix_VideoCapturer_Stop==true”,就不做任何事,也就不会申请VideoCaptureImpl::_apiCs。

 

十、深度相机窗口

深度相机窗口要处理三种情况。

  • case_base_subtask。查看执行底盘任务时,摄像头实时视频。
  • case_moveit。查看操作机械臂时,摄像头实时视频。
  • nposm。既没后台任务运行,也没底盘任务,只是开下相机,查看摄像头实时视频。

如果正有后台任务在运行,那进入深度相机前,会要求先结束后台任务。

 

10.1 case_base_subtask

在case_base_subtask,一开始使用的是底盘场景执行体,一旦要运行后台任务,会改用后台任务执行体。后台任务结束,切换回底盘场景。这就造成深度相机窗口要面对这么个情况:这个窗口,有时会使用底盘场景执行体,有时使用后台任务执行体,而显示视频都用着深度相机窗口界面的逻辑。

要如何实现?——可确定底盘场景、后台任务要做为相机执行体。如果把深度相机窗口(tdcamera)也实现为相机执行体,那意味着得允许同时使用两个执行体,一个是深度像机,另一个或底盘场景,或后台任务。这违背规则:不能存时同时存在两个相机执行体。以下是正采用方法。

底盘场景执行体(tbase_driver)、后台任务执行体(tbg_task2)派生于tcamera::tslot。

深度相机窗口(tdcamera)派生于查看器tcamera::tviewer。并设置为那两执行体的查看器。

对底盘场景执行体、后台任务执行体,调用显示api时,立即发向查看器对应api。也主是说,两个执行体api只是充当着代理功能,真正发生作用的是深度相机窗口中函数。

camera_viewer_类型是tcamera::tviewer
------
void tcamera::tslot::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)
{
  if (camera_viewer_ != nullptr) {
    camera_viewer_->camera_did_draw_slice_c(id, renderer, locals, locals_count, remotes, remotes_count, draw_rect);
  }
}

简单说,把深度相机的显示逻辑封装成tcamera::tviewer,并同时作为底盘场景和后台任务的内在显示逻辑。这就实现子这么个功能:底盘场景运行了,那它会自动使用深度相机窗口显示逻辑;同样,换后台任务运行了,也会自动使用这逻辑。

tdcamera中有数个api有显示逻辑相关,重载它们便实现了一套自个显示逻辑。tcamera::tviewer是将和显示逻辑相关单独集合在一块,然后作为一个对象设置到相机执行体,成为后者camera_viewer_变量。这样深度相机窗口用这个camera_viewer_,实现快速切换。除深度相机窗口tdcamera,中心窗口(tcenter)也在用tcamera::tviewer。

为简单,tdcamera是在构造函数就把底盘任务和后台任务执行体的查看器设为this。等析构时,设回nullptr。不等到'tcamera::tasking()==true'才设置为this,是因为要做到这个逻辑太复杂。深度相机窗口期间,可能发生底盘任务挂起、恢复运行、切换到下一场景,又有后台任务运行,结束。

全部评论: 0

    写评论: