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

6 回复

【背景知识】

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


直接使用file.fd创建图像源即可

const file = await fs.open(uri, fs.OpenMode.READ_ONLY);

// 创建图像源
const imageSource = image.createImageSource(file.fd);
// 图片预处理

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);
  // ... 其余处理逻辑保持不变
}

这样就能正确处理相册选择的图片路径问题。

回到顶部