HarmonyOS鸿蒙Next中选择图片出现问题
HarmonyOS鸿蒙Next中选择图片出现问题
import { faceComparator } from '@kit.CoreVisionKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { promptAction, AlertDialog, } from '@kit.ArkUI';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import image from '@ohos.multimedia.image';
import fs from '@ohos.file.fs';
@Entry
@Component
struct FaceComparePage {
@State images: PixelMap[] = []
@State result: string = "请选择图片并点击比对"
@State groupResults: number[][] = []
@State isProcessing: boolean = false
private maxSelectCount: number = 3
// 错误码映射表
private ERROR_MAP: Record<number, string> = {
1001: "图片解析失败(请检查格式)",
2003: "人脸检测失败(无有效人脸)",
3002: "特征提取超时(请优化图片质量)"
};
build() {
Column() {
// 标题
Text("人脸相似度比对")
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
.margin({ top: 20, bottom: 30 })
// 图片展示区
Grid() {
ForEach(this.images, (img: PixelMap, index: number) => {
GridItem() {
Stack() {
Image(img)
.height(250)
.width('100%')
.objectFit(ImageFit.Cover)
.borderRadius(10)
.border({ width: 2, color: '#36D' })
Button('删除')
.size({ width: 60, height: 30 })
.fontSize(14)
.fontColor('#FFF')
.backgroundColor('#F44')
.position({ x: 10, y: 10 })
.onClick(() => this.removeImage(index))
}
}
}, (img: PixelMap, index: number) => `${index}`)
// 空位置占位符
ForEach(Array.from({ length: this.maxSelectCount - this.images.length }) as number[],
(item: number, i: number) => {
GridItem() {
Column() {
Image($r('app.media.add_image'))
.width(60)
.height(60)
.opacity(0.5)
Text('点击添加图片')
.fontSize(14)
.fontColor('#999')
.opacity(0.5)
.margin({ top: 10 })
}
.width('100%')
.height(250)
.borderRadius(10)
.border({ width: 2, style: BorderStyle.Dashed, color: '#999' })
.onClick(() => this.checkPermissionAndSelectImages())
}
}, (i: number) => `empty-${i}`)
}
.columnsTemplate(this.images.length > 1 ? '1fr 1fr' : '1fr')
.columnsGap(10)
.width('100%')
.margin({ bottom: 30 })
// 操作按钮区
Row({ space: 20 }) {
Button('选择图片')
.type(ButtonType.Normal)
.backgroundColor('#36D')
.fontColor('#FFF')
.width(150)
.onClick(() => this.checkPermissionAndSelectImages())
.enabled(!this.isProcessing)
Button('开始比对')
.type(ButtonType.Normal)
.backgroundColor('#07C160')
.fontColor('#FFF')
.width(150)
.onClick(() => this.compareFaces())
.enabled(this.images.length >= 2 && !this.isProcessing)
}
.margin({ bottom: 30 })
// 加载状态 - 修正ProgressType
if (this.isProcessing) {
Progress({ value: 50, type: ProgressType.Ring })
.width(40)
.height(40)
.color('#36D')
.margin({ bottom: 20 })
}
// 结果展示区
if (this.images.length === 2 && this.result) {
Column() {
Text('比对结果')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
.margin({ bottom: 10 })
Text(this.result)
.fontSize(16)
.fontColor('#666')
.textAlign(TextAlign.Center)
.padding(10)
.backgroundColor('#F5F5F5')
.borderRadius(8)
}
.width('100%')
}
// 多人比对矩阵结果
if (this.images.length > 2 && this.groupResults.length > 0) {
Column() {
Text('多人比对矩阵')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
.margin({ bottom: 10 })
Grid() {
ForEach(this.groupResults, (row: number[], i: number) => {
ForEach(row, (score: number, j: number) => {
GridItem() {
Text(`${score.toFixed(1)}%`)
.fontSize(14)
.fontColor(score > 70 ? '#FFF' : '#000')
.padding(8)
.backgroundColor(this.getScoreColor(score))
.borderRadius(4)
.textAlign(TextAlign.Center)
}
})
})
}
.columnsTemplate(this.images.map(() => '1fr').join(' '))
.columnsGap(5)
.width('100%')
Text('相似度等级说明:\n≥95%: 极高(双胞胎级)\n≥85%: 高(同一人)\n≥70%: 中(相似脸)\n<70%: 低(不同人)')
.fontSize(12)
.fontColor('#666')
.margin({ top: 10, left: 10 })
.textAlign(TextAlign.Start)
}
.width('100%')
}
}
.padding(20)
.width('100%')
.height('100%')
.backgroundColor('#F9F9F9')
}
// 检查权限并选择图片
private async checkPermissionAndSelectImages() {
try {
this.isProcessing = true;
const grantStatus = await this.reqPermissionsFromUser();
if (grantStatus.every(status => status === 0)) {
await this.showPrivacyDialog().catch(() => {
this.isProcessing = false;
promptAction.showToast({ message: '需要同意隐私声明才能使用' });
});
await this.selectImages();
} else {
promptAction.showToast({ message: '需要权限才能使用该功能' });
this.isProcessing = false;
}
} catch (err) {
promptAction.showToast({ message: '权限申请失败' });
this.isProcessing = false;
}
}
// 权限申请实现
async reqPermissionsFromUser(): Promise<number[]> {
console.warn('开始请求用户权限');
const context = getContext() as common.UIAbilityContext;
const atManager = abilityAccessCtrl.createAtManager();
let grantStatus = await atManager.requestPermissionsFromUser(context,
['ohos.permission.READ_MEDIA', 'ohos.permission.CAMERA', 'ohos.permission.WRITE_MEDIA']);
return grantStatus.authResults;
}
// 显示隐私声明对话框
private async showPrivacyDialog(): Promise<void> {
return new Promise((resolve, reject) => {
AlertDialog.arguments({
title: '隐私声明',
message: '需要访问您的相册以进行人脸比对,数据仅在本地处理,不会上传至云端',
confirm: {
value: '同意',
action: () => resolve()
},
cancel: () => {
reject();
},
autoCancel: false
});
});
}
// 选择图片
private async selectImages() {
try {
const selectOptions = new photoAccessHelper.PhotoSelectOptions();
selectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
selectOptions.maxSelectNumber = this.maxSelectCount - this.images.length;
const photoPicker = new photoAccessHelper.PhotoViewPicker();
const selectResult = await photoPicker.select(selectOptions);
if (selectResult.photoUris && selectResult.photoUris.length > 0) {
await this.loadImages(selectResult.photoUris);
}
} catch (err) {
console.error('选择图片失败:', err);
promptAction.showToast({ message: '选择图片失败' });
}
}
// 加载图片并预处理
private async loadImages(uris: string[]) {
try {
for (const uri of uris) {
const pixelMap = await this.preprocessImage(uri);
this.images.push(pixelMap);
if (this.images.length >= this.maxSelectCount) {
break;
}
}
} catch (err) {
console.error('加载图片失败:', err);
promptAction.showToast({ message: '加载图片失败' });
}
console.warn('选中的图片路径:', uris);
}
// 图片预处理
private async preprocessImage(uri: string): Promise<PixelMap> {
try {
// 读取文件
const file = await fs.open(uri, fs.OpenMode.READ_ONLY);
const stat = await fs.stat(uri);
console.warn('文件状态:', stat);
const buffer = new ArrayBuffer(stat.size);
await fs.read(file.fd, buffer);
await fs.close(file);
// 创建图像源
const imageSource = image.createImageSource(buffer);
const decodeOptions: image.DecodingOptions = {
desiredSize: { width: 640, height: 480 },
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
};
console.warn('图片解码成功');
// 解码获取PixelMap
return await imageSource.createPixelMap(decodeOptions);
} catch (err) {
console.error('图片预处理失败:', err);
throw new Error('图片处理失败,请尝试其他图片');
}
}
// 移除图片
private removeImage(index: number) {
this.images.splice(index, 1);
this.result = "请选择图片并点击比对";
this.groupResults = [];
}
// 人脸比对主逻辑
private async compareFaces() {
if (this.images.length < 2) {
promptAction.showToast({ message: '请至少选择两张图片' });
return;
}
this.isProcessing = true;
this.result = "";
this.groupResults = [];
try {
// 尺寸校验
if (!this.images.every(this.validateSize)) {
this.result = "请选择高清图片(≥480x640)";
return;
}
// 初始化比对器
await faceComparator.init();
// 根据图片数量选择比对方式
if (this.images.length === 2) {
await this.compareTwoFaces();
} else {
await this.compareGroupFaces();
}
} catch (error) {
const err = error as BusinessError;
this.result = `[${err.code}] ${this.ERROR_MAP[err.code] || err.message}`;
console.error('比对失败:', err);
} finally {
// 释放资源
await faceComparator.release().catch((err: Error) =>
console.warn('释放资源失败:', err)
);
this.isProcessing = false;
}
}
// 两张图片比对
private async compareTwoFaces() {
if (this.images.length < 2) {
return;
}
// 构建比对数据 - 修复2: 使用正确的类型定义
const vision1: faceComparator.VisionInfo = {
pixelMap: this.images[0]
};
const vision2: faceComparator.VisionInfo = {
pixelMap: this.images[1]
};
// 执行比对
const result: faceComparator.FaceCompareResult = await faceComparator.compareFaces(vision1, vision2);
// 处理结果
this.result = `相似度:${(result.similarity * 100).toFixed(1)}%\n` +
`${result.isSamePerson ? "√ 匹配成功" : "× 匹配失败"}\n` +
`等级:${this.getLevel(result.similarity)}`;
}
// 多人比对
private async compareGroupFaces() {
const results: number[][] = [];
for (let i = 0; i < this.images.length; i++) {
results[i] = [];
for (let j = 0; j < this.images.length; j++) {
if (i === j) {
results[i][j] = 100; // 自比对100%
} else {
const vision1: faceComparator.VisionInfo = { pixelMap: this.images[i] };
const vision2: faceComparator.VisionInfo = { pixelMap: this.images[j] };
const res = await faceComparator.compareFaces(vision1, vision2);
results[i][j] = res.similarity * 100;
}
}
}
this.groupResults = results;
}
// 验证图片尺寸
private async validateSize(img: image.PixelMap): Promise<boolean> {
// getImageInfo返回的是Promise,需要使用await获取结果
const imgInfo: image.ImageInfo = await img.getImageInfo();
return imgInfo.size.width >= 480 && imgInfo.size.height >= 640;
}
// 获取置信度等级
private getLevel(score: number): string {
if (score >= 0.95) {
return "极高(双胞胎级)";
}
if (score >= 0.85) {
return "高(同一人)";
}
if (score >= 0.7) {
return "中(相似脸)";
}
return "低(不同人)";
}
// 根据分数获取颜色
private getScoreColor(score: number): string {
if (score >= 95) {
return '#10B981';
} // 绿色
if (score >= 85) {
return '#3B82F6';
} // 蓝色
if (score >= 70) {
return '#F59E0B';
} // 黄色
return '#EF4444'; // 红色
}
}
fs.open只能打开/data/storage/el2/…里面的所以怎么才能把图片路径改正
更多关于HarmonyOS鸿蒙Next中选择图片出现问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html
【背景知识】
fs.stat:获取文件或目录详细属性信息,支持传入文件、目录应用沙箱路径或已打开的文件描述符fd。
【问题定位】
通过断点分析,在preprocessImage方法中,fs.stat(uri)传入的是媒体库内图片资源的uri(如:file://media/Photo),而fs.stat传入的参数需要为文件、目录的应用沙箱路径或已打开的文件描述符fd,所以出现了ErrorCode: 13900002, Message: No such file or directory报错。
【分析结论】
fs.stat传入的路径非应用沙箱路径,导致无法找到具体的文件路径,所以报错No such file or directory。
【修改建议】
修改fs.stat传入的参数为已打开的文件描述符fd,如:
// 读取文件
const file = await fs.open(uri, fs.OpenMode.READ_ONLY);
const stat = await fs.stat(file.fd);
更多关于HarmonyOS鸿蒙Next中选择图片出现问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
// 图片预处理
private async preprocessImage(uri: string): Promise<PixelMap> {
try {
// 1. 打开文件(保留原逻辑)
const file = await fs.open(uri, fs.OpenMode.READ_ONLY);
const stat = await fs.stat(uri);
console.warn('文件状态:', stat);
// 2. 关键修改:直接使用file.fd创建图像源(删除原buffer读取逻辑)
const imageSource = image.createImageSource(file.fd);
const decodeOptions: image.DecodingOptions = {
desiredSize: { width: 640, height: 480 },
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
};
console.warn('图片解码成功');
// 3. 解码获取PixelMap(保留原逻辑)
return await imageSource.createPixelMap(decodeOptions);
} catch (err) {
console.error('图片预处理失败:', err);
throw new Error('图片处理失败,请尝试其他图片');
} finally {
}
}
const stat = await fs.stat(uri);
改为 fs.stat(file.fd) 也传入fd,就是这里导致报错的,
在HarmonyOS Next中,选择图片问题可能涉及权限配置或API调用方式。需检查应用是否已申请ohos.permission.READ_IMAGEVIDEO权限,并在module.json5中正确定义。若使用PhotoViewPicker,确认其select()方法参数设置无误,包括选择数量与文件类型限制。同时验证系统图库应用是否正常可用。若问题持续,排查沙箱路径访问权限及文件URI解析逻辑。
在HarmonyOS Next中,fs.open() 只能访问应用沙箱路径 /data/storage/el2/,而相册选择返回的URI是媒体库路径。需要在 loadImages 方法中添加文件复制逻辑:
private async loadImages(uris: string[]) {
try {
for (const uri of uris) {
// 将媒体库文件复制到应用沙箱
const sandboxUri = await this.copyToSandbox(uri);
const pixelMap = await this.preprocessImage(sandboxUri);
this.images.push(pixelMap);
if (this.images.length >= this.maxSelectCount) {
break;
}
}
} catch (err) {
console.error('加载图片失败:', err);
promptAction.showToast({ message: '加载图片失败' });
}
}
private async copyToSandbox(mediaUri: string): Promise<string> {
const context = getContext() as common.UIAbilityContext;
const sandboxDir = context.filesDir;
const fileName = mediaUri.split('/').pop() || `image_${Date.now()}.jpg`;
const sandboxPath = `${sandboxDir}/${fileName}`;
// 使用photoAccessHelper获取文件并复制
const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
const asset = await phAccessHelper.openAsset(mediaUri);
const file = await asset.open('r');
// 复制文件到沙箱
await fs.copyFile(file.fd, sandboxPath);
await file.close();
return sandboxPath;
}
修改 preprocessImage 方法,直接使用沙箱路径:
private async preprocessImage(sandboxUri: string): Promise<PixelMap> {
const file = await fs.open(sandboxUri, fs.OpenMode.READ_ONLY);
// ... 其余处理逻辑保持不变
}
这样就能正确处理相册选择的图片路径问题。


