文档管理中心

自定义相机如何隔一段时间获取并保存预览帧的图像

问题现象

如何使用自定义相机打开前置摄像头,隔一段时间截取视频帧,编码成图片文件后保存到沙箱,以供后续发送图像数据进行识别?

背景知识

  • ImageReceiver为图像接收类,用于获取组件surface id,接收最新的图片和读取下一张图片,以及释放ImageReceiver实例。
  • packToData可用于图片压缩或重新编码,可以使用此接口将PixelMap类型的数据编码成图片文件保存至应用沙箱。

解决方案

  1. 首先使用ImageReceiver接收预览帧图片,具体方法可参考双路预览。
  2. 将获取到的预览帧图片编码成图片保存至应用沙箱,参考代码如下:
    收起
    自动换行
    深色代码主题
    复制
    1. /**
    2. * 将PixelMap对象的图片数据保存至应用沙箱路径
    3. * @param pixelMap 需要保存的pixelMap对象
    4. * @returns
    5. */
    6. async packToData(pixelMap: PixelMap) {
    7. try {
    8. let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    9. let cacheDir: string = context.cacheDir;
    10. let now = Date.now();
    11. let fileUrl = cacheDir + `/${now}_packToData.jpg`;
    12. if (fs.accessSync(fileUrl)) {
    13. fs.unlinkSync(fileUrl);
    14. }
    15. let packOpts: image.PackingOption = { format: 'image/jpeg', quality: 98 };
    16. const imagePackerApi: image.ImagePacker = image.createImagePacker();
    17. imagePackerApi.packToData(pixelMap, packOpts)
    18. .then((data: ArrayBuffer) => {
    19. let file = fs.openSync(fileUrl, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    20. fs.write(file.fd, data).then((writeLen: number) => {
    21. hilog.info(DOMAIN, TAG, `packToData succeed and size is: ${writeLen}, path is ${fileUrl}}`);
    22. }).catch((err: BusinessError) => {
    23. hilog.error(DOMAIN, TAG, `packToData failed with error message: ${err.message}, error code: ${err.code}`);
    24. }).finally(() => {
    25. fs.closeSync(file);
    26. });
    27. }).catch((error: BusinessError) => {
    28. hilog.error(DOMAIN, TAG, `packToData error,code ${error.code}, message is ${error.message}`);
    29. });
    30. } catch (err) {
    31. let e: BusinessError = err;
    32. hilog.error(DOMAIN, TAG,
    33. `write file by pixelmap error is: ${JSON.stringify(e.message)} and stack: ${JSON.stringify(e.stack)}`);
    34. }
    35. }
  3. 使用setInterval定时器,设置时间定时执行步骤2:
    收起
    自动换行
    深色代码主题
    复制
    1. onPageShow(): void {
    2. hilog.info(DOMAIN, TAG, 'onPageShow');
    3. this.initImageReceiver();
    4. if (this.xComponentSurfaceId !== '') {
    5. this.initCamera();
    6. }
    7. // 将提取到的视频帧编码进应用沙箱
    8. setInterval(() => {
    9. this.packToData(this.previewImage!);
    10. }, this.period * 1000);
    11. }

完整代码如下:

收起
自动换行
深色代码主题
复制
  1. import { camera } from '@kit.CameraKit';
  2. import { image } from '@kit.ImageKit';
  3. import { BusinessError } from '@kit.BasicServicesKit';
  4. import { fileIo as fs } from '@kit.CoreFileKit';
  5. import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
  6. import { hilog } from '@kit.PerformanceAnalysisKit';
  7. interface CameraResources {
  8. videoOutput?: camera.VideoOutput;
  9. cameraInput?: camera.CameraInput;
  10. previewOutput1?: camera.PreviewOutput;
  11. previewOutput2?: camera.PreviewOutput;
  12. session?: camera.VideoSession;
  13. }
  14. const DOMAIN: number = 0x0000;
  15. const TAG: string = '[CameraDemo]';
  16. @Entry
  17. @Component
  18. struct Index {
  19. @State isShow: boolean = false;
  20. private previewImage: image.PixelMap | undefined = undefined;
  21. private imageReceiver: image.ImageReceiver | undefined = undefined;
  22. private imageReceiverSurfaceId: string = '';
  23. private xComponentCtl: XComponentController = new XComponentController();
  24. private xComponentSurfaceId: string = '';
  25. private imageWidth: number = 1920;
  26. private imageHeight: number = 1080;
  27. private cameraManager: camera.CameraManager | undefined = undefined;
  28. private cameras: Array<camera.CameraDevice> | undefined = [];
  29. private uiContext: UIContext = this.getUIContext();
  30. private context: Context | undefined = this.uiContext.getHostContext();
  31. private cameraPermission: Permissions = 'ohos.permission.CAMERA'; // 申请权限
  32. private period: number = 3; // 定义提取视频帧时间间隔
  33. private cameraResources: CameraResources = {};
  34. /**
  35. * 向用户申请权限
  36. * @returns
  37. */
  38. async requestPermissionsFn(): Promise<void> {
  39. let atManager = abilityAccessCtrl.createAtManager();
  40. if (this.context) {
  41. let res = await atManager.requestPermissionsFromUser(this.context, [this.cameraPermission]);
  42. if (!res || !res.permissions || res.permissions.length === 0) {
  43. hilog.error(DOMAIN, TAG, 'requestPermissionsFromUser interface call fails');
  44. return;
  45. }
  46. if (res.permissions.length !== res.authResults.length) {
  47. hilog.error(DOMAIN, TAG, 'Authentication result mismatch');
  48. return;
  49. }
  50. res.permissions.forEach((value: string, index: number, permissions: string[]) => {
  51. if (this.cameraPermission.toString() === value && res.authResults[index] === 0) {
  52. this.isShow = true;
  53. }
  54. permissions.forEach((permission: string) => {
  55. hilog.info(DOMAIN, TAG, `${permission} granted`);
  56. });
  57. });
  58. }
  59. }
  60. aboutToAppear(): void {
  61. this.requestPermissionsFn();
  62. }
  63. onPageShow(): void {
  64. hilog.info(DOMAIN, TAG, 'onPageShow');
  65. this.initImageReceiver();
  66. if (this.xComponentSurfaceId !== '') {
  67. this.initCamera();
  68. }
  69. // 将提取到的视频帧编码进应用沙箱
  70. setInterval(() => {
  71. this.packToData(this.previewImage!);
  72. }, this.period * 1000);
  73. }
  74. onPageHide(): void {
  75. hilog.info(DOMAIN, TAG, 'onPageHide');
  76. this.releaseCamera();
  77. }
  78. build() {
  79. Column() {
  80. if (this.isShow) {
  81. XComponent({
  82. id: 'componentId',
  83. type: XComponentType.SURFACE,
  84. controller: this.xComponentCtl
  85. })
  86. .onLoad(async () => {
  87. hilog.info(DOMAIN, TAG, 'onLoad is called');
  88. this.xComponentSurfaceId = this.xComponentCtl.getXComponentSurfaceId(); // 获取组件surfaceId
  89. // 初始化相机,组件实时渲染每帧预览流数据
  90. this.initCamera();
  91. })
  92. .width(this.uiContext.px2vp(this.imageHeight))
  93. .height(this.uiContext.px2vp(this.imageWidth))
  94. }
  95. }
  96. .justifyContent(FlexAlign.Center)
  97. .height('100%')
  98. .width('100%')
  99. }
  100. /**
  101. * 获取ImageReceiver的SurfaceId
  102. * @returns
  103. */
  104. async initImageReceiver(): Promise<void> {
  105. if (!this.imageReceiver) {
  106. // 创建ImageReceiver
  107. let size: image.Size = { width: this.imageWidth, height: this.imageHeight };
  108. try {
  109. this.imageReceiver = image.createImageReceiver(size, image.ImageFormat.JPEG, 8);
  110. } catch (error) {
  111. let err = error as BusinessError;
  112. hilog.error(DOMAIN, TAG, `Init image receiver failed. error code: ${err.code}`);
  113. return;
  114. }
  115. // 获取第一路流SurfaceId
  116. this.imageReceiverSurfaceId = await this.imageReceiver.getReceivingSurfaceId();
  117. hilog.info(DOMAIN, TAG, `initImageReceiver imageReceiverSurfaceId:${this.imageReceiverSurfaceId}`);
  118. // 注册监听处理预览流每帧图像数据
  119. this.onImageArrival(this.imageReceiver);
  120. }
  121. }
  122. // Image格式与PixelMap格式映射关系
  123. private formatToPixelMapFormatMap = new Map<number, image.PixelMapFormat>([
  124. [12, image.PixelMapFormat.RGBA_8888],
  125. [25, image.PixelMapFormat.NV21],
  126. [35, image.PixelMapFormat.YCBCR_P010],
  127. [36, image.PixelMapFormat.YCRCB_P010]
  128. ]);
  129. // PixelMapFormat格式的单个像素点大小映射关系
  130. private pixelMapFormatToSizeMap = new Map<image.PixelMapFormat, number>([
  131. [image.PixelMapFormat.RGBA_8888, 4],
  132. [image.PixelMapFormat.NV21, 1.5],
  133. [image.PixelMapFormat.YCBCR_P010, 3],
  134. [image.PixelMapFormat.YCRCB_P010, 3]
  135. ]);
  136. /**
  137. * 将PixelMap对象的图片数据保存至应用沙箱路径
  138. * @param pixelMap 需要保存的pixelMap对象
  139. * @returns
  140. */
  141. async packToData(pixelMap: PixelMap) {
  142. try {
  143. let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  144. let cacheDir: string = context.cacheDir;
  145. let now = Date.now();
  146. let fileUrl = cacheDir + `/${now}_packToData.jpg`;
  147. if (fs.accessSync(fileUrl)) {
  148. fs.unlinkSync(fileUrl);
  149. }
  150. let packOpts: image.PackingOption = { format: 'image/jpeg', quality: 98 };
  151. const imagePackerApi: image.ImagePacker = image.createImagePacker();
  152. imagePackerApi.packToData(pixelMap, packOpts)
  153. .then((data: ArrayBuffer) => {
  154. let file = fs.openSync(fileUrl, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  155. fs.write(file.fd, data).then((writeLen: number) => {
  156. hilog.info(DOMAIN, TAG, `packToData succeed and size is: ${writeLen}, path is ${fileUrl}}`);
  157. }).catch((err: BusinessError) => {
  158. hilog.error(DOMAIN, TAG, `packToData failed with error message: ${err.message}, error code: ${err.code}`);
  159. }).finally(() => {
  160. fs.closeSync(file);
  161. });
  162. }).catch((error: BusinessError) => {
  163. hilog.error(DOMAIN, TAG, `packToData error,code ${error.code}, message is ${error.message}`);
  164. });
  165. } catch (err) {
  166. let e: BusinessError = err;
  167. hilog.error(DOMAIN, TAG,
  168. `write file by pixelmap error is: ${JSON.stringify(e.message)} and stack: ${JSON.stringify(e.stack)}`);
  169. }
  170. }
  171. /**
  172. * 注册ImageReceiver图像监听
  173. * @param receiver
  174. * @returns
  175. */
  176. onImageArrival(receiver: image.ImageReceiver): void {
  177. // 注册imageArrival监听
  178. receiver.on('imageArrival', () => {
  179. hilog.info(DOMAIN, TAG, 'image arrival');
  180. // 获取图像
  181. receiver.readNextImage((err: BusinessError, nextImage: image.Image) => {
  182. if (err || nextImage === undefined) {
  183. hilog.error(DOMAIN, TAG, 'readNextImage failed');
  184. return;
  185. }
  186. // 解析图像内容
  187. nextImage.getComponent(image.ComponentType.JPEG, async (err: BusinessError, imgComponent: image.Component) => {
  188. if (err || imgComponent === undefined) {
  189. hilog.error(DOMAIN, TAG, 'getComponent failed');
  190. }
  191. if (imgComponent.byteBuffer) {
  192. let width = nextImage.size.width; // 获取图片的宽
  193. let height = nextImage.size.height; // 获取图片的高
  194. let stride = imgComponent.rowStride; // 获取图片的stride
  195. let imageFormat = nextImage.format; // 获取图片的format
  196. let pixelMapFormat = this.formatToPixelMapFormatMap.get(imageFormat) ?? image.PixelMapFormat.NV21;
  197. let mSize = this.pixelMapFormatToSizeMap.get(pixelMapFormat) ?? 1.5;
  198. hilog.info(DOMAIN, TAG, `getComponent with width:${width} height:${height} stride:${stride}`);
  199. // pixelMap创建时使用的size、srcPixelFormat需要与相机预览输出流previewProfile中的size、format保持一致,此处format以NV21格式为例
  200. // stride与width一致
  201. let pixelMap: image.PixelMap;
  202. if (stride === width) {
  203. pixelMap = await image.createPixelMap(imgComponent.byteBuffer, {
  204. size: { height: height, width: width },
  205. srcPixelFormat: pixelMapFormat,
  206. });
  207. this.previewImage = pixelMap;
  208. } else {
  209. // stride与width不一致
  210. const dstBufferSize = width * height * mSize; // 以NV21为例(YUV_420_SP格式的图片)YUV_420_SP内存计算公式:长x宽+(长x宽)/2
  211. const dstArr = new Uint8Array(dstBufferSize);
  212. for (let j = 0; j < height * mSize; j++) {
  213. const srcBuf = new Uint8Array(imgComponent.byteBuffer, j * stride, width);
  214. dstArr.set(srcBuf, j * width);
  215. }
  216. pixelMap = await image.createPixelMap(dstArr.buffer, {
  217. size: { height: height, width: width },
  218. srcPixelFormat: pixelMapFormat,
  219. });
  220. this.previewImage = pixelMap;
  221. }
  222. } else {
  223. hilog.error(DOMAIN, TAG, 'byteBuffer is null');
  224. }
  225. // 确保当前buffer没有在使用的情况下,可进行资源释放
  226. // 如果对buffer进行异步操作,需要在异步操作结束后再释放该资源(nextImage.release())
  227. nextImage.release();
  228. hilog.info(DOMAIN, TAG, 'image process done');
  229. });
  230. });
  231. });
  232. }
  233. /**
  234. * 初始化相机流程
  235. * @returns
  236. */
  237. async initCamera(): Promise<void> {
  238. hilog.info(DOMAIN, TAG,
  239. `initCamera imageReceiverSurfaceId:${this.imageReceiverSurfaceId} xComponentSurfaceId:${this.xComponentSurfaceId}`);
  240. try {
  241. // 获取相机管理器实例
  242. this.cameraManager = camera.getCameraManager(this.context);
  243. if (!this.cameraManager) {
  244. hilog.error(DOMAIN, TAG, 'getCameraManager call failed');
  245. return;
  246. }
  247. // 获取当前设备支持的相机device列表
  248. this.cameras = this.cameraManager.getSupportedCameras();
  249. if (!this.cameras || this.cameras.length === 0) {
  250. hilog.error(DOMAIN, TAG, 'getSupportedCameras call failed');
  251. return;
  252. }
  253. let cameraDevice: camera.CameraDevice | undefined = undefined;
  254. this.cameras.forEach((device: camera.CameraDevice) => {
  255. if (device.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT) {
  256. cameraDevice = device;
  257. return;
  258. }
  259. });
  260. // 未找到前置摄像头
  261. if (cameraDevice === undefined) {
  262. hilog.error(DOMAIN, TAG, 'Front camera device not found!');
  263. return;
  264. }
  265. // 选择一个相机device,创建cameraInput输出对象
  266. this.cameraResources.cameraInput = this.cameraManager.createCameraInput(cameraDevice);
  267. if (!this.cameraResources.cameraInput) {
  268. hilog.error(DOMAIN, TAG, 'createCameraInput call failed');
  269. return;
  270. }
  271. // 打开相机
  272. await this.cameraResources.cameraInput.open();
  273. // 获取相机device支持的profile
  274. let capability: camera.CameraOutputCapability =
  275. this.cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_VIDEO);
  276. if (!capability) {
  277. hilog.error(DOMAIN, TAG, 'getSupportedOutputCapability call failed');
  278. this.releaseCamera();
  279. return;
  280. }
  281. let minRatioDiff: number = 0.1;
  282. let surfaceRatio: number = this.imageWidth / this.imageHeight; // 最接近16:9宽高比
  283. let previewProfile: camera.Profile = capability.previewProfiles[0];
  284. // 应用开发者根据实际业务需求选择一个支持的预览流previewProfile
  285. // 此处以选择CAMERA_FORMAT_YUV_420_SP(NV21)格式、满足限定条件分辨率的预览流previewProfile为例
  286. for (let index = 0; index < capability.previewProfiles.length; index++) {
  287. const tempProfile = capability.previewProfiles[index];
  288. let tempRatio = tempProfile.size.width >= tempProfile.size.height ?
  289. tempProfile.size.width / tempProfile.size.height : tempProfile.size.height / tempProfile.size.width;
  290. let currentRatio = Math.abs(tempRatio - surfaceRatio);
  291. if (currentRatio <= minRatioDiff && tempProfile.format === camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP) {
  292. previewProfile = tempProfile;
  293. break;
  294. }
  295. }
  296. this.imageWidth = previewProfile.size.width; // 更新xComponent组件的宽
  297. this.imageHeight = previewProfile.size.height; // 更新xComponent组件的高
  298. hilog.info(DOMAIN, TAG, `initCamera imageWidth: ${this.imageWidth} imageHeight: ${this.imageHeight}`);
  299. // 使用imageReceiverSurfaceId创建第一路预览
  300. this.cameraResources.previewOutput1 =
  301. this.cameraManager.createPreviewOutput(previewProfile, this.imageReceiverSurfaceId);
  302. if (!this.cameraResources.previewOutput1) {
  303. hilog.error(DOMAIN, TAG, 'initCamera createPreviewOutput1');
  304. this.releaseCamera();
  305. return;
  306. }
  307. // 使用xComponentSurfaceId创建第二路预览
  308. this.cameraResources.previewOutput2 =
  309. this.cameraManager.createPreviewOutput(previewProfile, this.xComponentSurfaceId);
  310. if (!this.cameraResources.previewOutput2) {
  311. hilog.error(DOMAIN, TAG, 'initCamera createPreviewOutput2');
  312. this.releaseCamera();
  313. return;
  314. }
  315. // 创建录像模式相机会话
  316. let session = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO);
  317. if (!session) {
  318. hilog.error(DOMAIN, TAG, 'session is null');
  319. this.releaseCamera();
  320. return;
  321. }
  322. this.cameraResources.session = session as camera.VideoSession;
  323. // 开始配置会话
  324. this.cameraResources.session.beginConfig();
  325. // 添加相机设备输入
  326. this.cameraResources.session.addInput(this.cameraResources.cameraInput);
  327. // 添加第一路预览流输出
  328. this.cameraResources.session.addOutput(this.cameraResources.previewOutput1);
  329. // 添加第二路预览流输出
  330. this.cameraResources.session.addOutput(this.cameraResources.previewOutput2);
  331. // 提交会话配置
  332. await this.cameraResources.session.commitConfig();
  333. // 开始启动已配置的输入输出流
  334. await this.cameraResources.session.start();
  335. } catch (error) {
  336. hilog.error(DOMAIN, TAG, `initCamera fail: ${JSON.stringify(error)}`);
  337. this.releaseCamera();
  338. }
  339. }
  340. /**
  341. * 释放相机相关资源
  342. * @returns
  343. */
  344. async releaseCamera(): Promise<void> {
  345. hilog.info(DOMAIN, TAG, 'releaseCamera E');
  346. try {
  347. // 停止当前会话
  348. await this.cameraResources.session?.stop();
  349. } catch (error) {
  350. hilog.error(DOMAIN, TAG, `session.stop call failed, error: ${JSON.stringify(error)}`);
  351. }
  352. try {
  353. // 释放相机输入流
  354. await this.cameraResources.cameraInput?.close();
  355. } catch (error) {
  356. hilog.error(DOMAIN, TAG, `camera close fail: ${JSON.stringify(error)}`);
  357. }
  358. try {
  359. // 释放预览输出流
  360. await this.cameraResources.previewOutput1?.release();
  361. } catch (error) {
  362. hilog.error(DOMAIN, TAG, `previewOutput1 release fail: ${JSON.stringify(error)}`);
  363. }
  364. try {
  365. // 释放拍照输出流
  366. await this.cameraResources.previewOutput2?.release();
  367. } catch (error) {
  368. hilog.error(DOMAIN, TAG, `previewOutput2 release fail: ${JSON.stringify(error)}`);
  369. }
  370. try {
  371. // 释放会话
  372. await this.cameraResources.session?.release();
  373. } catch (error) {
  374. hilog.error(DOMAIN, TAG, `session release fail: ${JSON.stringify(error)}`);
  375. }
  376. }
  377. }

注意使用相机功能前,需要在module.json5文件中申请相机权限:

收起
自动换行
深色代码主题
复制
  1. {
  2. "name": "ohos.permission.CAMERA",
  3. "reason": "$string:reason_camera",
  4. "usedScene": {
  5. "abilities": [
  6. "EntryAbility"
  7. ]
  8. }
  9. },