HarmonyOS AR Engine高精几何重建实战——扫描纸盒并测量体积

HarmonyOS AR Engine高精几何重建实战------扫描纸盒并测量体积

这次把一个真实小纸盒放进相机画面,手机扫过正面和侧面后,屏幕上出现了许多小点和一个青绿色立体框。小点表示系统找到的物体表面位置,立体框表示系统估算出的长方体外框。第一次锁定得到:

  • 长度:20.8 cm
  • 宽度:12.7 cm
  • 高度:3.5 cm
  • 体积:0.926 L
  • Cube 标签:CUBE_VOLUME
  • 置信度原始值:1.000

画面中的深色点、青绿色立体框、8 个白色角点和尺寸都来自同一轮真机扫描。页面没有提前放好一个盒子模型,也没有用随机点把效果补出来。

先看懂页面

第一次打开这个实验,不需要先理解三维数学。先看下面五个地方:

  • 黑色小方点:AR Engine 认为属于物体表面的空间点;"稠密"只是表示点比较多;
  • 青绿色立体框:系统返回的 Cube,也就是纸盒外面的长方体框,不是应用提前画好的固定模型;
  • 8 个白色角点:长方体的 8 个角,程序用它们计算三条边;
  • 长、宽、高和体积:直接由当前 Cube 的角点计算,不是手工填写;
  • 锁定按钮:把当前点、立体框和数字冻结,方便截图和核对。

判断是否真正成功,不能只看"检测到一个 Cube"。页面必须同时出现非零长宽高、体积和可见立体边,锁定按钮才会启用。

实验准备

本次使用的实际环境如下:

项目 实际环境
设备 HUAWEI Mate 60 Pro
系统 HarmonyOS 7.0
SDK API 26
工程 ArkTS Phone 应用,targetSdkVersion 26
必需权限 CAMERA、ACCELEROMETER、GYROSCOPE
必需能力 SLAM、SEMANTIC_DENSE
AR 会话类型 WORLD
高精重建模式 CUBE_VOLUME
扫描对象 六面基本平整、边缘清楚的小型纸盒

应用启动后先检查相机权限和八类 AR 能力。当前设备显示 8/8 支持,其中 SEMANTIC_DENSE 的说明为"高精几何重建"。

实验工作台新增了"实验 10:高精体积扫描仪"。这个入口只有在相机权限、SLAM 和 SEMANTIC_DENSE 同时满足时才会启动。

能力门禁代码如下:

ArkTS/ets 复制代码
private canStartSemanticDense(): boolean {
  const semanticDense: FeatureItem | undefined = this.features.find(
    (item: FeatureItem) =>
      item.type === arEngine.ARFeatureType
        .ARENGINE_FEATURE_TYPE_SEMANTIC_DENSE
  );
  return this.canStartAR() &&
    semanticDense !== undefined && semanticDense.supported;
}

private startSemanticDense(): void {
  if (!this.cameraGranted) {
    this.latestMessage =
      '拦截成功:未获得相机权限,不启动高精几何重建。';
    hilog.info(DOMAIN, TAG, 'DENSE_GUARD camera=false');
    return;
  }
  if (!this.canStartAR()) {
    this.latestMessage =
      '拦截成功:当前设备不支持 SLAM,不启动高精几何重建。';
    hilog.info(DOMAIN, TAG, 'DENSE_GUARD slam=false');
    return;
  }
  if (!this.canStartSemanticDense()) {
    this.latestMessage =
      '拦截成功:当前设备不支持高精几何重建,不创建 AR 会话。';
    hilog.info(DOMAIN, TAG,
      'DENSE_GUARD semanticDense=false');
    return;
  }

  hilog.info(DOMAIN, TAG,
    'DENSE_GUARD camera=true slam=true semanticDense=true');
  this.showSemanticDense = true;
}

系统怎样把纸盒变成体积

这张图只说明数据处理关系,不是真机结果。真机识别是否成功,仍以正文中的实际相机画面和日志为准。

相机每更新一次画面,就会产生一份 ARFrame。可以把它理解为"这一刻的相机画面和 AR 数据"。页面依次做五件事:

  1. 从当前画面取得高精几何重建数据;
  2. 读出物体表面小点的位置和颜色;
  3. 读出长方体的 8 个角点;
  4. 用角点计算三条相邻边,再把小点和立体框画回屏幕;
  5. 长、宽、高相乘得到立方米,再换算成更直观的升。

第一步:让 AR Engine 寻找长方体

页面底层用 ARView 显示真实相机,上层用 Canvas 画小点和立体框。这样移动手机时,标记会跟着真实纸盒的位置变化。

下面这段配置的关键只有一项:semanticDenseMode 选择 CUBE_VOLUME,告诉 AR Engine 本次要寻找可以计算体积的长方体。自动对焦保持开启,普通语义、深度和 Mesh 关闭。

ArkTS/ets 复制代码
private async initARView(): Promise<void> {
  const startTime: number = Date.now();
  const scene: Scene = await Scene.load();
  const context = new arViewController.ARViewContext();

  this.callback = new SemanticDenseCallback(
    (viewContext: arViewController.ARViewContext,
      timestamp: number) => {
      this.handleFrameUpdate(viewContext, timestamp);
    }
  );

  context.scene = scene;
  context.callback = this.callback;
  context.config = {
    type: arEngine.ARType.WORLD,
    planeFindingMode:
      arEngine.ARPlaneFindingMode.HORIZONTAL_AND_VERTICAL,
    powerMode: arEngine.ARPowerMode.NORMAL,
    semanticMode: arEngine.ARSemanticMode.NONE,
    semanticDenseMode:
      arEngine.ARSemanticDenseMode.CUBE_VOLUME,
    poseMode: arEngine.ARPoseMode.GRAVITY,
    depthMode: arEngine.ARDepthMode.DISABLED,
    meshMode: arEngine.ARMeshMode.DISABLED,
    focusMode: arEngine.ARFocusMode.AUTO
  };

  await context.init();
  this.arContext = context;
  this.sessionReadyTime = Date.now();

  hilog.info(DOMAIN, TAG,
    'DENSE_SESSION_INIT success costMs=%{public}d ' +
    'mode=2 focusMode=1 depthMode=0 meshMode=0',
    Date.now() - startTime);
}

最终真机初始化耗时 36 ms:

text 复制代码
DENSE_GUARD camera=true slam=true semanticDense=true
DENSE_SESSION_INIT success costMs=36 mode=2 focusMode=1 depthMode=0 meshMode=0

第二步:每隔 250 ms 读取一次

相机每秒会产生很多帧。如果每帧都读取并绘制几千个点,页面容易卡顿。下面的判断把读取频率限制为每 250 ms 最多一次;页面暂停、结果锁定、页面退出或上一轮还没结束时,都不重复处理。

ArkTS/ets 复制代码
private handleFrameUpdate(
  context: arViewController.ARViewContext,
  timestamp: number
): void {
  const now: number = Date.now();
  if (this.isDestroyed || this.isPaused ||
    this.evidenceLocked || this.samplingInFlight ||
    now - this.lastSampleTime < SAMPLE_INTERVAL_MS) {
    return;
  }

  this.lastSampleTime = now;
  this.samplingInFlight = true;
  this.sampleSemanticDense(context).finally(() => {
    this.samplingInFlight = false;
  });
}

每轮先确认相机正在稳定跟踪,再读取高精重建数据。没有稳定跟踪时直接等待,不显示旧数据冒充新结果:

ArkTS/ets 复制代码
frame = session.getFrame();
const camera: arEngine.ARCamera = frame.getCamera();
this.updateTrackingStatus(camera.state, camera.stateReason);

if (camera.state !== arEngine.ARTrackingState.TRACKING) {
  return;
}

denseData = frame.acquireSemanticDense();
this.sampleCount += 1;
this.pointCount = denseData.pointDataSize;
this.cubeCount = denseData.cubeDataSize;

刚进入页面时只有真实相机画面和青色扫描框,尺寸、体积和点数都是空值。

第三步:读出物体表面的空间点

每个小点至少需要知道三件事:它是谁、它在三维空间的哪个位置、它是什么颜色。接口把这三组内容放在三个 ArrayBuffer 中,ArkTS 需要先把它们转换成可以按下标读取的数组:

  • id 转为 Int32Array
  • position 转为 Float32Array,每个点占 4 个值:三维位置 x、y、z 和可信程度;
  • color 转为 Int32Array,每个点占 4 个值:r、g、b、a
ArkTS/ets 复制代码
let positions: Float32Array = new Float32Array(0);
let colors: Int32Array = new Int32Array(0);

if (denseData.pointDataSize > 0) {
  const pointData: arEngine.ARSemanticDensePointData =
    denseData.acquirePointData();

  const pointIds: Int32Array = new Int32Array(pointData.id);
  positions = new Float32Array(pointData.position);
  colors = new Int32Array(pointData.color);

  this.pointCount = Math.min(
    denseData.pointDataSize,
    pointIds.length,
    Math.floor(positions.length / 4),
    Math.floor(colors.length / 4)
  );
}

本轮首批点数据在会话建立后 18.242 秒出现,共 24 个点。位置数组和颜色数组分别有 96 个值,正好都是 24 × 4,说明数据布局能够对上。

text 复制代码
DENSE_FIRST_POINT latencyMs=18242 points=24 idValues=24 positionValues=96 colorValues=96

继续绕纸盒移动后,点数增长到 553。Canvas 最多画 700 个点;如果系统返回更多,只隔几个取一个来画,页面上的原始点数仍然照常显示。

ArkTS/ets 复制代码
private drawPointCloud(
  positions: Float32Array,
  colors: Int32Array,
  viewMatrix: Array<number>,
  projectionMatrix: Array<number>,
  canvasWidth: number,
  canvasHeight: number
): number {
  const sourcePoints: number = Math.min(
    this.pointCount,
    Math.floor(positions.length / 4)
  );
  const stride: number = Math.max(
    1,
    Math.ceil(sourcePoints / MAX_DRAWN_POINTS)
  );

  let drawn: number = 0;
  for (let pointIndex: number = 0;
    pointIndex < sourcePoints;
    pointIndex += stride) {
    const offset: number = pointIndex * 4;
    const projected: number[] = this.projectWorldPoint(
      positions[offset], positions[offset + 1],
      positions[offset + 2], viewMatrix, projectionMatrix,
      canvasWidth, canvasHeight
    );
    if (projected.length !== 2) {
      continue;
    }

    const confidence: number = positions[offset + 3];
    const alpha: number = Math.max(
      0.34,
      Math.min(0.92, 0.42 + confidence * 0.45)
    );
    this.overlayContext.fillStyle =
      `rgba(${colors[offset]}, ${colors[offset + 1]}, ` +
      `${colors[offset + 2]}, ${alpha.toFixed(2)})`;
    this.overlayContext.fillRect(
      projected[0] - 1.35,
      projected[1] - 1.35,
      2.7, 2.7
    );
    drawn += 1;
  }
  return drawn;
}

遇到的情况:已经有 Cube,尺寸还是 0

这次扫描中出现了一个很容易误判的中间状态:日志已经显示系统返回了 1 个 Cube,但页面尺寸仍然是 0。

第 62 次采样已经返回:

text 复制代码
DENSE_SAMPLE sample=62 points=24 drawnPoints=0 cubes=1 cubeId=0 vertexSize=8 drawnEdges=0 label=10 confidence=1.000 lengthM=0.000 widthM=0.000 heightM=0.000 volumeL=0.000

这里的 cubes=1 只表示"系统已经创建了一条外框记录",不代表 8 个角点已经张开成一个可测量的盒子。此时三条相邻边仍是 0,所以页面继续等待。直到 37.692 秒后,三条边都变成有效非零值,才记录首个可用 Cube:

text 复制代码
DENSE_FIRST_CUBE latencyMs=37692 id=0 vertexSize=8 label=10 confidence=1.000

因此,页面把成功条件收紧为下面四项:

  • vertexSize === 8
  • vertexData 至少包含 24 个数;
  • 24 个值都是有限数;
  • 三条相邻边都大于 1 mm,且没有异常大于 10 m。
ArkTS/ets 复制代码
private firstValidCube(
  cubes: Array<arEngine.ARSemanticDenseCubeData>
): arEngine.ARSemanticDenseCubeData | undefined {
  for (let index: number = 0; index < cubes.length;
    index += 1) {
    const cube: arEngine.ARSemanticDenseCubeData = cubes[index];
    if (cube.vertexSize === 8 &&
      cube.vertexData.length >= 24 &&
      this.hasFiniteVertices(cube.vertexData)) {
      return cube;
    }
  }
  return undefined;
}

第四步:用 8 个角点算三条边

一个长方体有 8 个角。按照接口返回的角点顺序,v0-v1v1-v2v0-v4 正好是从相邻角点连出的三条边。程序分别算出三条线的长度,再把最长的显示为"长"、中间的显示为"宽"、最短的显示为"高"。

ArkTS/ets 复制代码
private measureCube(
  vertices: Array<number>,
  vertexSize: number
): CubeMeasurement {
  const invalid: CubeMeasurement = {
    valid: false,
    vertices: [],
    lengthMeters: 0,
    widthMeters: 0,
    heightMeters: 0,
    volumeLiters: 0
  };
  if (vertexSize !== 8 || vertices.length < 24 ||
    !this.hasFiniteVertices(vertices)) {
    return invalid;
  }

  const edgeA: number = this.vertexDistance(vertices, 0, 1);
  const edgeB: number = this.vertexDistance(vertices, 1, 2);
  const edgeC: number = this.vertexDistance(vertices, 0, 4);

  if (edgeA <= 0.001 || edgeB <= 0.001 ||
    edgeC <= 0.001 || edgeA > 10 ||
    edgeB > 10 || edgeC > 10) {
    return invalid;
  }

  const dimensions: number[] = [edgeA, edgeB, edgeC]
    .sort((first: number, second: number) => second - first);

  return {
    valid: true,
    vertices: vertices.slice(0, 24),
    lengthMeters: dimensions[0],
    widthMeters: dimensions[1],
    heightMeters: dimensions[2],
    volumeLiters: edgeA * edgeB * edgeC * 1000
  };
}

private vertexDistance(
  vertices: Array<number>,
  firstIndex: number,
  secondIndex: number
): number {
  const firstOffset: number = firstIndex * 3;
  const secondOffset: number = secondIndex * 3;
  const deltaX: number =
    vertices[firstOffset] - vertices[secondOffset];
  const deltaY: number =
    vertices[firstOffset + 1] - vertices[secondOffset + 1];
  const deltaZ: number =
    vertices[firstOffset + 2] - vertices[secondOffset + 2];
  return Math.sqrt(
    deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ
  );
}

接口中的坐标单位是米,所以三边相乘后得到立方米。为了让小纸盒的结果更好读,再乘以 1000 换成升:

text 复制代码
0.208 m × 0.127 m × 0.035 m = 0.000926 m³
0.000926 m³ × 1000 = 0.926 L

第五步:把空间点画回屏幕

接口给出的点是三维空间坐标,而 Canvas 需要的是屏幕上的横纵像素,二者不能直接混用。页面使用相机的观察矩阵和投影矩阵,把"空间中的点"换算成"屏幕上的位置"。第一次阅读时不必推导矩阵,只要记住这一步负责让立体框跟着真实纸盒移动。

ArkTS/ets 复制代码
private projectWorldPoint(
  worldX: number,
  worldY: number,
  worldZ: number,
  viewMatrix: Array<number>,
  projectionMatrix: Array<number>,
  canvasWidth: number,
  canvasHeight: number
): number[] {
  const viewX: number = viewMatrix[0] * worldX +
    viewMatrix[4] * worldY + viewMatrix[8] * worldZ +
    viewMatrix[12];
  const viewY: number = viewMatrix[1] * worldX +
    viewMatrix[5] * worldY + viewMatrix[9] * worldZ +
    viewMatrix[13];
  const viewZ: number = viewMatrix[2] * worldX +
    viewMatrix[6] * worldY + viewMatrix[10] * worldZ +
    viewMatrix[14];
  const viewW: number = viewMatrix[3] * worldX +
    viewMatrix[7] * worldY + viewMatrix[11] * worldZ +
    viewMatrix[15];

  const clipX: number = projectionMatrix[0] * viewX +
    projectionMatrix[4] * viewY +
    projectionMatrix[8] * viewZ +
    projectionMatrix[12] * viewW;
  const clipY: number = projectionMatrix[1] * viewX +
    projectionMatrix[5] * viewY +
    projectionMatrix[9] * viewZ +
    projectionMatrix[13] * viewW;
  const clipW: number = projectionMatrix[3] * viewX +
    projectionMatrix[7] * viewY +
    projectionMatrix[11] * viewZ +
    projectionMatrix[15] * viewW;

  if (!Number.isFinite(clipW) || clipW <= 0.001) {
    return [];
  }

  const normalizedX: number = clipX / clipW;
  const normalizedY: number = clipY / clipW;
  return [
    (normalizedX * 0.5 + 0.5) * canvasWidth,
    (0.5 - normalizedY * 0.5) * canvasHeight
  ];
}

换算完成后,8 个角点可以连成 6 个半透明面和 12 条边。只有立体边确实进入当前画面后,页面才允许锁定;最终锁定时 12 条边全部可见。

跟着真机扫一遍

1. 让纸盒完整入镜

把纸盒放在木纹桌面、地板等有纹理的地方。进入实验 10 后,让纸盒正面、顶部和至少一个侧面同时进入青色扫描框。移动的是手机,不是在屏幕上滑动。

页面开始时会显示"等待纸箱体积"。本轮前 56 次采样仍然是 points=0 / cubes=0,页面继续等待,没有拿默认数字填空。

2. 缓慢经过正面和两个侧面

拿着手机从纸盒正面缓慢移动到左侧或右侧,尽量让纸盒始终完整入镜。18.242 秒后出现首批 24 个小点;继续看到更多侧面后,页面开始取得 Cube,但尺寸还没有立刻可用。

37.692 秒时三条边第一次都有非零长度,页面开始显示青绿色立体框。之后点数从 21 增长到 375、426,最后达到 553。看到立体框不再贴错位置、长宽高不为 0,才进入下一步。

3. 等立体框覆盖纸盒后锁定

移动视角时,可见 Cube 边数曾从 12 变为 5、4、9,再回到 12。这不是 Cube 数据消失,而是部分顶点暂时落到当前相机视野外。

当纸盒完整入镜、小点覆盖盒子边缘、立体框能跟住纸盒时,点击"锁定当前体积证据"。按钮变成黄色"继续扫描纸箱",说明当前结果已经冻结。第 115 次采样日志为:

text 复制代码
DENSE_EVIDENCE_LOCK sample=115 points=553 cubeId=0 vertexSize=8 label=10 confidence=1.000 lengthM=0.208 widthM=0.127 heightM=0.035 volumeL=0.926 drawnEdges=12

这行日志翻译成页面数据就是:553 个点、8 个角点、12 条可见边,长宽高为 20.8、12.7、3.5 cm,体积为 0.926 L。页面为了简洁把体积显示成一位小数 0.9 L

4. 锁定后继续扫描

为了验证锁定后能否重新工作,又对同一只纸盒做了一轮扫描。第二轮在第 473 次采样锁定了 6627 个点,尺寸为 23.2 × 15.0 × 5.0 cm,体积为 1.741 L

text 复制代码
DENSE_EVIDENCE_LOCK sample=473 points=6627 cubeId=0 vertexSize=8 label=10 confidence=1.000 lengthM=0.232 widthM=0.150 heightM=0.050 volumeL=1.741 drawnEdges=12

点击黄色"继续扫描纸箱"后,按钮恢复为"锁定当前体积证据",状态从"证据已锁定"变成"已识别"。采样数从 473 继续增长到 480、488,证明页面确实重新读取了 AR 帧。

text 复制代码
DENSE_EVIDENCE_CONTINUE sample=473
DENSE_SAMPLE sample=480 points=6627 drawnPoints=663 cubes=1 vertexSize=8 drawnEdges=12 lengthM=0.232 widthM=0.150 heightM=0.050 volumeL=1.741
DENSE_SAMPLE sample=488 points=6627 drawnPoints=663 cubes=1 vertexSize=8 drawnEdges=12 lengthM=0.232 widthM=0.150 heightM=0.050 volumeL=1.741

两轮对同一纸盒的结果并不完全相同:第一轮为 20.8 × 12.7 × 3.5 cm / 0.926 L,第二轮为 23.2 × 15.0 × 5.0 cm / 1.741 L。第二轮看到了更完整的顶部和侧面,点数也从 553 增长到 6627。这说明取景范围和扫描完整度会影响结果。本文只验证接口、Cube、页面交互和体积计算链路可以运行;没有用尺子做标定,所以不能把这些数字当成计量仪器的精度证明。

暂停、恢复和销毁

锁定后点击"暂停重建",页面停在第 115 次采样,按钮变成"恢复重建"。等待期间没有新的采样数。

text 复制代码
DENSE_SESSION_PAUSE sample=115

点击"恢复重建"后,AR 会话恢复。因为当前证据仍处于锁定状态,页面继续保留第 115 帧结果,不会偷偷覆盖已锁定尺寸。

text 复制代码
DENSE_SESSION_RESUME sample=115 locked=true

最后点击"销毁退出"。页面返回实验工作台,销毁汇总显示 pending=0 / errors=0,AR 相机页面关闭。

text 复制代码
DENSE_SESSION_DESTROY sample=115 pending=0 errors=0 points=553 cubes=1 locked=true

退出页面时释放相机数据

每次读取都会占用一份高精数据和一帧相机数据。下面的 finally 无论成功还是报错都会执行,先释放高精数据,再释放相机帧,避免连续扫描后资源越积越多。

ArkTS/ets 复制代码
} finally {
  if (denseData) {
    try {
      await denseData.release();
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      hilog.error(DOMAIN, TAG,
        'DENSE_DATA_RELEASE error=%{public}d', err.code);
    }
  }
  if (frame) {
    try {
      await frame.release();
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      hilog.error(DOMAIN, TAG,
        'DENSE_FRAME_RELEASE error=%{public}d', err.code);
    }
  }
}

页面退出时销毁 ARViewContext 并清空 Canvas:

ArkTS/ets 复制代码
private async destroyARView(): Promise<void> {
  if (!this.arContext || this.isDestroyed) {
    return;
  }
  this.isDestroyed = true;

  const context: arViewController.ARViewContext = this.arContext;
  this.arContext = undefined;
  await context.destroy();

  if (this.canvasReady) {
    this.overlayContext.clearRect(
      0, 0,
      this.overlayContext.width,
      this.overlayContext.height
    );
  }
}

构建并确认数据来自真机

静态回归覆盖了以下内容:

  • SEMANTIC_DENSE 能力门禁;
  • CUBE_VOLUME 会话配置;
  • 点 ID、位置和颜色的数组转换;
  • Cube 8 顶点及 24 个坐标值检查;
  • 三边距离和升数计算;
  • View/Projection 相机投影;
  • 点云、6 个面、12 条边和 8 个角点绘制;
  • 锁定、继续扫描、暂停、恢复和销毁;
  • ARSemanticDenseDataARFrame 释放;
  • 随机或模拟点云实现为 0 命中。

执行结果:

text 复制代码
AR Engine 高精几何重建静态回归检查通过。

随后执行 clean 签名构建:

powershell 复制代码
hvigorw clean assembleHap --no-daemon

最终结果:

text 复制代码
CompileArkTS SUCCESS
PackageHap  SUCCESS
SignHap     SUCCESS
BUILD SUCCESSFUL in 26 s 223 ms

签名 HAP 大小为 2,923,106 bytes,覆盖安装和 Ability 启动均成功。

最后得到的结果

验证项 真机结果
CAMERA、SLAM、SEMANTIC_DENSE 门禁 通过
会话初始化 36 ms
首批稠密点 18.242 s,24 个点
首个有效 Cube 37.692 s
最终稠密点 553
Cube 1 个,vertexSize=8
标签 label=10CUBE_VOLUME
置信度原始值 1.000
可见立体边 12/12
长 × 宽 × 高 20.8 × 12.7 × 3.5 cm
体积 0.926 L
第二轮锁定 6627 点,23.2 × 15.0 × 5.0 cm,1.741 L
继续扫描 第 473 次解除锁定,采样继续到 480、488
暂停 第 115 次采样保持不变
恢复 会话恢复,锁定证据保持
销毁 pending=0 / errors=0,返回工作台

本次案例完成了从真实 AR 帧到高精重建结果的完整链路:真机生成稠密点和 8 顶点 Cube,ArkTS 校验顶点并计算三条空间边,Canvas 把世界坐标重新投影到相机画面,最后锁定实际尺寸和体积,并完成继续扫描、暂停、恢复、销毁与逐帧资源释放。两轮扫描的尺寸差异也作为真实结果保留,没有用生成数据把它们改成一致。

官方资料

相关推荐
用户09340777351420 分钟前
HarmonyOS WPS Open SDK 实践:registerApp 鉴权与就绪门禁
android·typescript·harmonyos
特立独行的猫a1 小时前
仓颉语言原生 Coding Agent:cjh · 仓颉语言实现的 Harness
ai·agent·harmonyos·仓颉·cangjie·harness
大锅盖11 小时前
警示橙如何驱动巡检闭环?ArkUI 物业安全平台的声明式实现
安全·华为·harmonyos
贾伟康1 小时前
【时光清单|03】HarmonyOS ArkTS 提醒服务实战:计算触发时间并防止重复注册
harmonyos·arkts·后台任务·幂等设计·通知提醒
李蚊子1 小时前
从代理提醒到真实响铃:懒熊闹钟的鸿蒙开发实践
前端·harmonyos
GKxx1 小时前
在 HarmonyOS 上从源码构建 GCC 16(gcc/g++ + libstdc++ + libsanitizer):完整记录
c++·华为·harmonyos·鸿蒙·gcc
贾伟康1 小时前
【时光清单|04】HarmonyOS ArkTS 通知服务实战:管理权限、渠道和点击跳转
harmonyos·arkts·系统通知·notificationkit·wantagent
黑臂麒麟2 小时前
HarmonyOS随手账本App——从项目搭建到数据模型设计
华为·app·鸿蒙
黑臂麒麟2 小时前
Harmony鸿蒙实战应用4:随手账本——编辑与删除账单
ubuntu·华为·鸿蒙