基于HarmonyOS分布式文件系统和AI语音识别功能开发的语音照相机
1. 项目介绍
生活中我们经常会遇到这样的例子:张三和李四外出旅游,他们俩需要相互拍照片,张三在远处摆好了姿势,李四拍完照片以后往往需要把手机递给张三去看这张照片是否符合他的要求,如果不符合,李四又要跑回远处重新拍照,反反复复、来来回回,可能就错过了某些美丽的风景。或者一群人需要拍合影,把手机摆好后大家大喊一声"茄子",拍完以后大伙蜂拥而至到手机前面看这张照片,可能每个人对这张照片观察的点都不一样,大家都会把自己的头像放大,仔细去看,很是麻烦。最低效的是,拍好的照片还需要来回保存,彼此之间反复分享,来回挑选自己喜欢的照片。
为了解决上述问题,我们基于HarmonyOS分布式文件系统和AI语音识别功能开发了一款分布式语音照相机,这款相机能将拍摄的照片实时共享到同一分布式网络下的不同设备中,这样大家都可以看到拍摄的照片,就不需要一窝蜂的跑到一台手机前,不满意也无需来回反复地传递手机,对于满意的照片还可以实时保存,避免后期反复挑选,高效的解决了上述例子中的痛点问题。
本篇Codelab将为您重点介绍相机模块、AI语音识别、分布式文件系统三项HarmonyOS基础能力,正式介绍之前,我们先对分布式语音照相机进行展示,让您快速了解到本篇Codelab所实现的功能。
图1-1 分布式语音照相机效果
图1-2 分布式语音照相机UX预览图
2. 代码结构解读
本篇Codelab我们只是对核心代码进行讲解,您可以在最后的参考中下载完整代码,首先来介绍下整个工程的代码结构:
- listener:MyAsrListener,是自动语音识别(Automatic Speech Recognition, ASR)的回调函数。
- slice:MainAbilitySlice为相机的主页面,ImageAbilitySlice为图像预页面。
- utils:封装了公共方法,包括相机的工具类、分布式文件系统的工具类、日志工具类、绘图工具类、权限工具类。
- resources:存放工程使用到的资源文件,其中resources\base\layout下存放xml布局文件;resources\base\media下存放图片资源。
- config.json:配置文件。
3. 相机模块
HarmonyOS相机模块支持相机业务的开发,相机模块主要工作是给相机应用开发者提供基本的相机API接口,用于使用相机系统的功能。开发者可以通过调用已开放的接口实现相机硬件的访问、操作和新功能开发,最常见的操作如:预览、拍照、连拍和录像等。您可以参考HarmonyOS开发者指导中相机开发指导进行学习。
分布式语音相机权限申请
在使用相机之前,需要申请相机的相关权限,保证应用拥有相机硬件及其他功能权限,应用权限的介绍请参考权限章节,本工程涉及权限如下表。
相机权限列表
权限名称 |
权限属性值 |
是否必选 |
相机权限 |
ohos.permission.CAMERA |
必选(照相机权限) |
分布式数据管理权限 |
ohos.permission.DISTRIBUTED_DATASYNC |
必选(分布式文件服务权限) |
录音权限 |
ohos.permission.MICROPHONE |
必选(语音功能必备权限) |
存储权限 |
ohos.permission.WRITE_MEDIA |
可选(需要保存图像及视频到设备的外部存储时申请) |
位置权限 |
ohos.permission.MEDIA_LOCATION |
可选(需要保存图像及视频位置信息时申请) |
相机设备创建
CameraKit类是相机的入口API类,用于获取相机设备特性、打开相机。相机设备创建的代码如下:
private void openCamera() {
imageReceiver = ImageReceiver.create(SCREEN_WIDTH, SCREEN_HEIGHT, ImageFormat.JPEG, IMAGE_RCV_CAPACITY);
imageReceiver.setImageArrivalListener(this::saveImage);
CameraKit cameraKit = CameraKit.getInstance(getApplicationContext());
String[] cameraList = cameraKit.getCameraIds();
String cameraId = cameraList.length > 1 && isCameraRear ? cameraList[1] : cameraList[0];
CameraStateCallbackImpl cameraStateCallback = new CameraStateCallbackImpl();
cameraKit.createCamera(cameraId, cameraStateCallback, creamEventHandler);
}
相机设备配置
当一个新的相机设备成功创建后,首先需要对相机进行配置,调用configure(CameraConfig)方法实现配置。相机配置主要是设置预览、拍照、录像用到的Surface(详见ohos.agp.graphics.Surface),没有配置过Surface,相应的功能不能使用。相机设备配置的代码如下:
private void initSurface() {
surfaceProvider = new SurfaceProvider(this);
DirectionalLayout.LayoutConfig params = new DirectionalLayout.LayoutConfig(
ComponentContainer.LayoutConfig.MATCH_PARENT, ComponentContainer.LayoutConfig.MATCH_PARENT);
surfaceProvider.setLayoutConfig(params);
surfaceProvider.pinToZTop(true);
surfaceProvider.getSurfaceOps().get().addCallback(new SurfaceCallBack());
if (findComponentById(ResourceTable.Id_surface_container) instanceof ComponentContainer) {
((ComponentContainer) findComponentById(ResourceTable.Id_surface_container)).addComponent(surfaceProvider);
}
}
class CameraStateCallbackImpl extends CameraStateCallback {
CameraStateCallbackImpl() {}
@Override
public void onCreated(Camera camera) {
previewSurface = surfaceProvider.getSurfaceOps().get().getSurface();
if (previewSurface == null) {
HiLog.info(TAG, "create camera filed, preview surface is null");
return;
}
try {
Thread.sleep(200);
} catch (InterruptedException exception) {
HiLog.info(TAG, "Waiting to be interrupted");
}
CameraConfig.Builder cameraConfigBuilder = camera.getCameraConfigBuilder();
cameraConfigBuilder.addSurface(previewSurface);
cameraConfigBuilder.addSurface(imageReceiver.getRecevingSurface());
camera.configure(cameraConfigBuilder.build());
cameraDevice = camera;
enableImageGroup();
}
@Override
public void onConfigured(Camera camera) {
FrameConfig.Builder framePreviewConfigBuilder = camera.getFrameConfigBuilder(FRAME_CONFIG_PREVIEW);
framePreviewConfigBuilder.addSurface(previewSurface);
try {
camera.triggerLoopingCapture(framePreviewConfigBuilder.build());
} catch (IllegalArgumentException e) {
HiLog.error(TAG, "Argument Exception");
} catch (IllegalStateException e) {
HiLog.error(TAG, "State Exception");
}
}
private void enableImageGroup() {
takePictureImage.setEnabled(true);
switchCameraImage.setEnabled(true);
}
}
相机帧捕获
拍照功能属于相机应用的最重要功能之一,而且照片质量对用户至关重要。相机模块基于相机复杂的逻辑,从应用接口层到器件驱动层都已经默认的做好了最适合用户的配置,这些默认配置尽可能地保证用户拍出的每张照片的质量。相机拍照的代码如下:
private void takePicture(Component component) {
HiLog.info(TAG, "takePicture");
if (!takePictureImage.isEnabled()) {
HiLog.info(TAG, "takePicture return");
return;
}
if (cameraDevice == null || imageReceiver == null) {
return;
}
FrameConfig.Builder framePictureConfigBuilder = cameraDevice.getFrameConfigBuilder(FRAME_CONFIG_PICTURE);
framePictureConfigBuilder.addSurface(imageReceiver.getRecevingSurface());
FrameConfig pictureFrameConfig = framePictureConfigBuilder.build();
cameraDevice.triggerSingleCapture(pictureFrameConfig);
}
相机设备释放
使用完相机后,必须通过release()来关闭相机和释放资源,否则可能导致其他相机应用无法启动。一旦相机被释放,它所提供的操作就不能再被调用,否则会导致不可预期的结果,或是会引发状态异常。相机设备释放的代码如下:
private void releaseCamera() {
if (cameraDevice != null) {
cameraDevice.release();
cameraDevice = null;
}
if (imageReceiver != null) {
imageReceiver.release();
imageReceiver = null;
}
if (creamEventHandler != null) {
creamEventHandler.removeAllEvent();
creamEventHandler = null;
}
}
4. AI语音识别模块
语音识别功能提供面向移动终端的语音识别能力。它基于华为智慧引擎(HUAWEI HiAI Engine)中的语音识别引擎,向开发者提供人工智能应用层API。该技术可以将语音文件、实时语音数据流转换为汉字序列,准确率达到90%以上(本地识别95%)。本相机语音识别的关键词为"拍照"、"茄子",当有人说出这两个词的时候,相机就会调用拍照的功能,拍摄一张照片。
语音识别技术,也称为自动语音识别(Automatic Speech Recognition, ASR),可以基于机器识别和理解,将语音信号转变为文本或命令。本例中初始化ASR服务的代码如下所示:
private void initASRClient() {
asrClient = AsrClient.createAsrClient(this).orElse(null);
TaskDispatcher taskDispatcher = getAbility().getMainTaskDispatcher();
taskDispatcher.asyncDispatch(new Runnable() {
@Override
public void run() {
initListener();
}
});
}
语音识别的开发步骤和API的相关调用可以参考语音识别开发指导。ASR引擎语音识别过程中,当部分识别结果可以获取到时,就会调用onIntermediateResults回调处理中间过程的识别结果。例如检查到有人在说话,就会回调onIntermediateResults,得到识别结果的文本内容result。而后,通过调用recognizeWords函数,对照关键词中是否存在"拍照"、"茄子",得到布尔类型的识别结果recognizeResult,如果recognizeResult为true,就会调用takePicture,进行拍照,关键代码如下所示:
@Override
public void onIntermediateResults(PacMap pacMap) {
super.onIntermediateResults(pacMap);
HiLog.info(TAG, "======onIntermediateResults:");
String result = pacMap.getString(AsrResultKey.RESULTS_INTERMEDIATE);
boolean recognizeResult = recognizeWords(result);
if (recognizeResult && !recognizeOver) {
recognizeOver = true;
takePicture(new Component(getContext()));
asrClient.stopListening();
}
}
5. 分布式文件系统
分布式文件服务能够为用户设备中的应用程序提供多设备之间的文件共享能力,支持相同帐号下同一应用文件的跨设备访问,应用程序可以不感知文件所在的存储设备,能够在多个设备之间无缝获取文件。分布式文件服务采用无中心节点的设计,每个设备都存储一份全量的文件元数据和本设备上产生的分布式文件,元数据在多台设备间互相同步,当应用需要访问分布式文件时,分布式文件服务首先查询本设备上的文件元数据,获取文件所在的存储设备,然后对存储设备上的分布式文件服务发起文件访问请求,将文件内容读取到本地。分布式文件服务运作示意图如下所示:本篇Codelab中我们为您提供了一个分布式文件系统的工具类DistributeFileUtil。拍摄完图片后,相机会先将照片保存到本地,而后会将本地的照片拷贝到分布式文件系统中,拷贝图片到分布式文件服务的示例代码如下所示:
public static void copyPicToDistributedDir(Context context, File sourceFile, String fileName) {
InputStream in = null;
OutputStream out = null;
File disPath = getDisFile(context, fileName);
if (disPath == null) {
return;
}
try {
in = new FileInputStream(sourceFile);
out = new FileOutputStream(disPath);
byte[] buffer = new byte[CACHE_SIZE];
int len;
while ((len = in.read(buffer)) != IO_END_LEN) {
out.write(buffer, 0, len);
}
// 拷贝成功,启动远程FA
startRemoteFas(context, disPath.getCanonicalPath());
} catch (IOException e) {
LogUtil.error(TAG, "copy error occur while copy");
showTip(context, "分布式是文件服务异常,拷贝异常!");
} finally {
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
} catch (IOException e) {
LgUtil.error(TAG, "io close exception");
}
}
}
保存完成后,系统会自动拉起该分布式网络下其他手机的图库,对所拍摄的照片进行展示,启动其他手机相册的代码如下所示:
private static void startRemoteFas(Context context, String filePath) {
List<DeviceInfo> deviceInfos =
DeviceManager.getDeviceList(ohos.distributedschedule.interwork.DeviceInfo.FLAG_GET_ONLINE_DEVICE);
if (deviceInfos == null || deviceInfos.size() < 1) {
return;
}
Intent[] intents = new Intent[deviceInfos.size()];
for (int i = 0; i < deviceInfos.size(); i++) {
Intent intent = new Intent();
intent.setParam("filePath", filePath);
Operation operation = new Intent.OperationBuilder()
.withDeviceId(deviceInfos.get(i).getDeviceId())
.withBundleName(context.getBundleName())
.withAbilityName(ImageAbility.class.getName())
.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
.build();
intent.setOperation(operation);
intents[i] = intent;
}
context.startAbilities(intents);
}
6. 回顾和总结
本篇Codelab从生活中好朋友之间互拍照片、拍摄集体照时的痛点问题出发,利用HarmonyOS分布式文件系统和AI语音识别功能开发了一款分布式语音照相机。使用该款相机应用,处于同一分布式网络下的不同设备可以实时看到主设备所拍摄的照片,这高效地解决了互拍照片时需要来回跑动传递手机的痛点问题,此外主设备还支持了语音控制的拍照功能,使您在远处就可以对相机进行控制。
从技术功能点上,我们分别向您介绍了相机模块的开发、AI语音识别模块的开发、分布式文件系统的开发,教您手把手的学会了一款HarmonyOS分布式语音照相机。如果您想对拍照后的图片进行操作(如旋转、剪裁、缩放、镜像等),您还可以参考图像开发的相关知识,对相机进行优化。
7. 恭喜您
目前您已经成功完成了Codelab并且学到了:
- HarmonyOS相机模块
- AI语音模块开发
- 分布式文件系统
8. 参考
请问,两台手机连接到一个分布式网络下了但是拉起不了另一个手机的图库是为什么?
支持一波~~