diff --git a/README.en.md b/README.en.md index 94a46c51b65153e8421b6204a5dfd18e55475214..6c5bea60228df3ba57290438848820856f7aadbe 100644 --- a/README.en.md +++ b/README.en.md @@ -2,7 +2,7 @@ ### Overview -Based on the Camera Kit, this sample implements a range of core camera functionalities such as basic preview, preview image adjustments (switching between the front and rear cameras, flash light, focus, zoom, etc.), advanced preview functionalities (grid line, level, timeout pause, etc.), dual-channel preview, photographing (such as motion photo and delayed shooting), and video recording. It serves as a comprehensive reference and practice guidance for developing a custom camera service. +Based on the Camera Kit, this sample implements a range of core camera functionalities such as basic preview, preview image adjustments (switching between the front and rear cameras, flash light, focus, zoom, etc.), advanced preview functionalities (grid line, level, timeout pause, face detection, etc.), dual-channel preview, photographing (such as motion photo and delayed shooting), and video recording. It serves as a comprehensive reference and practice guidance for developing a custom camera service. ### Preview @@ -29,6 +29,7 @@ How to use: │ │ └──cameraManagers │ │ ├──CamaraManager.ets // Camera session management class. │ │ ├──ImageReceiverManager.ets // ImageReceiver preview stream. +│ │ ├──MetadataManager.ets // Metadata output stream. │ │ ├──OutputManager.ets // Output stream management abstract. │ │ ├──PhotoManager.ets // Photo stream management class. │ │ ├──VideoManager.ets // Video stream management class. diff --git a/README.md b/README.md index 0e423c84bf94c590e2f5503d53e92ecea3c6c366..227744da5ad00da182a8320d5c58a30472722d59 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ### 介绍 -本示例基于Camera Kit相机服务,使用ArkTS API实现基础预览、预览画面调整(前后置镜头切换、闪光灯、对焦、调焦、设置曝光中心点等)、预览进阶功能(网格线、水平仪、超时暂停等)、双路预览(获取预览帧数据)、拍照(动图拍摄、延迟拍摄等)、录像等核心功能。为开发者提供自定义相机开发的完整参考与实践指导。 +本示例基于Camera Kit相机服务,使用ArkTS API实现基础预览、预览画面调整(前后置镜头切换、闪光灯、对焦、调焦、设置曝光中心点等)、预览进阶功能(网格线、水平仪、人脸检测、超时暂停等)、双路预览(获取预览帧数据)、拍照(动图拍摄、延迟拍摄等)、录像等核心功能。为开发者提供自定义相机开发的完整参考与实践指导。 ### 效果预览 @@ -28,6 +28,7 @@ │ │ └──cameraManagers │ │ ├──CamaraManager.ets // 相机会话管理类 │ │ ├──ImageReceiverManager.ets // ImageReceiver预览流管理类 +│ │ ├──MetadataManager.ets // 元数据输出流管理类 │ │ ├──OutputManager.ets // 输出流管理类抽象接口 │ │ ├──PhotoManager.ets // 拍照流管理类 │ │ ├──VideoManager.ets // 视频流管理类 diff --git a/camera/Index.ets b/camera/Index.ets index ccfbb991d25157b984dd7911b8e1bcb8dffb967b..c8eba2c9439e42abdab6e3b659c54a9ee2ecd56e 100644 --- a/camera/Index.ets +++ b/camera/Index.ets @@ -18,5 +18,6 @@ export { PreviewManager } from './src/main/ets/cameramanagers/PreviewManager'; export { PhotoManager } from './src/main/ets/cameramanagers/PhotoManager'; export { VideoManager, AVRecorderState } from './src/main/ets/cameramanagers/VideoManager'; export { ImageReceiverManager } from './src/main/ets/cameramanagers/ImageReceiverManager'; +export { MetadataManager } from './src/main/ets/cameramanagers/MetadataManager'; export { GridLine } from './src/main/ets/components/GridLine'; export { LevelIndicator } from './src/main/ets/components/LevelIndicator'; diff --git a/camera/src/main/ets/cameramanagers/MetadataManager.ets b/camera/src/main/ets/cameramanagers/MetadataManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..934cd041cd0f46e077be32a1dea378a58dce28e0 --- /dev/null +++ b/camera/src/main/ets/cameramanagers/MetadataManager.ets @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 ("the License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import OutputManager, { CreateOutputConfig } from "./OutputManager"; +import { BusinessError } from '@kit.BasicServicesKit'; +import { camera } from "@kit.CameraKit"; +import { Logger } from "commons"; + +const TAG_LOG = 'MetadataManager'; + +export class MetadataManager implements OutputManager { + output?: camera.MetadataOutput; + isActive: boolean = true; + onMetadataObjectsAvailable: (faceBoxArr: camera.Rect[]) => void; + + constructor(onMetadataObjectsAvailable: (faceBoxArr: camera.Rect[]) => void) { + this.onMetadataObjectsAvailable = onMetadataObjectsAvailable; + } + + async createOutput(config: CreateOutputConfig) { + const cameraOutputCap = config.cameraManager?.getSupportedOutputCapability(config.device, config.sceneMode); + if (!cameraOutputCap) { + Logger.error(TAG_LOG, 'Failed to get supported output capability.'); + return; + } + let metadataObjectTypes: Array = cameraOutputCap!.supportedMetadataObjectTypes; + try { + this.output = config.cameraManager?.createMetadataOutput(metadataObjectTypes); + if (this.output) { + this.addOutputListener(this.output); + } + } catch (error) { + console.error(`Failed to createMetadataOutput, error code: ${error.code}`); + } + return this.output; + } + + addOutputListener(output: camera.MetadataOutput) { + this.addMetadataObjectsAvailableListener(output); + this.addMetadataErrorListener(output); + } + + addMetadataObjectsAvailableListener(metadataOutput: camera.MetadataOutput): void { + metadataOutput.on('metadataObjectsAvailable', + (err: BusinessError, metadataObjectArr: Array) => { + if (err !== undefined && err.code !== 0) { + return; + } + let boxRectArr: camera.Rect[] = []; + metadataObjectArr.forEach((obj: camera.MetadataObject)=>{ + boxRectArr.push(obj.boundingBox); + }); + this.onMetadataObjectsAvailable(boxRectArr); + }); + } + + addMetadataErrorListener(metadataOutput: camera.MetadataOutput): void { + metadataOutput.on('error', (metadataOutputError: BusinessError) => { + Logger.error(TAG_LOG, `Metadata output error code: ${metadataOutputError.code}`); + }); + } + + async release() { + try { + await this.output?.release(); + } catch (exception) { + Logger.error(TAG_LOG, `release failed, code is ${exception.code}, message is ${exception.message}`); + } + this.output = undefined; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 8b3f6d214d58f1e93b0c260227ed23c76224191b..4e130445739bbd2c8df132968091634b2babc41e 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -15,27 +15,35 @@ import { sensor } from '@kit.SensorServiceKit'; import { common } from '@kit.AbilityKit'; -import { display } from '@kit.ArkUI'; +import { display, window } from '@kit.ArkUI'; import { curves } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; +import { camera } from '@kit.CameraKit'; import { CameraManager, GridLine, ImageReceiverManager, LevelIndicator, + MetadataManager, PhotoManager, PreviewManager, VideoManager } from 'camera'; import CameraConstant from '../constants/Constants'; -import { calCameraPoint, getClampedChildPosition, limitNumberInRange, showToast } from '../utils/CommonUtil'; +import { + calCameraPoint, + calFaceBoxLinePoint, + getClampedChildPosition, + limitNumberInRange, + showToast +} from '../utils/CommonUtil'; import RefreshableTimer from '../utils/RefreshableTimer'; import PermissionManager from '../utils/PermissionManager'; import ZoomButtonsView from '../views/ZoomButtonsView'; import ModeButtonsView from '../views/ModeButtonsView'; import SettingButtonsView from '../views/SettingButtonsView'; import OperateButtonsView from '../views/OperateButtonsView'; -import PreviewViewModel from '../viewmodels/PreviewViewModel'; +import PreviewViewModel, { LinePoint } from '../viewmodels/PreviewViewModel'; import { Logger } from 'commons'; @Extend(SymbolGlyph) @@ -47,14 +55,14 @@ function funcButtonStyle() { .backgroundColor('#664D4D4D') } -const TAG = 'Index' +const TAG = 'Index'; @Entry @Component struct Index { private context: Context = this.getUIContext().getHostContext()!; private applicationContext = this.context.getApplicationContext(); - private windowClass = (this.context as common.UIAbilityContext).windowStage.getMainWindowSync(); + private windowClass: window.Window | undefined = undefined; @State videoManager: VideoManager = new VideoManager(this.context); @State isSinglePhoto: boolean = false; @State isLivePhoto: boolean = false; @@ -65,8 +73,11 @@ struct Index { private imageReceiverManager: ImageReceiverManager = new ImageReceiverManager(px => { this.onImageReceiver(px); }); + private metadataManager: MetadataManager = new MetadataManager(faceBoxArr => { + this.onMetadataObjectsAvailable(faceBoxArr); + }); private cameraManager: CameraManager = new CameraManager(this.context, [this.previewManager, - this.photoManager, this.videoManager, this.imageReceiverManager]); + this.photoManager, this.videoManager, this.imageReceiverManager, this.metadataManager]); @State previewVM: PreviewViewModel = new PreviewViewModel(this.getUIContext()); @State isGridLineVisible: boolean = false; @State isLevelIndicatorVisible: boolean = false; @@ -98,12 +109,19 @@ struct Index { @State isShowBlack: boolean = false; @StorageLink('captureClick') @Watch('onCaptureClick') captureClickFlag: number = 0; @State flashBlackOpacity: number = 1; + @State faceBoundingBoxArr: camera.Rect[] = []; async aboutToAppear() { this.addGravityEventListener(); this.initSleepTimer(); this.registerApplicationStateChange(); this.addOrientationChangeEventListener(); + try { + this.windowClass = (this.context as common.UIAbilityContext).windowStage.getMainWindowSync(); + } catch (exception) { + Logger.error(TAG, `getMainWindowSync failed, code is ${exception.code}, message is ${exception.message}`); + } + try { display.on('foldStatusChange', () => { this.onFoldStatusChange() @@ -131,11 +149,11 @@ struct Index { // [End addGravityEventListener] addOrientationChangeEventListener() { - this.windowClass.on('windowSizeChange', this.setPreviewSize); + this.windowClass?.on('windowSizeChange', this.setPreviewSize); } removeOrientationChangeEventListener() { - this.windowClass.off('windowSizeChange', this.setPreviewSize); + this.windowClass?.off('windowSizeChange', this.setPreviewSize); } onImageReceiver(pixelMap: PixelMap) { @@ -200,6 +218,17 @@ struct Index { this.previewVM.closePreviewBlur(); } + onMetadataObjectsAvailable(faceBoxArr: camera.Rect[]) { + faceBoxArr.forEach((value) => { + // Convert normalized coordinates to actual coordinates. + value.topLeftX *= this.previewVM.getPreviewWidth(); + value.topLeftY *= this.previewVM.getPreviewHeight(); + value.width *= this.previewVM.getPreviewWidth(); + value.height *= this.previewVM.getPreviewHeight(); + }) + this.faceBoundingBoxArr = faceBoxArr; + } + initZooms() { const zoomRange = this.cameraManager.getZoomRange(); const minZoom = zoomRange[0]; @@ -246,6 +275,17 @@ struct Index { this.flashBlackAnim(); } + @Builder + faceBox(faceBoxRect: camera.Rect) { + ForEach(calFaceBoxLinePoint(faceBoxRect), (linePoint: LinePoint) => { + Line() + .startPoint([0, 0]) + .endPoint([linePoint.increment.x, linePoint.increment.y]) + .stroke(Color.White) + .position({ x: linePoint.start.x, y: linePoint.start.y }) + }) + } + @Builder preview() { // [Start Stack] @@ -337,6 +377,12 @@ struct Index { .position(this.getExposurePosition()) } + if (!this.isFocusBoxVisible) { + ForEach(this.faceBoundingBoxArr, (value: camera.Rect) => { + this.faceBox(value); + }) + } + if (this.isDelayTakePhoto) { Text(`${this.photoRemainder}S`) .fontSize(44) diff --git a/entry/src/main/ets/utils/CommonUtil.ets b/entry/src/main/ets/utils/CommonUtil.ets index dc1a899d345721c0acbae23979a93d0e78d1c96f..c0c97297aad3c74f8690c0dbe97f434c0e9b36af 100644 --- a/entry/src/main/ets/utils/CommonUtil.ets +++ b/entry/src/main/ets/utils/CommonUtil.ets @@ -18,6 +18,9 @@ import { camera } from '@kit.CameraKit'; import { display } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; import { Logger } from 'commons'; +import { LinePoint } from '../viewmodels/PreviewViewModel'; + +const FACE_BOX_LINE_RATIO: number = 0.3; export function limitNumberInRange(src: number, range: number[]) { if (range.length < 2) { @@ -78,6 +81,36 @@ export function getClampedChildPosition(childSize: Size, parentSize: Size, point return { left, top }; } +// Calculate the coordinates of the face detection box. +export function calFaceBoxLinePoint(faceBoxRect: camera.Rect): LinePoint[] { + // The length of the sides of the box. + let lineLength: number = Math.min(faceBoxRect.width, faceBoxRect.height) * FACE_BOX_LINE_RATIO; + let linePoints: LinePoint[] = []; + + // The coordinates of the four vertices of the detection box. + let startPoints: camera.Point[] = [ + { x: faceBoxRect.topLeftX, y: faceBoxRect.topLeftY }, + { x: faceBoxRect.topLeftX + faceBoxRect.width, y: faceBoxRect.topLeftY }, + { x: faceBoxRect.topLeftX, y: faceBoxRect.topLeftY + faceBoxRect.height }, + { x: faceBoxRect.topLeftX + faceBoxRect.width, y: faceBoxRect.topLeftY + faceBoxRect.height }]; + + // Calculate the relative coordinates of each edge. + startPoints.forEach((startPoint: camera.Point) => { + let HorizontalLine: LinePoint = { + start: startPoint, + increment: { x: startPoint.x > faceBoxRect.topLeftX ? -lineLength : lineLength, y: 0 } + }; + + let verticalLine: LinePoint = { + start: startPoint, + increment: { x: 0, y: startPoint.y > faceBoxRect.topLeftY ? -lineLength : lineLength } + }; + + linePoints.push(HorizontalLine, verticalLine); + }); + return linePoints; +} + // [End getClampedChildPosition] export function showToast( diff --git a/entry/src/main/ets/viewmodels/PreviewViewModel.ets b/entry/src/main/ets/viewmodels/PreviewViewModel.ets index 87dcf0f760c6bd7d7b926c6a1d74a13fd701a141..c5a4b7e1c3ee194202428dd561fb0b70438a92c8 100644 --- a/entry/src/main/ets/viewmodels/PreviewViewModel.ets +++ b/entry/src/main/ets/viewmodels/PreviewViewModel.ets @@ -24,6 +24,11 @@ export enum CameraMode { VIDEO } +export interface LinePoint { + start: camera.Point, + increment: camera.Point +} + const TAG = 'PreviewViewModel'; /**