
鸿蒙跨端植物识别园艺助手开发指南 原创
鸿蒙跨端植物识别园艺助手开发指南
一、项目概述
本指南基于HarmonyOS的分布式能力和AI图像识别技术,开发一款智能植物识别园艺助手应用。该系统能够通过设备摄像头识别植物种类,获取植物养护知识,并将识别结果同步到多设备,借鉴了《鸿蒙跨端U同步》中多设备数据同步的技术原理。
二、系统架构
±--------------------+ ±--------------------+ ±--------------------+
主设备 <-----> 分布式数据总线 <-----> 从设备
(手机/平板) (Distributed Bus) (智能手表/其他设备)
±---------±---------+ ±---------±---------+ ±---------±---------+
±---------v----------+ ±---------v----------+ ±---------v----------+
图像识别模块 植物知识模块 数据同步模块
(Image Recognition) (Plant Knowledge) (Data Sync)
±--------------------+ ±--------------------+ ±--------------------+
三、核心代码实现
植物识别服务
// src/main/ets/service/PlantService.ts
import { distributedData } from ‘@ohos.data.distributedData’;
import { BusinessError } from ‘@ohos.base’;
import { camera } from ‘@ohos.multimedia.camera’;
import { image } from ‘@ohos.multimedia.image’;
import { http } from ‘@ohos.net.http’;
import { imageClassification } from ‘@ohos.ai.imageClassification’;
interface PlantInfo {
id: string;
name: string;
scientificName: string;
confidence: number;
timestamp: number;
imageUri: string;
interface PlantDetail {
id: string;
watering: string;
sunlight: string;
soil: string;
temperature: string;
fertilization: string;
pruning: string;
export class PlantService {
private static instance: PlantService;
private kvStore: distributedData.KVStore | null = null;
private readonly STORE_ID = ‘plant_data_store’;
private cameraInput: camera.CameraInput | null = null;
private previewOutput: camera.PreviewOutput | null = null;
private imageClassifier: imageClassification.ImageClassifier | null = null;
private httpRequest = http.createHttp();
private plantHistory: PlantInfo[] = [];
private readonly API_KEY = ‘YOUR_PLANT_API_KEY’; // 替换为实际API密钥
private readonly API_URL = ‘https://plant-api.example.com/v1/identify’;
private constructor() {
this.initKVStore();
this.initCamera();
this.initImageClassifier();
public static getInstance(): PlantService {
if (!PlantService.instance) {
PlantService.instance = new PlantService();
return PlantService.instance;
private async initKVStore(): Promise<void> {
try {
const options: distributedData.KVManagerConfig = {
bundleName: 'com.example.plantidentifier',
userInfo: {
userId: '0',
userType: distributedData.UserType.SAME_USER_ID
};
const kvManager = distributedData.createKVManager(options);
this.kvStore = await kvManager.getKVStore({
storeId: this.STORE_ID,
options: {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true,
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION
});
this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_REMOTE, (data) => {
data.insertEntries.forEach((entry: distributedData.Entry) => {
if (entry.key === 'plant_history') {
this.notifyHistoryChange(entry.value.value as PlantInfo[]);
});
});
catch (e) {
console.error(Failed to initialize KVStore. Code: {e.code}, message: {e.message});
}
private async initCamera(): Promise<void> {
try {
const cameraManager = camera.getCameraManager();
const cameras = cameraManager.getSupportedCameras();
if (cameras.length === 0) {
console.error(‘No camera available’);
return;
// 使用后置摄像头
this.cameraInput = cameraManager.createCameraInput(cameras[0]);
await this.cameraInput.open();
// 创建预览输出
const surfaceId = 'previewSurface';
this.previewOutput = cameraManager.createPreviewOutput(surfaceId);
// 创建会话并开始预览
const captureSession = cameraManager.createCaptureSession();
await captureSession.beginConfig();
await captureSession.addInput(this.cameraInput);
await captureSession.addOutput(this.previewOutput);
await captureSession.commitConfig();
await captureSession.start();
catch (e) {
console.error(Failed to initialize camera. Code: {e.code}, message: {e.message});
}
private async initImageClassifier(): Promise<void> {
try {
const config: imageClassification.ImageClassifierConfig = {
modelType: imageClassification.ModelType.PLANT, // 植物识别模型
scoreThreshold: 0.5
};
this.imageClassifier = await imageClassification.createImageClassifier(config);
catch (e) {
console.error(Failed to initialize image classifier. Code: {e.code}, message: {e.message});
}
public async identifyPlant(imageObj: image.Image): Promise<PlantInfo | null> {
if (!this.imageClassifier) return null;
try {
// 本地AI识别
const localResult = await this.localIdentify(imageObj);
// 如果本地识别置信度低,调用云端API
if (localResult.confidence < 0.7) {
const cloudResult = await this.cloudIdentify(imageObj);
if (cloudResult) return cloudResult;
return localResult;
catch (e) {
console.error(Failed to identify plant. Code: {e.code}, message: {e.message});
return null;
}
private async localIdentify(imageObj: image.Image): Promise<PlantInfo> {
if (!this.imageClassifier) throw new Error(‘Image classifier not initialized’);
const results = await this.imageClassifier.classify(imageObj);
const bestMatch = results[0];
return {
id: local_${Date.now()},
name: bestMatch.className,
scientificName: '', // 本地模型可能不提供学名
confidence: bestMatch.score,
timestamp: Date.now(),
imageUri: '' // 实际应用中应保存图片路径
};
private async cloudIdentify(imageObj: image.Image): Promise<PlantInfo | null> {
try {
// 将图像转换为Base64
const imageData = await this.imageToBase64(imageObj);
// 调用云端植物识别API
const response = await this.httpRequest.request(
this.API_URL,
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'API-Key': this.API_KEY
},
extraData: {
images: [imageData],
modifiers: ['common_names', 'scientific_names'],
plant_details: ['common_names']
}
);
const result = JSON.parse(response.result);
if (result.suggestions && result.suggestions.length > 0) {
const suggestion = result.suggestions[0];
return {
id: suggestion.id,
name: suggestion.plant_name,
scientificName: suggestion.scientific_name,
confidence: suggestion.probability,
timestamp: Date.now(),
imageUri: '' // 实际应用中应保存图片路径
};
} catch (e) {
console.error(Failed to identify plant via cloud API. Code: {e.code}, message: {e.message});
return null;
private async imageToBase64(imageObj: image.Image): Promise<string> {
// 简化的图像转换逻辑 (实际应用中应使用更完整的实现)
const buffer = await imageObj.getComponent(image.ImageComponent.JPEG);
return data:image/jpeg;base64,${buffer.toString('base64')};
public async getPlantDetail(plantId: string): Promise<PlantDetail | null> {
try {
// 调用云端API获取植物详情
const response = await this.httpRequest.request(
${this.API_URL}/details,
method: http.RequestMethod.GET,
header: {
'API-Key': this.API_KEY
},
extraData: {
plant_id: plantId
}
);
const detail = JSON.parse(response.result);
return {
id: plantId,
watering: detail.watering || '未知',
sunlight: detail.sunlight || '未知',
soil: detail.soil || '未知',
temperature: detail.temperature || '未知',
fertilization: detail.fertilization || '未知',
pruning: detail.pruning || '未知'
};
catch (e) {
console.error(Failed to get plant details. Code: {e.code}, message: {e.message});
return null;
}
private async addToHistory(plantInfo: PlantInfo): Promise<void> {
this.plantHistory.unshift(plantInfo);
if (this.plantHistory.length > 50) {
this.plantHistory = this.plantHistory.slice(0, 50);
await this.syncHistory();
private async syncHistory(): Promise<void> {
if (this.kvStore) {
try {
await this.kvStore.put('plant_history', { value: this.plantHistory });
catch (e) {
console.error(Failed to sync plant history. Code: {e.code}, message: {e.message});
}
private notifyHistoryChange(newHistory: PlantInfo[]): void {
// 合并新旧历史记录,去重
const mergedHistory = [...this.plantHistory];
newHistory.forEach(newItem => {
if (!mergedHistory.some(item => item.id === newItem.id)) {
mergedHistory.push(newItem);
});
// 按时间戳排序
this.plantHistory = mergedHistory.sort((a, b) => b.timestamp - a.timestamp).slice(0, 50);
public async getPlantHistory(): Promise<PlantInfo[]> {
if (!this.kvStore) return this.plantHistory;
try {
const entry = await this.kvStore.get('plant_history');
return entry?.value || this.plantHistory;
catch (e) {
console.error(Failed to get plant history. Code: {e.code}, message: {e.message});
return this.plantHistory;
}
public async captureAndIdentify(): Promise<PlantInfo | null> {
if (!this.previewOutput) return null;
try {
const imageObj = await this.previewOutput.getFrame();
const plantInfo = await this.identifyPlant(imageObj);
if (plantInfo) {
await this.addToHistory(plantInfo);
imageObj.release();
return plantInfo;
catch (e) {
console.error(Failed to capture and identify plant. Code: {e.code}, message: {e.message});
return null;
}
public async destroy(): Promise<void> {
if (this.kvStore) {
this.kvStore.off(‘dataChange’);
if (this.cameraInput) {
await this.cameraInput.close();
if (this.imageClassifier) {
this.imageClassifier.release();
}
植物识别组件
// src/main/ets/components/PlantIdentifier.ets
@Component
export struct PlantIdentifier {
private plantService = PlantService.getInstance();
@State identifiedPlant: PlantInfo | null = null;
@State plantDetail: PlantDetail | null = null;
@State previewSurfaceId: string = ‘previewSurface’;
@State showDetail: boolean = false;
@State history: PlantInfo[] = [];
aboutToAppear(): void {
this.loadHistory();
private async loadHistory(): Promise<void> {
this.history = await this.plantService.getPlantHistory();
build() {
Stack() {
// 摄像头预览
CameraPreview({ surfaceId: this.previewSurfaceId })
.width('100%')
.height('60%');
// 识别按钮
Button('识别植物')
.type(ButtonType.Circle)
.width(80)
.height(80)
.backgroundColor('#4CAF50')
.fontColor('#FFFFFF')
.position({ x: '50%', y: '80%' })
.margin({ left: -40 })
.onClick(() => {
this.identifyPlant();
});
// 识别结果
if (this.identifiedPlant) {
this.buildResultCard()
.position({ x: '10%', y: '10%' });
// 历史记录按钮
Button($r('app.media.ic_history'))
.type(ButtonType.Circle)
.width(60)
.height(60)
.backgroundColor('#2196F3')
.fontColor('#FFFFFF')
.position({ x: '85%', y: '10%' })
.onClick(() => {
this.showHistory = true;
});
.width(‘100%’)
.height('100%')
// 植物详情对话框
if (this.showDetail && this.plantDetail) {
Dialog.show({
title: this.identifiedPlant?.name || '植物详情',
content: this.buildDetailContent(),
confirm: {
value: '关闭',
action: () => {
this.showDetail = false;
}
});
// 历史记录对话框
if (this.showHistory) {
Dialog.show({
title: '识别历史',
content: this.buildHistoryContent(),
confirm: {
value: '关闭',
action: () => {
this.showHistory = false;
}
});
}
@Builder
private buildResultCard() {
Column() {
Text(this.identifiedPlant?.name || ‘未知植物’)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 });
Text(置信度: ${Math.floor((this.identifiedPlant?.confidence || 0) * 100)}%)
.fontSize(14)
.fontColor('#666666')
.margin({ bottom: 10 });
Button('查看养护方法')
.type(ButtonType.Capsule)
.width('80%')
.backgroundColor('#FF4081')
.fontColor('#FFFFFF')
.margin({ bottom: 10 })
.onClick(() => {
this.showPlantDetail();
});
Button('添加到我的花园')
.type(ButtonType.Capsule)
.width('80%')
.backgroundColor('#2196F3')
.fontColor('#FFFFFF')
.onClick(() => {
this.addToMyGarden();
});
.width(‘80%’)
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(15)
.shadow({ radius: 10, color: '#E0E0E0', offsetX: 0, offsetY: 5 });
@Builder
private buildDetailContent() {
if (!this.plantDetail) return;
Scroll() {
Column() {
Row() {
Text('浇水:')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ right: 10 });
Text(this.plantDetail.watering)
.fontSize(16);
.margin({ bottom: 15 });
Row() {
Text('光照:')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ right: 10 });
Text(this.plantDetail.sunlight)
.fontSize(16);
.margin({ bottom: 15 });
Row() {
Text('土壤:')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ right: 10 });
Text(this.plantDetail.soil)
.fontSize(16);
.margin({ bottom: 15 });
Row() {
Text('温度:')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ right: 10 });
Text(this.plantDetail.temperature)
.fontSize(16);
.margin({ bottom: 15 });
Row() {
Text('施肥:')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ right: 10 });
Text(this.plantDetail.fertilization)
.fontSize(16);
.margin({ bottom: 15 });
Row() {
Text('修剪:')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ right: 10 });
Text(this.plantDetail.pruning)
.fontSize(16);
}
.width('100%')
.padding(10)
.width(‘100%’)
.height(300)
@Builder
private buildHistoryContent() {
Column() {
if (this.history.length > 0) {
List({ space: 10 }) {
ForEach(this.history, (item) => {
ListItem() {
Row() {
Image($r(‘app.media.ic_plant’))
.width(40)
.height(40)
.margin({ right: 15 });
Column() {
Text(item.name)
.fontSize(16)
.fontWeight(FontWeight.Bold);
Text(new Date(item.timestamp).toLocaleString())
.fontSize(12)
.fontColor('#666666');
.layoutWeight(1);
Text(${Math.floor(item.confidence * 100)}%)
.fontSize(14)
.fontColor('#4CAF50');
.width(‘100%’)
.padding(10)
.onClick(() => {
this.loadPlantFromHistory(item);
});
})
.width(‘100%’)
.height(400)
else {
Text('暂无识别历史')
.fontSize(16)
.fontColor('#666666')
.margin({ top: 50 });
}
.width('100%')
.height('100%')
private async identifyPlant(): Promise<void> {
const plantInfo = await this.plantService.captureAndIdentify();
this.identifiedPlant = plantInfo;
if (plantInfo) {
this.plantDetail = await this.plantService.getPlantDetail(plantInfo.id);
}
private async showPlantDetail(): Promise<void> {
if (this.identifiedPlant && !this.plantDetail) {
this.plantDetail = await this.plantService.getPlantDetail(this.identifiedPlant.id);
this.showDetail = true;
private async addToMyGarden(): Promise<void> {
if (this.identifiedPlant) {
// 实际应用中应该保存到用户的花园列表
console.log(Added ${this.identifiedPlant.name} to my garden);
}
private async loadPlantFromHistory(item: PlantInfo): Promise<void> {
this.identifiedPlant = item;
this.plantDetail = await this.plantService.getPlantDetail(item.id);
this.showHistory = false;
}
主界面实现
// src/main/ets/pages/PlantPage.ets
import { PlantService } from ‘…/service/PlantService’;
import { PlantIdentifier } from ‘…/components/PlantIdentifier’;
@Entry
@Component
struct PlantPage {
@State activeTab: number = 0;
@State deviceList: string[] = [];
private plantService = PlantService.getInstance();
build() {
Column() {
// 标题
Text(‘智能植物识别’)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 标签页
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
// 植物识别标签页
PlantIdentifier()
.tabBar(‘识别植物’);
TabContent() {
// 我的花园标签页
this.buildGardenTab()
.tabBar(‘我的花园’);
TabContent() {
// 设备管理标签页
this.buildDevicesTab()
.tabBar(‘设备管理’);
.barWidth(‘100%’)
.barHeight(50)
.width('100%')
.height('80%')
.width(‘100%’)
.height('100%')
.padding(20)
.onAppear(() => {
// 模拟获取设备列表
setTimeout(() => {
this.deviceList = ['我的手机', '平板电脑', '智能手表'];
}, 1000);
});
@Builder
private buildGardenTab() {
Column() {
Text(‘我的花园’)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 模拟花园植物
Grid() {
GridItem() {
this.buildPlantCard('玫瑰', 'Rosa rugosa');
GridItem() {
this.buildPlantCard('向日葵', 'Helianthus annuus');
GridItem() {
this.buildPlantCard('仙人掌', 'Cactaceae');
GridItem() {
this.buildPlantCard('绿萝', 'Epipremnum aureum');
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.columnsGap(15)
.rowsGap(15)
.width('100%')
.height('70%');
Button('添加新植物')
.type(ButtonType.Capsule)
.width('80%')
.margin({ top: 20 })
.backgroundColor('#4CAF50')
.fontColor('#FFFFFF');
.width(‘100%’)
.height('100%')
.padding(10);
@Builder
private buildPlantCard(name: string, scientificName: string) {
Column() {
Image($r(‘app.media.ic_plant’))
.width(80)
.height(80)
.margin({ bottom: 10 });
Text(name)
.fontSize(16)
.fontWeight(FontWeight.Bold);
Text(scientificName)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 5 });
.width(‘100%’)
.height('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.shadow({ radius: 5, color: '#E0E0E0', offsetX: 0, offsetY: 2 })
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center);
@Builder
private buildDevicesTab() {
Column() {
Text(‘已连接设备’)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
if (this.deviceList.length > 0) {
List({ space: 15 }) {
ForEach(this.deviceList, (device) => {
ListItem() {
Row() {
Image($r('app.media.ic_device'))
.width(40)
.height(40)
.margin({ right: 15 });
Text(device)
.fontSize(16)
.layoutWeight(1);
if (device === '我的手机') {
Text('主设备')
.fontSize(14)
.fontColor('#4CAF50');
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(10)
})
.width(‘100%’)
.layoutWeight(1);
else {
Text('没有连接的设备')
.fontSize(16)
.fontColor('#666666')
.margin({ top: 50 });
Button(‘添加设备’)
.type(ButtonType.Capsule)
.width('80%')
.margin({ top: 30 })
.backgroundColor('#2196F3')
.fontColor('#FFFFFF');
.width(‘100%’)
.height('100%')
.padding(10);
}
四、与游戏同步技术的结合点
分布式数据同步:借鉴游戏中多玩家状态同步机制,实现植物识别历史的跨设备同步
实时图像处理:类似游戏中的实时数据流,处理摄像头图像数据
设备角色分配:类似游戏中的主机/客户端角色,确定主识别设备和从属设备
冲突解决策略:使用时间戳优先策略解决多设备同时添加植物的冲突
数据压缩传输:优化植物识别结果的传输效率,类似游戏中的网络优化
五、关键特性实现
混合识别模式:
const localResult = await this.localIdentify(imageObj);
if (localResult.confidence < 0.7) {
const cloudResult = await this.cloudIdentify(imageObj);
if (cloudResult) return cloudResult;
return localResult;
植物详情获取:
const response = await this.httpRequest.request(
${this.API_URL}/details,
method: http.RequestMethod.GET,
header: { 'API-Key': this.API_KEY },
extraData: { plant_id: plantId }
);
历史记录同步:
this.kvStore.on('dataChange', (data) => {
data.insertEntries.forEach((entry) => {
if (entry.key === 'plant_history') {
this.notifyHistoryChange(entry.value.value as PlantInfo[]);
});
});
图像处理优化:
const buffer = await imageObj.getComponent(image.ImageComponent.JPEG);
return data:image/jpeg;base64,${buffer.toString(‘base64’)};
六、性能优化策略
本地缓存优先:
public async getPlantHistory(): Promise<PlantInfo[]> {
// 先返回本地缓存
const cachedHistory = this.plantHistory;
// 异步从分布式存储获取最新历史
if (this.kvStore) {
this.kvStore.get('plant_history').then((entry) => {
if (entry?.value) {
this.plantHistory = entry.value;
});
return cachedHistory;
批量历史更新:
private async addToHistory(plantInfo: PlantInfo): Promise<void> {
this.plantHistory.unshift(plantInfo);
if (this.plantHistory.length > 50) {
this.plantHistory = this.plantHistory.slice(0, 50);
await this.syncHistory();
资源释放管理:
public async destroy(): Promise<void> {
if (this.imageClassifier) {
this.imageClassifier.release();
if (this.cameraInput) {
await this.cameraInput.close();
}
网络请求节流:
private lastApiRequestTime = 0;
private readonly API_REQUEST_INTERVAL = 3000; // 3秒请求间隔
private async cloudIdentify(imageObj: image.Image): Promise<PlantInfo | null> {
const now = Date.now();
if (now - this.lastApiRequestTime < this.API_REQUEST_INTERVAL) {
return null;
this.lastApiRequestTime = now;
// API请求逻辑...
七、项目扩展方向
植物健康监测:通过图像分析植物健康状况
养护提醒:设置浇水、施肥等提醒
社区分享:分享植物照片和养护经验
AR植物展示:通过AR技术展示植物生长过程
植物百科:建立本地植物知识库
八、总结
本植物识别园艺助手实现了以下核心功能:
基于HarmonyOS的本地和云端混合植物识别
植物养护知识获取与展示
识别历史记录的分布式同步
直观的用户界面和交互体验
通过借鉴游戏中的多设备同步技术,我们构建了一个实用的园艺辅助工具。该项目展示了HarmonyOS在图像处理和分布式技术方面的强大能力,为开发者提供了生活服务类应用开发的参考方案。
