
鸿蒙跨端智能相册分类系统开发指南 原创
鸿蒙跨端智能相册分类系统开发指南
一、系统架构设计
基于HarmonyOS的AI能力和分布式技术,构建智能相册分类系统:
图像处理层:识别照片中的人物和场景
分类管理层:管理照片分类和标签
跨端同步层:多设备间同步分类结果和相册状态
用户界面层:展示分类结果和提供交互
!https://example.com/harmony-photo-classifier-arch.png
二、核心代码实现
图像分类服务
// PhotoClassifierService.ets
import ai from ‘@ohos.ai’;
import distributedData from ‘@ohos.distributedData’;
import { Photo, Person, Scene, Album } from ‘./PhotoTypes’;
class PhotoClassifierService {
private static instance: PhotoClassifierService = null;
private modelManager: ai.ModelManager;
private dataManager: distributedData.DataManager;
private listeners: ClassifierListener[] = [];
private constructor() {
this.initModelManager();
this.initDataManager();
public static getInstance(): PhotoClassifierService {
if (!PhotoClassifierService.instance) {
PhotoClassifierService.instance = new PhotoClassifierService();
return PhotoClassifierService.instance;
private initModelManager(): void {
try {
this.modelManager = ai.createModelManager(getContext());
// 加载人脸识别模型
this.modelManager.loadModel({
modelName: 'face_recognition',
modelPath: 'resources/rawfile/face_recognition.model',
callback: (err, data) => {
if (err) {
console.error('加载人脸识别模型失败:', JSON.stringify(err));
}
});
// 加载场景识别模型
this.modelManager.loadModel({
modelName: 'scene_recognition',
modelPath: 'resources/rawfile/scene_recognition.model',
callback: (err, data) => {
if (err) {
console.error('加载场景识别模型失败:', JSON.stringify(err));
}
});
catch (err) {
console.error('初始化模型管理器失败:', JSON.stringify(err));
}
private initDataManager(): void {
this.dataManager = distributedData.createDataManager({
bundleName: ‘com.example.photoclassifier’,
area: distributedData.Area.GLOBAL,
isEncrypted: true
});
this.dataManager.registerDataListener('photo_sync', (data) => {
this.handleSyncData(data);
});
public async requestPermissions(): Promise<boolean> {
try {
const permissions = [
'ohos.permission.USE_AI',
'ohos.permission.READ_MEDIA',
'ohos.permission.DISTRIBUTED_DATASYNC'
];
const result = await abilityAccessCtrl.requestPermissionsFromUser(
getContext(),
permissions
);
return result.grantedPermissions.length === permissions.length;
catch (err) {
console.error('请求权限失败:', JSON.stringify(err));
return false;
}
public async classifyPhoto(imageData: ArrayBuffer): Promise<Photo> {
try {
// 识别照片中的人物
const faceInput = {
data: imageData,
width: 224,
height: 224,
format: ‘RGB’
};
const faceOutput = await this.modelManager.runModel({
modelName: 'face_recognition',
input: faceInput
});
// 识别照片场景
const sceneInput = {
data: imageData,
width: 224,
height: 224,
format: 'RGB'
};
const sceneOutput = await this.modelManager.runModel({
modelName: 'scene_recognition',
input: sceneInput
});
const photo: Photo = {
id: Date.now().toString(),
imageData: imageData,
people: faceOutput.result.faces.map(face => ({
id: face.id,
name: '未知',
confidence: face.confidence,
boundingBox: face.boundingBox
})),
scene: {
type: sceneOutput.result.scene,
confidence: sceneOutput.result.confidence
},
timestamp: Date.now()
};
// 同步分类结果
this.syncPhoto(photo);
return photo;
catch (err) {
console.error('照片分类失败:', JSON.stringify(err));
throw err;
}
public async createAlbum(name: string, coverPhotoId: string): Promise<Album> {
const album: Album = {
id: Date.now().toString(),
name: name,
coverPhotoId: coverPhotoId,
photoIds: [],
createdAt: Date.now()
};
this.syncAlbum(album);
return album;
public async addPhotoToAlbum(photoId: string, albumId: string): Promise<void> {
this.dataManager.syncData('album_update', {
type: 'add_photo',
data: {
photoId: photoId,
albumId: albumId
},
timestamp: Date.now()
});
public async renamePerson(personId: string, name: string): Promise<void> {
this.dataManager.syncData('person_update', {
type: 'rename_person',
data: {
personId: personId,
name: name
},
timestamp: Date.now()
});
private syncPhoto(photo: Photo): void {
this.dataManager.syncData('photo_sync', {
type: 'photo_classified',
data: photo,
timestamp: Date.now()
});
private syncAlbum(album: Album): void {
this.dataManager.syncData('album_sync', {
type: 'album_created',
data: album,
timestamp: Date.now()
});
private handleSyncData(data: any): void {
if (!data) return;
switch (data.type) {
case 'photo_classified':
this.notifyPhotoClassified(data.data);
break;
case 'album_created':
this.notifyAlbumCreated(data.data);
break;
case 'add_photo':
this.notifyPhotoAddedToAlbum(data.data);
break;
case 'rename_person':
this.notifyPersonRenamed(data.data);
break;
}
private notifyPhotoClassified(photo: Photo): void {
this.listeners.forEach(listener => {
listener.onPhotoClassified?.(photo);
});
private notifyAlbumCreated(album: Album): void {
this.listeners.forEach(listener => {
listener.onAlbumCreated?.(album);
});
private notifyPhotoAddedToAlbum(data: { photoId: string, albumId: string }): void {
this.listeners.forEach(listener => {
listener.onPhotoAddedToAlbum?.(data.photoId, data.albumId);
});
private notifyPersonRenamed(data: { personId: string, name: string }): void {
this.listeners.forEach(listener => {
listener.onPersonRenamed?.(data.personId, data.name);
});
public addListener(listener: ClassifierListener): void {
if (!this.listeners.includes(listener)) {
this.listeners.push(listener);
}
public removeListener(listener: ClassifierListener): void {
this.listeners = this.listeners.filter(l => l !== listener);
}
interface ClassifierListener {
onPhotoClassified?(photo: Photo): void;
onAlbumCreated?(album: Album): void;
onPhotoAddedToAlbum?(photoId: string, albumId: string): void;
onPersonRenamed?(personId: string, name: string): void;
export const photoClassifierService = PhotoClassifierService.getInstance();
相册管理界面
// AlbumScreen.ets
import { photoClassifierService } from ‘./PhotoClassifierService’;
import { Photo, Person, Album } from ‘./PhotoTypes’;
@Component
export struct AlbumScreen {
@State hasPermission: boolean = false;
@State isProcessing: boolean = false;
@State photos: Photo[] = [];
@State people: Person[] = [];
@State albums: Album[] = [];
@State selectedTab: ‘photos’ ‘people’
‘albums’ = ‘photos’;
@State showAlbumDialog: boolean = false;
@State newAlbumName: string = ‘’;
@State selectedPhotos: string[] = [];
aboutToAppear() {
this.checkPermissions();
photoClassifierService.addListener({
onPhotoClassified: (photo) => {
this.handlePhotoClassified(photo);
},
onAlbumCreated: (album) => {
this.handleAlbumCreated(album);
},
onPhotoAddedToAlbum: (photoId, albumId) => {
this.handlePhotoAddedToAlbum(photoId, albumId);
},
onPersonRenamed: (personId, name) => {
this.handlePersonRenamed(personId, name);
});
aboutToDisappear() {
photoClassifierService.removeListener({
onPhotoClassified: (photo) => {
this.handlePhotoClassified(photo);
},
onAlbumCreated: (album) => {
this.handleAlbumCreated(album);
},
onPhotoAddedToAlbum: (photoId, albumId) => {
this.handlePhotoAddedToAlbum(photoId, albumId);
},
onPersonRenamed: (personId, name) => {
this.handlePersonRenamed(personId, name);
});
build() {
Column() {
// 标题栏
Row() {
Text('智能相册')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Button(this.hasPermission ? '添加照片' : '授权')
.width(100)
.onClick(() => {
if (this.hasPermission) {
this.pickPhotos();
else {
this.requestPermissions();
})
.padding(10)
.width('100%')
// 标签栏
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
// 照片视图
this.buildPhotosView()
}.tabBar('照片')
TabContent() {
// 人物视图
this.buildPeopleView()
}.tabBar('人物')
TabContent() {
// 相册视图
this.buildAlbumsView()
}.tabBar('相册')
.index(0)
.vertical(false)
.barWidth('100%')
.barHeight(40)
.onChange((index: number) => {
this.selectedTab = ['photos', 'people', 'albums'][index] as 'photos' 'people'
‘albums’;
})
.width(‘100%’)
.height('100%')
.padding(20)
// 创建相册对话框
if (this.showAlbumDialog) {
DialogComponent({
title: '新建相册',
content: this.buildAlbumDialogContent(),
confirm: {
value: '创建',
action: () => this.createAlbum()
},
cancel: {
value: '取消',
action: () => this.showAlbumDialog = false
})
}
private buildPhotosView(): void {
if (this.photos.length === 0) {
Column() {
Text(‘暂无照片’)
.fontSize(18)
.margin({ bottom: 10 })
Text('点击"添加照片"按钮导入照片')
.fontSize(16)
.fontColor('#666666')
.padding(20)
.width('90%')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 50 })
else {
Grid() {
ForEach(this.photos, (photo) => {
GridItem() {
Stack() {
Image(photo.imageData)
.width('100%')
.height(150)
.objectFit(ImageFit.Cover)
if (this.selectedPhotos.includes(photo.id)) {
Image($r('app.media.ic_check'))
.width(24)
.height(24)
.position({ x: '85%', y: '85%' })
}
.borderRadius(8)
.onClick(() => {
this.togglePhotoSelection(photo.id);
})
})
.columnsTemplate(‘1fr 1fr 1fr’)
.rowsGap(10)
.columnsGap(10)
.margin({ top: 10 })
if (this.selectedPhotos.length > 0) {
Row() {
Button('添加到相册')
.width(150)
.height(50)
.fontSize(18)
.onClick(() => {
this.showAlbumDialog = true;
})
Button('取消选择')
.width(150)
.height(50)
.fontSize(18)
.margin({ left: 20 })
.onClick(() => {
this.selectedPhotos = [];
})
.margin({ top: 20 })
}
private buildPeopleView(): void {
if (this.people.length === 0) {
Column() {
Text('未识别到人物')
.fontSize(18)
.margin({ bottom: 10 })
Text('请添加包含人物的照片')
.fontSize(16)
.fontColor('#666666')
.padding(20)
.width('90%')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 50 })
else {
List({ space: 10 }) {
ForEach(this.people, (person) => {
ListItem() {
Row() {
Image(person.faceImage)
.width(60)
.height(60)
.borderRadius(30)
.margin({ right: 15 })
Text(person.name)
.fontSize(16)
.layoutWeight(1)
Text(${person.photoCount}张照片)
.fontSize(14)
.fontColor('#666666')
.padding(10)
.width('100%')
.onClick(() => {
this.showPersonPhotos(person.id);
})
})
.height(‘80%’)
}
private buildAlbumsView(): void {
if (this.albums.length === 0) {
Column() {
Text(‘暂无相册’)
.fontSize(18)
.margin({ bottom: 10 })
Text('选择照片后可以创建相册')
.fontSize(16)
.fontColor('#666666')
.padding(20)
.width('90%')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 50 })
else {
List({ space: 10 }) {
ForEach(this.albums, (album) => {
ListItem() {
Row() {
Image(this.getAlbumCover(album))
.width(80)
.height(80)
.borderRadius(8)
.margin({ right: 15 })
Column() {
Text(album.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 })
Text(${album.photoIds.length}张照片)
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
.padding(10)
.width('100%')
.onClick(() => {
this.showAlbumPhotos(album.id);
})
})
.height(‘80%’)
}
private buildAlbumDialogContent(): void {
Column() {
TextInput({ placeholder: ‘输入相册名称’, text: this.newAlbumName })
.onChange((value: string) => {
this.newAlbumName = value;
})
if (this.selectedPhotos.length > 0) {
Text(将添加${this.selectedPhotos.length}张照片到相册)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 10 })
}
.padding(20)
.width('100%')
private getAlbumCover(album: Album): Resource {
const coverPhoto = this.photos.find(p => p.id === album.coverPhotoId);
return coverPhoto ? coverPhoto.imageData : $r('app.media.ic_default_album');
private async checkPermissions(): Promise<void> {
try {
const permissions = [
'ohos.permission.USE_AI',
'ohos.permission.READ_MEDIA',
'ohos.permission.DISTRIBUTED_DATASYNC'
];
const result = await abilityAccessCtrl.verifyPermissions(
getContext(),
permissions
);
this.hasPermission = result.every(perm => perm.granted);
catch (err) {
console.error('检查权限失败:', JSON.stringify(err));
this.hasPermission = false;
}
private async requestPermissions(): Promise<void> {
this.hasPermission = await photoClassifierService.requestPermissions();
if (!this.hasPermission) {
prompt.showToast({ message: '授权失败,无法访问照片' });
}
private async pickPhotos(): Promise<void> {
try {
const picker = new photo.Picker();
const result = await picker.select({
type: photo.PickerType.IMAGE,
maxSelectNumber: 20
});
if (result.photoUris.length === 0) return;
this.isProcessing = true;
for (const uri of result.photoUris) {
const file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
const buffer = await fileIo.read(file.fd, { length: 0 });
await fileIo.close(file.fd);
const photo = await photoClassifierService.classifyPhoto(buffer.buffer);
this.photos = [...this.photos, photo];
} catch (err) {
console.error('选择照片失败:', JSON.stringify(err));
prompt.showToast({ message: '选择照片失败,请重试' });
finally {
this.isProcessing = false;
}
private togglePhotoSelection(photoId: string): void {
if (this.selectedPhotos.includes(photoId)) {
this.selectedPhotos = this.selectedPhotos.filter(id => id !== photoId);
else {
this.selectedPhotos = [...this.selectedPhotos, photoId];
}
private async createAlbum(): Promise<void> {
if (!this.newAlbumName.trim()) {
prompt.showToast({ message: ‘请输入相册名称’ });
return;
if (this.selectedPhotos.length === 0) {
prompt.showToast({ message: '请选择要添加到相册的照片' });
return;
try {
const album = await photoClassifierService.createAlbum(
this.newAlbumName,
this.selectedPhotos[0]
);
for (const photoId of this.selectedPhotos) {
await photoClassifierService.addPhotoToAlbum(photoId, album.id);
this.albums = […this.albums, album];
this.selectedPhotos = [];
this.showAlbumDialog = false;
this.newAlbumName = '';
catch (err) {
console.error('创建相册失败:', JSON.stringify(err));
prompt.showToast({ message: '创建相册失败,请重试' });
}
private handlePhotoClassified(photo: Photo): void {
this.photos = […this.photos, photo];
// 更新人物列表
photo.people.forEach(person => {
const existingPerson = this.people.find(p => p.id === person.id);
if (existingPerson) {
existingPerson.photoCount += 1;
else {
this.people = [...this.people, {
id: person.id,
name: person.name,
faceImage: this.extractFaceImage(photo.imageData, person.boundingBox),
photoCount: 1
}];
});
private extractFaceImage(imageData: ArrayBuffer, boundingBox: any): ArrayBuffer {
// 实际应用中应实现从原图中裁剪人脸区域
return imageData; // 简化为返回原图
private handleAlbumCreated(album: Album): void {
this.albums = [...this.albums, album];
private handlePhotoAddedToAlbum(photoId: string, albumId: string): void {
const album = this.albums.find(a => a.id === albumId);
if (album && !album.photoIds.includes(photoId)) {
album.photoIds = [...album.photoIds, photoId];
}
private handlePersonRenamed(personId: string, name: string): void {
const person = this.people.find(p => p.id === personId);
if (person) {
person.name = name;
}
private showPersonPhotos(personId: string): void {
// 导航到人物照片列表页面
router.push({
url: ‘pages/PersonPhotos’,
params: { personId: personId }
});
private showAlbumPhotos(albumId: string): void {
// 导航到相册照片列表页面
router.push({
url: 'pages/AlbumPhotos',
params: { albumId: albumId }
});
}
类型定义
// PhotoTypes.ets
export interface Photo {
id: string;
imageData: ArrayBuffer;
people: {
id: string;
name: string;
confidence: number;
boundingBox: any;
}[];
scene: {
type: string;
confidence: number;
};
timestamp: number;
export interface Person {
id: string;
name: string;
faceImage: ArrayBuffer;
photoCount: number;
export interface Album {
id: string;
name: string;
coverPhotoId: string;
photoIds: string[];
createdAt: number;
三、项目配置与权限
权限配置
// module.json5
“module”: {
"requestPermissions": [
“name”: “ohos.permission.USE_AI”,
"reason": "使用AI模型识别照片内容"
},
“name”: “ohos.permission.READ_MEDIA”,
"reason": "访问相册照片"
},
“name”: “ohos.permission.DISTRIBUTED_DATASYNC”,
"reason": "同步分类结果"
],
"abilities": [
“name”: “MainAbility”,
"type": "page",
"visible": true
]
}
四、总结与扩展
本智能相册分类系统实现了以下核心功能:
智能分类:自动识别照片中的人物和场景
人物管理:聚合同一人物的所有照片
相册创建:支持自定义相册分类
跨设备同步:多设备间同步分类结果和相册
扩展方向:
场景相册:自动创建旅行、家庭聚会等场景相册
智能搜索:通过自然语言搜索照片
回忆功能:按时间线展示历史照片
共享相册:与家人朋友共享特定相册
照片增强:自动优化照片质量
云备份:将照片备份到云端
通过HarmonyOS的分布式技术,我们构建了一个智能化的相册管理系统,能够自动整理照片并在多设备间同步分类结果,大大提升了照片管理的效率和体验。
