mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

实战指南:利用CameraX与AR Core构建高精度AR测量应用

在移动开发领域,将增强现实(AR)与设备摄像头能力结合,创造出实用的工具应用,正成为开发者探索的热点。其中,AR测量应用凭借其“所见即所得”的直观体验,从家装测量到工业巡检,展现出巨大的潜力。本文将深入探讨如何协同使用Google的CameraX与AR Core框架,从原理剖析到代码实践,一步步构建一个稳定、精准的AR测量工具。无论你是使用Kotlin/Java进行Android开发,还是对TypeScriptPython等语言在跨平台或后端处理中感兴趣,其中的设计思想与算法逻辑都具有普适的参考价值。

一、AR测量:技术原理与核心挑战

AR测量并非简单地在摄像头画面上叠加标尺。其本质是一个将2D屏幕感知转化为3D世界理解的过程。应用需要准确知道用户在屏幕上点击的点,对应在真实三维空间中的具体位置。

这背后依赖一套完整的技术栈:

  • 环境感知(AR Core):通过SLAM(同步定位与地图构建)技术,理解设备的位姿和周围环境的几何结构(如检测到的平面)。
  • 图像采集(CameraX):提供稳定、高效的相机数据流,并处理生命周期、预览分析等繁琐任务。
  • 坐标转换算法:连接屏幕与3D世界的桥梁,这是精度保障的关键。

其核心架构可以概括为以下流程,它清晰地展示了数据从采集到最终呈现的路径:

┌─────────────────────────────────────┐
│         用户界面与交互层              │
│  测量标注、手势识别、结果展示           │
├─────────────────────────────────────┤
│         AR引擎层 (AR Core)           │
│  运动跟踪│环境理解│光照估计│点云生成   │
├─────────────────────────────────────┤
│     相机控制层 (CameraX)             │
│  图像采集│实时预览│图像分析│自动对焦   │
├─────────────────────────────────────┤
│      传感器融合层                     │
│  陀螺仪│加速度计│磁力计│深度传感器    │
├─────────────────────────────────────┤
│        计算机视觉算法层               │
│  特征点检测│平面检测│距离计算│3D重建  │
└─────────────────────────────────────┘

与传统测量工具相比,AR测量具有独特优势,但也面临挑战:

测量方式传统卷尺激光测距仪AR测量
精度±1-2mm±1-2mm±2-5cm
测量范围0-10m0-100m0-10m
操作难度中等简单非常简单
功能扩展单一单一长度、面积、体积、角度
环境要求需要反射面需要纹理丰富的平面
成本中等只需手机

开发者面临的主要挑战包括:如何实现高精度的屏幕到世界坐标转换?如何在不同光照和纹理环境下保持稳定?以及如何优化性能以保证流畅体验。解决方案通常结合了光线投射(Ray Casting)、多帧数据融合滤波以及传感器数据补偿。

二、项目基石:环境搭建与双框架初始化

万事开头难,一个正确的项目配置是成功的第一步。首先,我们需要在build.gradle文件中声明必要的依赖。CameraX简化了相机操作,而AR Core提供了环境理解能力。

// app/build.gradle
android {
    compileSdk 34
    defaultConfig {
        applicationId "com.example.armeasure"
        minSdk 24  // AR Core最低要求
        targetSdk 34
        versionCode 1
        versionName "1.0"
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
dependencies {
    // AR Core核心库
    implementation "com.google.ar:core:1.40.0"
    // Sceneform UX(AR场景管理)
    implementation "com.google.ar.sceneform.ux:sceneform-ux:1.40.0"
    // CameraX核心库
    def camerax_version = "1.3.0"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    implementation "androidx.camera:camera-view:${camerax_version}"
    // 视图相关
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.10.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    // 数学计算(向量、矩阵运算)
    implementation 'org.apache.commons:commons-math3:3.6.1'
    // 单元测试
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

接着,必须在AndroidManifest.xml中声明应用所需的权限和特性。AR功能需要相机权限,并且应声明应用使用AR Core,以便Google Play商店进行设备兼容性过滤。

<!-- AndroidManifest.xml -->
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.armeasure">
  <!-- AR Core必需权限 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <!-- 可选权限:提高AR体验 -->
      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
      <!-- AR Core必需特性声明 -->
        <uses-feature
          android:name="android.hardware.camera.ar"
          android:required="true" />
        <uses-feature
          android:name="android.hardware.camera"
          android:required="true" />
        <uses-feature
          android:name="android.hardware.camera.autofocus"
          android:required="false" />  <!-- 可选,但推荐 -->
          <!-- 应用配置 -->
            <application
              android:allowBackup="true"
              android:icon="@mipmap/ic_launcher"
              android:label="@string/app_name"
              android:theme="@style/Theme.ARMeasure">
            <!-- AR Core检查Activity -->
              <activity
                android:name=".CheckArActivity"
                android:exported="true"
                android:theme="@style/Theme.ARMeasure.Fullscreen">
              <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
              </intent-filter>
            </activity>
            <!-- 主测量Activity -->
              <activity
                android:name=".MeasureActivity"
                android:configChanges="orientation|screenSize|keyboardHidden"
                android:exported="false"
                android:screenOrientation="portrait"
                android:theme="@style/Theme.ARMeasure.Fullscreen" />
              <!-- AR Core配置 -->
                <meta-data
                  android:name="com.google.ar.core"
                  android:value="required" />
                <!-- AR Core最小版本 -->
                  <meta-data
                    android:name="com.google.ar.core.min_apk_version"
                    android:value="1.40.0" />
                  <!-- 支持深度传感器(如果设备有) -->
                    <meta-data
                      android:name="com.google.ar.core.depth"
                      android:value="optional" />
                  </application>
                </manifest>

完成基础配置后,我们就可以进入核心的协同工作环节。想象一下,这就像组建一个乐队:CameraX是稳健的节奏部,负责提供清晰稳定的图像流;AR Core则是灵动的旋律部,负责理解和诠释环境。两者必须完美同步。

[AFFILIATE_SLOT_1]

三、双核驱动:CameraX与AR Core的协同实战

初始化CameraX相对直接。我们配置一个用于预览的PreviewUseCase和一个用于AR Core分析的ImageAnalysisUseCase。后者将每一帧图像及时传递给AR Core会话进行处理。

class ARCameraManager(
private val context: Context,
private val surfaceProvider: Preview.SurfaceProvider
) {
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var preview: Preview
private var camera: Camera? = null
// 相机配置
data class CameraConfig(
val targetResolution: Size = Size(1920, 1080),
val focusMode: Int = CameraSelector.LENS_FACING_BACK,
val enableAutoFocus: Boolean = true,
val frameRate: IntRange = 30..30
)
/**
* 初始化CameraX相机
*/
fun initializeCamera(config: CameraConfig = CameraConfig()): ListenableFuture<Camera> {
  val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
  cameraProviderFuture.addListener({
  try {
  cameraProvider = cameraProviderFuture.get()
  // 配置预览用例
  preview = Preview.Builder()
  .setTargetResolution(config.targetResolution)
  .setTargetAspectRatio(AspectRatio.RATIO_16_9)
  .build()
  .also {
  it.setSurfaceProvider(surfaceProvider)
  }
  // 选择摄像头(后置)
  val cameraSelector = CameraSelector.Builder()
  .requireLensFacing(config.focusMode)
  .build()
  // 绑定到生命周期
  camera = cameraProvider.bindToLifecycle(
  context as LifecycleOwner,
  cameraSelector,
  preview
  )
  // 配置自动对焦
  if (config.enableAutoFocus) {
  setupAutoFocus()
  }
  } catch (e: Exception) {
  Log.e("ARCameraManager", "相机初始化失败", e)
  }
  }, ContextCompat.getMainExecutor(context))
  return cameraProviderFuture
  }
  /**
  * 设置连续自动对焦(AR测量需要稳定对焦)
  */
  private fun setupAutoFocus() {
  camera?.cameraControl?.setLinearFocus(0f)  // 0表示自动对焦
  // 监听对焦状态
  camera?.cameraInfo?.focusState?.observe(context as LifecycleOwner) { focusState ->
  when (focusState?.state) {
  FocusState.STATE_FOCUSED -> {
  Log.d("ARCameraManager", "对焦成功")
  }
  FocusState.STATE_NOT_FOCUSED -> {
  Log.d("ARCameraManager", "未对焦")
  }
  else -> {
  // 对焦中或其他状态
  }
  }
  }
  }
  /**
  * 获取相机内参(用于AR Core坐标转换)
  */
  fun getCameraIntrinsics(): CameraIntrinsics? {
  return camera?.cameraInfo?.cameraCharacteristics?.let { characteristics ->
  val focalLength = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)?.firstOrNull()
  val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
  val pixelArraySize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)
  if (focalLength != null && sensorSize != null && pixelArraySize != null) {
  CameraIntrinsics(
  focalLength = focalLength,
  sensorWidth = sensorSize.width,
  sensorHeight = sensorSize.height,
  imageWidth = pixelArraySize.width,
  imageHeight = pixelArraySize.height
  )
  } else {
  null
  }
  }
  }
  data class CameraIntrinsics(
  val focalLength: Float,      // 焦距(毫米)
  val sensorWidth: Float,      // 传感器宽度(毫米)
  val sensorHeight: Float,     // 传感器高度(毫米)
  val imageWidth: Int,         // 图像宽度(像素)
  val imageHeight: Int         // 图像高度(像素)
  ) {
  // 计算焦距像素值
  fun focalLengthPixels(): Pair<Float, Float> {
    val fx = (focalLength * imageWidth) / sensorWidth
    val fy = (focalLength * imageHeight) / sensorHeight
    return Pair(fx, fy)
    }
    // 计算主点坐标(通常为图像中心)
    fun principalPoint(): Pair<Float, Float> {
      val cx = imageWidth / 2f
      val cy = imageHeight / 2f
      return Pair(cx, cy)
      }
      }
      }

AR Core会话的管理是AR应用的中枢。我们需要在Activity的生命周期中妥善管理会话的创建、暂停和销毁,并配置其运行模式(例如只进行平面检测)。

class ARSessionManager(
private val context: Context,
private val arSceneView: ArSceneView
) {
private var arSession: Session? = null
private var arConfig: Config? = null
private var isSessionCreated = false
// AR会话状态
enum class SessionState {
NOT_INITIALIZED,
INITIALIZING,
TRACKING,
PAUSED,
STOPPED,
ERROR
}
private var currentState = SessionState.NOT_INITIALIZED
/**
* 创建AR会话(关键步骤)
*/
fun createARSession(): SessionState {
if (isSessionCreated) {
return currentState
}
try {
currentState = SessionState.INITIALIZING
// 1. 检查AR Core可用性
val availability = ArCoreApk.getInstance().checkAvailability(context)
if (!availability.isSupported) {
throw ARNotSupportedException("设备不支持AR Core")
}
// 2. 请求安装AR Core(如果需要)
if (availability.isTransient) {
// 显示安装对话框
ArCoreApk.getInstance().requestInstall(context, true)
}
// 3. 创建AR会话
arSession = Session(context).apply {
// 配置会话
arConfig = Config(this).apply {
// 启用平面检测
planeFindingMode = Config.PlaneFindingMode.HORIZONTAL
// 启用光照估计
lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
// 启用深度(如果设备支持)
depthMode = Config.DepthMode.AUTOMATIC
// 启用点云(用于特征点可视化)
cloudAnchorMode = Config.CloudAnchorMode.ENABLED
}
// 应用配置
configure(arConfig)
}
// 4. 设置AR SceneView的会话
arSceneView.setupSession(arSession!!)
isSessionCreated = true
currentState = SessionState.TRACKING
// 5. 开始平面检测
setupPlaneDetection()
Log.i("ARSessionManager", "AR会话创建成功")
} catch (e: Exception) {
currentState = SessionState.ERROR
Log.e("ARSessionManager", "AR会话创建失败", e)
}
return currentState
}
/**
* 设置平面检测回调
*/
private fun setupPlaneDetection() {
arSession?.setOnTapPlaneListener { hitResult: HitResult, plane: Plane, motionEvent: MotionEvent ->
// 当用户在平面上点击时触发
onPlaneTapped?.invoke(hitResult, plane, motionEvent)
}
// 监听平面更新
arSceneView.scene.addOnUpdateListener { frameTime ->
val frame = arSession?.update()
frame?.let {
// 获取所有检测到的平面
val planes = it.getUpdatedTrackables(Plane::class.java)
for (plane in planes) {
if (plane.trackingState == TrackingState.TRACKING) {
onPlaneUpdated?.invoke(plane)
}
}
}
}
}
/**
* 执行光线投射(屏幕坐标转3D坐标)
*/
fun performRayCast(x: Float, y: Float): List<HitResult>? {
  val frame = arSession?.update() ?: return null
  return try {
  // 执行光线投射
  frame.hitTest(x, y)
  } catch (e: Exception) {
  Log.e("ARSessionManager", "光线投射失败", e)
  null
  }
  }
  /**
  * 计算两个3D点之间的距离
  */
  fun calculateDistance(point1: Pose, point2: Pose): Float {
  // 使用欧几里得距离公式
  val dx = point1.tx() - point2.tx()
  val dy = point1.ty() - point2.ty()
  val dz = point1.tz() - point2.tz()
  return sqrt(dx * dx + dy * dy + dz * dz)
  }
  /**
  * 暂停AR会话
  */
  fun pause() {
  arSession?.pause()
  currentState = SessionState.PAUSED
  }
  /**
  * 恢复AR会话
  */
  fun resume() {
  arSession?.resume()
  currentState = SessionState.TRACKING
  }
  /**
  * 销毁AR会话
  */
  fun destroy() {
  arSession?.close()
  arSession = null
  isSessionCreated = false
  currentState = SessionState.STOPPED
  }
  // 回调接口
  var onPlaneTapped: ((HitResult, Plane, MotionEvent) -> Unit)? = null
  var onPlaneUpdated: ((Plane) -> Unit)? = null
  class ARNotSupportedException(message: String) : Exception(message)
  }

当用户点击屏幕时,真正的魔法开始了。我们需要将屏幕上的一个二维点,通过相机投影矩阵和AR Core检测到的平面信息,“反向投影”到三维空间,获得一个真实的世界坐标。这个过程是测量功能的算法核心。

class CoordinateTransformer(
private val cameraIntrinsics: ARCameraManager.CameraIntrinsics
) {
/**
* 屏幕坐标转相机标准化坐标(NDC)
*/
fun screenToNDC(screenX: Float, screenY: Float, screenWidth: Int, screenHeight: Int): Pair<Float, Float> {
  // 归一化设备坐标(-1到1之间)
  val ndcX = (2.0f * screenX / screenWidth) - 1.0f
  val ndcY = 1.0f - (2.0f * screenY / screenHeight)  // Y轴反向
  return Pair(ndcX, ndcY)
  }
  /**
  * NDC坐标转相机空间坐标
  */
  fun ndcToCamera(ndcX: Float, ndcY: Float): Vector3 {
  // 获取相机内参
  val (fx, fy) = cameraIntrinsics.focalLengthPixels()
  val (cx, cy) = cameraIntrinsics.principalPoint()
  // 反向投影到相机空间
  val cameraX = (ndcX * cx) / fx
  val cameraY = (ndcY * cy) / fy
  val cameraZ = 1.0f  // 假设深度为1
  return Vector3(cameraX, cameraY, cameraZ)
  }
  /**
  * 相机空间坐标转世界坐标
  */
  fun cameraToWorld(cameraPoint: Vector3, cameraPose: Pose): Vector3 {
  // 获取相机的旋转矩阵和平移向量
  val rotationMatrix = FloatArray(16)
  val translationMatrix = FloatArray(16)
  cameraPose.toMatrix(rotationMatrix, 0)
  cameraPose.toMatrix(translationMatrix, 0)
  // 提取旋转和平移分量
  val rotation = Matrix3x3.fromArray(rotationMatrix)
  val translation = Vector3(
  translationMatrix[12],
  translationMatrix[13],
  translationMatrix[14]
  )
  // 应用变换:世界坐标 = 旋转 * 相机坐标 + 平移
  val rotatedPoint = rotation.multiply(cameraPoint)
  val worldPoint = rotatedPoint.add(translation)
  return worldPoint
  }
  /**
  * 完整转换:屏幕坐标 -> 世界坐标
  */
  fun screenToWorld(
  screenX: Float,
  screenY: Float,
  screenWidth: Int,
  screenHeight: Int,
  cameraPose: Pose,
  hitDepth: Float? = null
  ): Vector3? {
  try {
  // 步骤1:屏幕坐标 -> NDC
  val (ndcX, ndcY) = screenToNDC(screenX, screenY, screenWidth, screenHeight)
  // 步骤2:NDC -> 相机坐标
  var cameraPoint = ndcToCamera(ndcX, ndcY)
  // 如果有深度信息,调整Z值
  hitDepth?.let {
  cameraPoint = cameraPoint.normalize().multiply(it)
  }
  // 步骤3:相机坐标 -> 世界坐标
  return cameraToWorld(cameraPoint, cameraPose)
  } catch (e: Exception) {
  Log.e("CoordinateTransformer", "坐标转换失败", e)
  return null
  }
  }
  /**
  * 计算测量误差(基于设备移动)
  */
  fun calculateMeasurementError(
  point1: Vector3,
  point2: Vector3,
  cameraMovement: Float,
  distanceToObject: Float
  ): Float {
  // 误差模型:误差 = 基础误差 + 相机移动误差 + 距离误差
  val baseError = 0.02f  // 2cm基础误差
  // 相机移动带来的误差(假设移动1米带来5cm误差)
  val movementError = cameraMovement * 0.05f
  // 距离带来的误差(越远误差越大)
  val distanceError = distanceToObject * 0.03f
  return baseError + movementError + distanceError
  }
  }
  // 数学工具类
  data class Vector3(val x: Float, val y: Float, val z: Float) {
  fun add(other: Vector3): Vector3 {
  return Vector3(x + other.x, y + other.y, z + other.z)
  }
  fun subtract(other: Vector3): Vector3 {
  return Vector3(x - other.x, y - other.y, z - other.z)
  }
  fun multiply(scalar: Float): Vector3 {
  return Vector3(x * scalar, y * scalar, z * scalar)
  }
  fun normalize(): Vector3 {
  val length = sqrt(x * x + y * y + z * z)
  return if (length > 0) Vector3(x / length, y / length, z / length) else this
  }
  fun distanceTo(other: Vector3): Float {
  val dx = x - other.x
  val dy = y - other.y
  val dz = z - other.z
  return sqrt(dx * dx + dy * dy + dz * dz)
  }
  fun dot(other: Vector3): Float {
  return x * other.x + y * other.y + z * other.z
  }
  fun cross(other: Vector3): Vector3 {
  return Vector3(
  y * other.z - z * other.y,
  z * other.x - x * other.z,
  x * other.y - y * other.x
  )
  }
  }
  class Matrix3x3 private constructor(private val data: FloatArray) {
  companion object {
  fun fromArray(array: FloatArray): Matrix3x3 {
  // 提取3x3旋转矩阵(忽略平移部分)
  return Matrix3x3(floatArrayOf(
  array[0], array[1], array[2],
  array[4], array[5], array[6],
  array[8], array[9], array[10]
  ))
  }
  fun identity(): Matrix3x3 {
  return Matrix3x3(floatArrayOf(
  1f, 0f, 0f,
  0f, 1f, 0f,
  0f, 0f, 1f
  ))
  }
  }
  fun multiply(vector: Vector3): Vector3 {
  return Vector3(
  data[0] * vector.x + data[1] * vector.y + data[2] * vector.z,
  data[3] * vector.x + data[4] * vector.y + data[5] * vector.z,
  data[6] * vector.x + data[7] * vector.y + data[8] * vector.z
  )
  }
  }

四、从点到体:实现测量与高级功能

获得两个或更多点的3D坐标后,计算它们之间的欧几里得距离就变得简单。我们可以进一步扩展,实现面积(三角形或多边形)和体积(立方体)的测量。这涉及更多的几何计算,但原理相通。

class GeometryCalculator {
/**
* 计算点到直线的距离
*/
fun pointToLineDistance(
point: Vector3,
linePoint1: Vector3,
linePoint2: Vector3
): Float {
val lineVector = linePoint2.subtract(linePoint1)
val pointVector = point.subtract(linePoint1)
val lineLength = lineVector.distanceTo(Vector3(0f, 0f, 0f))
if (lineLength == 0f) return pointVector.distanceTo(Vector3(0f, 0f, 0f))
val projectionLength = pointVector.dot(lineVector) / lineLength
val projection = lineVector.normalize().multiply(projectionLength)
return pointVector.subtract(projection).distanceTo(Vector3(0f, 0f, 0f))
}
/**
* 计算三角形的面积(海伦公式)
*/
fun triangleArea(
pointA: Vector3,
pointB: Vector3,
pointC: Vector3
): Float {
val sideAB = pointA.distanceTo(pointB)
val sideBC = pointB.distanceTo(pointC)
val sideCA = pointC.distanceTo(pointA)
val s = (sideAB + sideBC + sideCA) / 2f
return sqrt(s * (s - sideAB) * (s - sideBC) * (s - sideCA))
}
/**
* 计算多边形的面积(适用于平面多边形)
*/
fun polygonArea(points: List<Vector3>): Float {
  if (points.size < 3) return 0f
  var area = 0f
  // 使用鞋带公式(Shoelace Formula)
  for (i in points.indices) {
  val current = points[i]
  val next = points[(i + 1) % points.size]
  area += (current.x * next.z - next.x * current.z)
  }
  return abs(area) / 2f
  }
  /**
  * 计算矩形的面积
  */
  fun rectangleArea(
  corner1: Vector3,
  corner2: Vector3,
  corner3: Vector3
  ): Float {
  val width = corner1.distanceTo(corner2)
  val height = corner2.distanceTo(corner3)
  return width * height
  }
  /**
  * 计算体积(长方体)
  */
  fun cuboidVolume(
  corner1: Vector3,
  corner2: Vector3,
  corner3: Vector3,
  heightPoint: Vector3
  ): Float {
  // 计算底面积
  val baseArea = rectangleArea(corner1, corner2, corner3)
  // 计算高度(点到平面的距离)
  val height = pointToPlaneDistance(heightPoint, corner1, corner2, corner3)
  return baseArea * height
  }
  /**
  * 计算点到平面的距离
  */
  fun pointToPlaneDistance(
  point: Vector3,
  planePoint1: Vector3,
  planePoint2: Vector3,
  planePoint3: Vector3
  ): Float {
  // 计算平面法向量
  val vector1 = planePoint2.subtract(planePoint1)
  val vector2 = planePoint3.subtract(planePoint1)
  val normal = vector1.cross(vector2).normalize()
  // 计算点到平面的距离
  val vectorToPoint = point.subtract(planePoint1)
  return abs(vectorToPoint.dot(normal))
  }
  /**
  * 计算角度(三点法)
  */
  fun calculateAngle(
  vertex: Vector3,
  point1: Vector3,
  point2: Vector3
  ): Float {
  val vector1 = point1.subtract(vertex).normalize()
  val vector2 = point2.subtract(vertex).normalize()
  val dotProduct = vector1.dot(vector2)
  val angle = acos(max(-1f, min(1f, dotProduct)))
  return Math.toDegrees(angle.toDouble()).toFloat()
  }
  /**
  * 检查点是否共线
  */
  fun arePointsCollinear(
  point1: Vector3,
  point2: Vector3,
  point3: Vector3,
  tolerance: Float = 0.01f
  ): Boolean {
  val area = triangleArea(point1, point2, point3)
  return area < tolerance
  }
  /**
  * 计算最佳拟合平面(最小二乘法)
  */
  fun bestFitPlane(points: List<Vector3>): PlaneEquation {
    if (points.size < 3) {
    throw IllegalArgumentException("至少需要3个点来计算平面")
    }
    // 计算重心
    val centroid = Vector3(
    points.map { it.x }.average().toFloat(),
    points.map { it.y }.average().toFloat(),
    points.map { it.z }.average().toFloat()
    )
    // 构建协方差矩阵
    var xx = 0f
    var xy = 0f
    var xz = 0f
    var yy = 0f
    var yz = 0f
    var zz = 0f
    for (point in points) {
    val dx = point.x - centroid.x
    val dy = point.y - centroid.y
    val dz = point.z - centroid.z
    xx += dx * dx
    xy += dx * dy
    xz += dx * dz
    yy += dy * dy
    yz += dy * dz
    zz += dz * dz
    }
    // 计算特征值和特征向量
    val detX = yy * zz - yz * yz
    val detY = xx * zz - xz * xz
    val detZ = xx * yy - xy * xy
    val maxDet = maxOf(detX, detY, detZ)
    val normal = when {
    maxDet == detX -> Vector3(
    detX,
    xz * yz - xy * zz,
    xy * yz - xz * yy
    )
    maxDet == detY -> Vector3(
    xz * yz - xy * zz,
    detY,
    xy * xz - yz * xx
    )
    else -> Vector3(
    xy * yz - xz * yy,
    xy * xz - yz * xx,
    detZ
    )
    }.normalize()
    // 平面方程: ax + by + cz + d = 0
    val d = -(normal.x * centroid.x + normal.y * centroid.y + normal.z * centroid.z)
    return PlaneEquation(normal.x, normal.y, normal.z, d)
    }
    data class PlaneEquation(val a: Float, val b: Float, val c: Float, val d: Float) {
    fun distanceToPoint(point: Vector3): Float {
    return abs(a * point.x + b * point.y + c * point.z + d) /
    sqrt(a * a + b * b + c * c)
    }
    }
    }

良好的用户体验离不开直观的UI。我们需要在摄像头预览画面上实时绘制测量点和连线,并清晰标注出距离数值。这通常通过自定义View覆盖在TextureView(CameraX预览)上来实现。

class MeasureActivity : AppCompatActivity() {
private lateinit var arSceneView: ArSceneView
private lateinit var cameraPreviewView: PreviewView
private lateinit var controlPanel: LinearLayout
private lateinit var measurementView: MeasurementOverlayView
private lateinit var arCameraManager: ARCameraManager
private lateinit var arSessionManager: ARSessionManager
private lateinit var coordinateTransformer: CoordinateTransformer
// 测量状态管理
private enum class MeasureMode {
LENGTH,     // 长度测量
AREA,       // 面积测量
VOLUME,     // 体积测量
ANGLE,      // 角度测量
MULTI_POINT // 多点测量
}
private var currentMode = MeasureMode.LENGTH
private val measurementPoints = mutableListOf<MeasurementPoint>()
  private var isMeasuring = false
  override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_measure)
  // 初始化视图
  initViews()
  // 请求权限
  requestPermissions()
  // 初始化AR组件
  initARComponents()
  // 设置交互监听
  setupInteractionListeners()
  }
  private fun initViews() {
  arSceneView = findViewById(R.id.ar_scene_view)
  cameraPreviewView = findViewById(R.id.camera_preview_view)
  controlPanel = findViewById(R.id.control_panel)
  measurementView = findViewById(R.id.measurement_overlay)
  // 设置AR SceneView配置
  arSceneView.apply {
  planeRenderer.isVisible = true
  planeRenderer.isShadowReceiver = true
  // 设置平面渲染颜色(半透明蓝色)
  planeRenderer.material.setFloat3(
  "color",
  Color.colorToFloatArray(Color.argb(100, 0, 120, 255))
  )
  }
  }
  private fun initARComponents() {
  // 初始化CameraX相机
  arCameraManager = ARCameraManager(
  context = this,
  surfaceProvider = cameraPreviewView.surfaceProvider
  )
  // 初始化AR Core会话
  arSessionManager = ARSessionManager(this, arSceneView)
  // 获取相机内参并初始化坐标转换器
  arCameraManager.getCameraIntrinsics()?.let { intrinsics ->
  coordinateTransformer = CoordinateTransformer(intrinsics)
  }
  // 设置AR会话回调
  arSessionManager.onPlaneTapped = { hitResult, plane, motionEvent ->
  handlePlaneTap(hitResult, plane, motionEvent)
  }
  arSessionManager.onPlaneUpdated = { plane ->
  updatePlaneVisualization(plane)
  }
  }
  private fun setupInteractionListeners() {
  // AR SceneView触摸监听
  arSceneView.setOnTouchListener { _, event ->
  when (event.action) {
  MotionEvent.ACTION_DOWN -> {
  if (isMeasuring) {
  handleMeasurementTap(event.x, event.y)
  true
  } else {
  false
  }
  }
  else -> false
  }
  }
  // 控制按钮监听
  setupControlButtons()
  }
  private fun setupControlButtons() {
  // 长度测量按钮
  findViewById<Button>(R.id.btn_length).setOnClickListener {
    currentMode = MeasureMode.LENGTH
    resetMeasurement()
    showInstruction("点击起点和终点测量长度")
    }
    // 面积测量按钮
    findViewById<Button>(R.id.btn_area).setOnClickListener {
      currentMode = MeasureMode.AREA
      resetMeasurement()
      showInstruction("点击三个点测量矩形面积")
      }
      // 体积测量按钮
      findViewById<Button>(R.id.btn_volume).setOnClickListener {
        currentMode = MeasureMode.VOLUME
        resetMeasurement()
        showInstruction("点击四个点测量长方体体积")
        }
        // 角度测量按钮
        findViewById<Button>(R.id.btn_angle).setOnClickListener {
          currentMode = MeasureMode.ANGLE
          resetMeasurement()
          showInstruction("点击三个点测量角度")
          }
          // 多点测量按钮
          findViewById<Button>(R.id.btn_multi_point).setOnClickListener {
            currentMode = MeasureMode.MULTI_POINT
            resetMeasurement()
            showInstruction("点击任意点,双击结束测量")
            }
            // 清除按钮
            findViewById<Button>(R.id.btn_clear).setOnClickListener {
              resetMeasurement()
              }
              // 保存按钮
              findViewById<Button>(R.id.btn_save).setOnClickListener {
                saveMeasurement()
                }
                // 校准按钮
                findViewById<Button>(R.id.btn_calibrate).setOnClickListener {
                  calibrateMeasurement()
                  }
                  }
                  /**
                  * 处理测量点击
                  */
                  private fun handleMeasurementTap(x: Float, y: Float) {
                  // 执行光线投射
                  val hitResults = arSessionManager.performRayCast(x, y)
                  hitResults?.firstOrNull { hit ->
                  hit.trackable is Plane && (hit.trackable as Plane).isPoseInPolygon(hit.hitPose)
                  }?.let { validHit ->
                  // 获取世界坐标
                  val worldPoint = Vector3(
                  validHit.hitPose.tx(),
                  validHit.hitPose.ty(),
                  validHit.hitPose.tz()
                  )
                  // 创建测量点
                  val measurementPoint = MeasurementPoint(
                  worldPosition = worldPoint,
                  screenPosition = PointF(x, y),
                  timestamp = System.currentTimeMillis()
                  )
                  // 根据当前模式处理点
                  when (currentMode) {
                  MeasureMode.LENGTH -> handleLengthMeasurement(measurementPoint)
                  MeasureMode.AREA -> handleAreaMeasurement(measurementPoint)
                  MeasureMode.VOLUME -> handleVolumeMeasurement(measurementPoint)
                  MeasureMode.ANGLE -> handleAngleMeasurement(measurementPoint)
                  MeasureMode.MULTI_POINT -> handleMultiPointMeasurement(measurementPoint)
                  }
                  // 更新UI
                  updateMeasurementDisplay()
                  } ?: run {
                  showToast("请点击在检测到的平面上")
                  }
                  }
                  /**
                  * 处理长度测量(两点)
                  */
                  private fun handleLengthMeasurement(point: MeasurementPoint) {
                  measurementPoints.add(point)
                  if (measurementPoints.size == 2) {
                  // 计算距离
                  val distance = coordinateTransformer.calculateDistance(
                  Pose.makeTranslation(
                  measurementPoints[0].worldPosition.x,
                  measurementPoints[0].worldPosition.y,
                  measurementPoints[0].worldPosition.z
                  ),
                  Pose.makeTranslation(
                  measurementPoints[1].worldPosition.x,
                  measurementPoints[1].worldPosition.y,
                  measurementPoints[1].worldPosition.z
                  )
                  )
                  // 显示结果
                  showResult("长度: ${String.format("%.2f", distance)} 米")
                  // 重置为下一次测量
                  resetMeasurement()
                  }
                  }
                  /**
                  * 处理面积测量(三点确定矩形)
                  */
                  private fun handleAreaMeasurement(point: MeasurementPoint) {
                  measurementPoints.add(point)
                  if (measurementPoints.size == 3) {
                  val calculator = GeometryCalculator()
                  val area = calculator.rectangleArea(
                  measurementPoints[0].worldPosition,
                  measurementPoints[1].worldPosition,
                  measurementPoints[2].worldPosition
                  )
                  showResult("面积: ${String.format("%.2f", area)} 平方米")
                  resetMeasurement()
                  }
                  }
                  /**
                  * 处理角度测量(三点确定角度)
                  */
                  private fun handleAngleMeasurement(point: MeasurementPoint) {
                  measurementPoints.add(point)
                  if (measurementPoints.size == 3) {
                  val calculator = GeometryCalculator()
                  val angle = calculator.calculateAngle(
                  measurementPoints[1].worldPosition,  // 顶点
                  measurementPoints[0].worldPosition,  // 边1
                  measurementPoints[2].worldPosition   // 边2
                  )
                  showResult("角度: ${String.format("%.1f", angle)}°")
                  resetMeasurement()
                  }
                  }
                  /**
                  * 更新测量显示
                  */
                  private fun updateMeasurementDisplay() {
                  measurementView.updatePoints(measurementPoints.map { it.screenPosition })
                  // 根据模式绘制不同的连线
                  when (currentMode) {
                  MeasureMode.LENGTH -> {
                  if (measurementPoints.size >= 2) {
                  measurementView.drawLine(
                  measurementPoints[0].screenPosition,
                  measurementPoints[1].screenPosition
                  )
                  }
                  }
                  MeasureMode.AREA -> {
                  if (measurementPoints.size >= 3) {
                  measurementView.drawPolygon(
                  measurementPoints.take(3).map { it.screenPosition }
                  )
                  }
                  }
                  // 其他模式的绘制逻辑...
                  }
                  }
                  data class MeasurementPoint(
                  val worldPosition: Vector3,
                  val screenPosition: PointF,
                  val timestamp: Long
                  )
                  }
class MeasurementOverlayView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val measurementPaint = Paint().apply {
color = Color.GREEN
strokeWidth = 4f
style = Paint.Style.STROKE
isAntiAlias = true
}
private val pointPaint = Paint().apply {
color = Color.RED
style = Paint.Style.FILL
isAntiAlias = true
}
private val textPaint = Paint().apply {
color = Color.WHITE
textSize = 48f
isAntiAlias = true
typeface = Typeface.DEFAULT_BOLD
}
private val pathPaint = Paint().apply {
color = Color.argb(100, 0, 255, 0)
style = Paint.Style.FILL
isAntiAlias = true
}
private val measurementPoints = mutableListOf<PointF>()
  private val measurementLines = mutableListOf<Pair<PointF, PointF>>()
    private val measurementPolygons = mutableListOf<List<PointF>>()
      private val measurementTexts = mutableListOf<TextAnnotation>()
        private var currentPath: Path? = null
        fun updatePoints(points: List<PointF>) {
          measurementPoints.clear()
          measurementPoints.addAll(points)
          invalidate()
          }
          fun drawLine(start: PointF, end: PointF) {
          measurementLines.add(Pair(start, end))
          invalidate()
          }
          fun drawPolygon(points: List<PointF>) {
            measurementPolygons.add(points)
            invalidate()
            }
            fun drawText(text: String, position: PointF) {
            measurementTexts.add(TextAnnotation(text, position))
            invalidate()
            }
            fun clear() {
            measurementPoints.clear()
            measurementLines.clear()
            measurementPolygons.clear()
            measurementTexts.clear()
            currentPath = null
            invalidate()
            }
            override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            // 绘制点
            measurementPoints.forEach { point ->
            canvas.drawCircle(point.x, point.y, 15f, pointPaint)
            // 绘制点编号
            val index = measurementPoints.indexOf(point)
            canvas.drawText(
            "${index + 1}",
            point.x + 20f,
            point.y - 20f,
            textPaint
            )
            }
            // 绘制线
            measurementLines.forEach { (start, end) ->
            canvas.drawLine(start.x, start.y, end.x, end.y, measurementPaint)
            // 在线段中点显示长度
            val midX = (start.x + end.x) / 2
            val midY = (start.y + end.y) / 2
            // 计算长度(像素),实际应用中应该转换真实世界长度
            val lengthPx = sqrt(
            (end.x - start.x).pow(2) + (end.y - start.y).pow(2)
            )
            canvas.drawText(
            "${String.format("%.1f", lengthPx)}px",
            midX,
            midY - 10f,
            textPaint
            )
            }
            // 绘制多边形
            measurementPolygons.forEach { polygon ->
            if (polygon.size >= 3) {
            val path = Path()
            path.moveTo(polygon[0].x, polygon[0].y)
            for (i in 1 until polygon.size) {
            path.lineTo(polygon[i].x, polygon[i].y)
            }
            path.close()
            // 绘制填充
            canvas.drawPath(path, pathPaint)
            // 绘制边框
            canvas.drawPath(path, measurementPaint)
            }
            }
            // 绘制文字标注
            measurementTexts.forEach { textAnnotation ->
            canvas.drawText(
            textAnnotation.text,
            textAnnotation.position.x,
            textAnnotation.position.y,
            textPaint
            )
            }
            // 绘制当前路径
            currentPath?.let {
            canvas.drawPath(it, measurementPaint)
            }
            }
            fun startNewPath(startPoint: PointF) {
            currentPath = Path().apply {
            moveTo(startPoint.x, startPoint.y)
            }
            invalidate()
            }
            fun addToPath(point: PointF) {
            currentPath?.lineTo(point.x, point.y)
            invalidate()
            }
            fun closePath() {
            currentPath?.close()
            currentPath?.let { path ->
            measurementPolygons.add(extractPointsFromPath(path))
            }
            currentPath = null
            invalidate()
            }
            private fun extractPointsFromPath(path: Path): List<PointF> {
              val points = mutableListOf<PointF>()
                val pathMeasure = PathMeasure(path, false)
                val length = pathMeasure.length
                var distance = 0f
                val step = length / 20  // 采样20个点
                while (distance < length) {
                val coords = FloatArray(2)
                pathMeasure.getPosTan(distance, coords, null)
                points.add(PointF(coords[0], coords[1]))
                distance += step
                }
                return points
                }
                data class TextAnnotation(val text: String, val position: PointF)
                }

然而,单次点击的坐标可能存在抖动。为了提高精度,我们可以引入多帧优化:当用户长按或确认一个点时,应用在短时间内采集多个帧的坐标估算值,然后取中位数或平均值,能有效滤除噪声。

class MeasurementOptimizer {
/**
* 卡尔曼滤波器 - 用于平滑测量结果
*/
class KalmanFilter(
private val processNoise: Float = 0.01f,
private val measurementNoise: Float = 0.1f,
private val estimationError: Float = 1f
) {
private var currentEstimate: Float = 0f
private var currentError: Float = estimationError
fun update(measurement: Float): Float {
// 预测步骤
val predictedError = currentError + processNoise
// 更新步骤
val kalmanGain = predictedError / (predictedError + measurementNoise)
currentEstimate = currentEstimate + kalmanGain * (measurement - currentEstimate)
currentError = (1 - kalmanGain) * predictedError
return currentEstimate
}
fun reset() {
currentEstimate = 0f
currentError = estimationError
}
}
/**
* 多帧平均 - 提高测量稳定性
*/
class MultiFrameAverager(private val windowSize: Int = 10) {
private val measurements = ArrayDeque<Float>()
  fun addMeasurement(measurement: Float): Float {
  measurements.addLast(measurement)
  if (measurements.size > windowSize) {
  measurements.removeFirst()
  }
  return measurements.average().toFloat()
  }
  fun clear() {
  measurements.clear()
  }
  }
  /**
  * 移动平均滤波器
  */
  fun movingAverageFilter(
  measurements: List<Float>,
    windowSize: Int = 5
    ): List<Float> {
      if (measurements.size < windowSize) {
      return measurements
      }
      val result = mutableListOf<Float>()
        for (i in measurements.indices) {
        val start = max(0, i - windowSize / 2)
        val end = min(measurements.size - 1, i + windowSize / 2)
        val window = measurements.subList(start, end + 1)
        result.add(window.average().toFloat())
        }
        return result
        }
        /**
        * 异常值检测与剔除
        */
        fun removeOutliers(
        measurements: List<Float>,
          threshold: Float = 2.0f
          ): List<Float> {
            if (measurements.size < 3) return measurements
            val mean = measurements.average().toFloat()
            val stdDev = calculateStandardDeviation(measurements, mean)
            return measurements.filter { value ->
            abs(value - mean) <= threshold * stdDev
            }
            }
            private fun calculateStandardDeviation(values: List<Float>, mean: Float): Float {
              val variance = values.map { (it - mean).pow(2) }.average().toFloat()
              return sqrt(variance)
              }
              /**
              * 基于传感器数据的精度补偿
              */
              fun compensateWithSensorData(
              measurement: Float,
              gyroData: GyroData,
              accelerometerData: AccelerometerData,
              magneticData: MagneticData
              ): Float {
              // 1. 设备移动补偿
              val movementCompensation = calculateMovementCompensation(gyroData, accelerometerData)
              // 2. 方向补偿(考虑设备倾斜)
              val orientationCompensation = calculateOrientationCompensation(
              accelerometerData,
              magneticData
              )
              // 3. 环境光补偿(如果可用)
              val lightCompensation = calculateLightCompensation()
              // 应用补偿
              val compensated = measurement *
              movementCompensation *
              orientationCompensation *
              lightCompensation
              return max(0f, compensated)
              }
              private fun calculateMovementCompensation(
              gyroData: GyroData,
              accelerometerData: AccelerometerData
              ): Float {
              // 计算设备移动速度
              val angularSpeed = sqrt(
              gyroData.x.pow(2) + gyroData.y.pow(2) + gyroData.z.pow(2)
              )
              val linearAcceleration = sqrt(
              accelerometerData.x.pow(2) +
              accelerometerData.y.pow(2) +
              accelerometerData.z.pow(2)
              )
              // 移动越大,补偿越多(误差越大)
              val movementFactor = 1.0f + angularSpeed * 0.1f + linearAcceleration * 0.05f
              return 1.0f / movementFactor
              }
              private fun calculateOrientationCompensation(
              accelerometerData: AccelerometerData,
              magneticData: MagneticData
              ): Float {
              // 计算设备倾斜角度
              val gravity = Vector3(
              accelerometerData.x,
              accelerometerData.y,
              accelerometerData.z
              ).normalize()
              val tiltAngle = acos(gravity.dot(Vector3(0f, 0f, 1f)))
              // 角度越大(设备越倾斜),补偿越多
              val tiltCompensation = 1.0f + abs(sin(tiltAngle)) * 0.2f
              return 1.0f / tiltCompensation
              }
              private fun calculateLightCompensation(): Float {
              // 简化版本,实际应该使用光照传感器
              return 1.0f
              }
              data class GyroData(val x: Float, val y: Float, val z: Float, val timestamp: Long)
              data class AccelerometerData(val x: Float, val y: Float, val z: Float, val timestamp: Long)
              data class MagneticData(val x: Float, val y: Float, val z: Float, val timestamp: Long)
              }

对于专业级应用,还可以引入校准系统。让用户测量一个已知长度的物体,应用据此计算出一个比例修正因子,用于后续所有测量,从而抵消设备间的系统误差。

class CalibrationSystem(
private val context: Context,
private val arSessionManager: ARSessionManager
) {
companion object {
// 已知长度的参考物体(单位:米)
private val REFERENCE_OBJECTS = mapOf(
"A4纸" to Pair(0.297f, 0.210f),     // A4纸尺寸
"信用卡" to Pair(0.0856f, 0.0539f),  // 信用卡尺寸
"iPhone 14" to Pair(0.1467f, 0.0715f) // iPhone 14尺寸
)
}
private var calibrationFactor = 1.0f
private var calibrationHistory = mutableListOf<CalibrationRecord>()
  private var isCalibrating = false
  /**
  * 开始校准流程
  */
  fun startCalibration(referenceObjectName: String): CalibrationResult {
  val referenceSize = REFERENCE_OBJECTS[referenceObjectName]
  ?: return CalibrationResult.error("未知的参考物体")
  isCalibrating = true
  calibrationHistory.clear()
  return CalibrationResult.success(
  message = "请测量 ${referenceObjectName}${referenceSize.first}米边",
  expectedLength = referenceSize.first,
  objectName = referenceObjectName
  )
  }
  /**
  * 添加校准测量数据
  */
  fun addCalibrationMeasurement(
  measuredLength: Float,
  expectedLength: Float,
  confidence: Float
  ): CalibrationResult {
  if (!isCalibrating) {
  return CalibrationResult.error("未开始校准")
  }
  // 计算校准因子
  val factor = expectedLength / measuredLength
  // 保存校准记录
  val record = CalibrationRecord(
  measuredLength = measuredLength,
  expectedLength = expectedLength,
  factor = factor,
  confidence = confidence,
  timestamp = System.currentTimeMillis()
  )
  calibrationHistory.add(record)
  // 计算平均校准因子(加权平均)
  val totalWeight = calibrationHistory.sumOf { it.confidence.toDouble() }
  val weightedSum = calibrationHistory.sumOf {
  (it.factor * it.confidence).toDouble()
  }
  calibrationFactor = (weightedSum / totalWeight).toFloat()
  return CalibrationResult.success(
  message = "校准进度: ${calibrationHistory.size}/3",
  calibrationFactor = calibrationFactor,
  confidence = calibrationHistory.map { it.confidence }.average().toFloat()
  )
  }
  /**
  * 完成校准
  */
  fun finishCalibration(): CalibrationResult {
  if (!isCalibrating || calibrationHistory.isEmpty()) {
  return CalibrationResult.error("没有校准数据")
  }
  isCalibrating = false
  // 保存校准结果
  saveCalibrationData()
  return CalibrationResult.success(
  message = "校准完成",
  calibrationFactor = calibrationFactor,
  confidence = calculateOverallConfidence()
  )
  }
  /**
  * 应用校准到测量值
  */
  fun applyCalibration(rawMeasurement: Float): Float {
  return rawMeasurement * calibrationFactor
  }
  /**
  * 验证校准准确性
  */
  fun verifyCalibration(knownLength: Float, measuredLength: Float): VerificationResult {
  val calibratedLength = applyCalibration(measuredLength)
  val error = abs(calibratedLength - knownLength)
  val errorPercentage = (error / knownLength) * 100
  val accuracy = when {
  errorPercentage < 1 -> "优秀 (<1%)"
    errorPercentage < 3 -> "良好 (1-3%)"
      errorPercentage < 5 -> "一般 (3-5%)"
        else -> "较差 (>5%)"
        }
        return VerificationResult(
        expectedLength = knownLength,
        measuredLength = measuredLength,
        calibratedLength = calibratedLength,
        error = error,
        errorPercentage = errorPercentage,
        accuracy = accuracy,
        isAcceptable = errorPercentage < 5
        )
        }
        /**
        * 保存校准数据
        */
        private fun saveCalibrationData() {
        val sharedPrefs = context.getSharedPreferences("ar_calibration", Context.MODE_PRIVATE)
        with(sharedPrefs.edit()) {
        putFloat("calibration_factor", calibrationFactor)
        putLong("calibration_time", System.currentTimeMillis())
        putInt("calibration_count", calibrationHistory.size)
        // 保存历史记录(JSON格式)
        val historyJson = Gson().toJson(calibrationHistory)
        putString("calibration_history", historyJson)
        apply()
        }
        }
        /**
        * 加载校准数据
        */
        fun loadCalibrationData(): Boolean {
        val sharedPrefs = context.getSharedPreferences("ar_calibration", Context.MODE_PRIVATE)
        calibrationFactor = sharedPrefs.getFloat("calibration_factor", 1.0f)
        // 检查校准是否过期(30天)
        val calibrationTime = sharedPrefs.getLong("calibration_time", 0)
        val daysSinceCalibration = (System.currentTimeMillis() - calibrationTime) /
        (1000 * 60 * 60 * 24)
        return daysSinceCalibration < 30
        }
        private fun calculateOverallConfidence(): Float {
        if (calibrationHistory.isEmpty()) return 0f
        // 置信度基于:测量次数、单个测量置信度、测量一致性
        val countConfidence = min(1.0f, calibrationHistory.size / 5.0f)
        val avgConfidence = calibrationHistory.map { it.confidence }.average().toFloat()
        // 计算测量一致性(方差)
        val variance = calibrationHistory.map { it.factor }.let { factors ->
        val mean = factors.average().toFloat()
        factors.map { (it - mean).pow(2) }.average().toFloat()
        }
        val consistency = 1.0f / (1.0f + variance * 10)
        return (countConfidence * 0.3f + avgConfidence * 0.4f + consistency * 0.3f)
        }
        data class CalibrationRecord(
        val measuredLength: Float,
        val expectedLength: Float,
        val factor: Float,
        val confidence: Float,
        val timestamp: Long
        )
        data class CalibrationResult(
        val success: Boolean,
        val message: String,
        val calibrationFactor: Float = 1.0f,
        val confidence: Float = 0f,
        val expectedLength: Float = 0f,
        val objectName: String = ""
        ) {
        companion object {
        fun success(
        message: String,
        calibrationFactor: Float = 1.0f,
        confidence: Float = 0f,
        expectedLength: Float = 0f,
        objectName: String = ""
        ): CalibrationResult {
        return CalibrationResult(
        success = true,
        message = message,
        calibrationFactor = calibrationFactor,
        confidence = confidence,
        expectedLength = expectedLength,
        objectName = objectName
        )
        }
        fun error(message: String): CalibrationResult {
        return CalibrationResult(
        success = false,
        message = message
        )
        }
        }
        }
        data class VerificationResult(
        val expectedLength: Float,
        val measuredLength: Float,
        val calibratedLength: Float,
        val error: Float,
        val errorPercentage: Float,
        val accuracy: String,
        val isAcceptable: Boolean
        )
        }
[AFFILIATE_SLOT_2]

五、超越长度:功能扩展与性能调优

基于基本的长度测量,我们可以构建更强大的功能。例如,通过测量三个点定义一个平面,然后计算该平面内一个多边形的面积。

class AdvancedMeasurement {
/**
* 多边形面积测量
*/
class AreaMeasurer {
fun measurePolygonArea(points: List<Vector3>): AreaResult {
  if (points.size < 3) {
  return AreaResult.error("至少需要3个点来测量面积")
  }
  val calculator = GeometryCalculator()
  // 检查点是否共面
  if (!arePointsCoplanar(points)) {
  return AreaResult.error("点不在同一平面上")
  }
  // 计算面积
  val area = calculator.polygonArea(points)
  // 计算周长
  val perimeter = calculatePerimeter(points)
  // 计算中心点
  val centroid = calculateCentroid(points)
  return AreaResult.success(
  area = area,
  perimeter = perimeter,
  centroid = centroid,
  pointCount = points.size,
  shapeType = classifyShape(points)
  )
  }
  private fun arePointsCoplanar(points: List<Vector3>, tolerance: Float = 0.01f): Boolean {
    if (points.size < 4) return true
    // 使用前三个点确定平面
    val plane = GeometryCalculator().bestFitPlane(points.take(3))
    // 检查其他点是否在平面上
    return points.all { point ->
    plane.distanceToPoint(point) < tolerance
    }
    }
    private fun calculatePerimeter(points: List<Vector3>): Float {
      var perimeter = 0f
      for (i in points.indices) {
      val current = points[i]
      val next = points[(i + 1) % points.size]
      perimeter += current.distanceTo(next)
      }
      return perimeter
      }
      private fun calculateCentroid(points: List<Vector3>): Vector3 {
        val sumX = points.sumOf { it.x.toDouble() }
        val sumY = points.sumOf { it.y.toDouble() }
        val sumZ = points.sumOf { it.z.toDouble() }
        val count = points.size.toDouble()
        return Vector3(
        (sumX / count).toFloat(),
        (sumY / count).toFloat(),
        (sumZ / count).toFloat()
        )
        }
        private fun classifyShape(points: List<Vector3>): String {
          return when (points.size) {
          3 -> "三角形"
          4 -> classifyQuadrilateral(points)
          5 -> "五边形"
          6 -> "六边形"
          else -> "多边形 (${points.size}边)"
          }
          }
          private fun classifyQuadrilateral(points: List<Vector3>): String {
            if (points.size != 4) return "未知"
            val sides = listOf(
            points[0].distanceTo(points[1]),
            points[1].distanceTo(points[2]),
            points[2].distanceTo(points[3]),
            points[3].distanceTo(points[0])
            )
            val angles = listOf(
            GeometryCalculator().calculateAngle(points[1], points[0], points[2]),
            GeometryCalculator().calculateAngle(points[2], points[1], points[3]),
            GeometryCalculator().calculateAngle(points[3], points[2], points[0]),
            GeometryCalculator().calculateAngle(points[0], points[3], points[1])
            )
            // 检查是否为矩形
            val isRectangle = angles.all { abs(it - 90f) < 5f }
            // 检查是否为正方形
            val sideVariance = sides.map { (it - sides.average()).pow(2) }.average()
            val isSquare = isRectangle && sideVariance < 0.001f
            return when {
            isSquare -> "正方形"
            isRectangle -> "矩形"
            else -> "四边形"
            }
            }
            }
            /**
            * 体积测量
            */
            class VolumeMeasurer {
            fun measureCuboidVolume(
            basePoints: List<Vector3>,  // 底面多边形(至少3个点)
              height: Float               // 高度
              ): VolumeResult {
              if (basePoints.size < 3) {
              return VolumeResult.error("至少需要3个点定义底面")
              }
              val areaMeasurer = AreaMeasurer()
              val baseAreaResult = areaMeasurer.measurePolygonArea(basePoints)
              if (!baseAreaResult.success) {
              return VolumeResult.error("底面面积计算失败: ${baseAreaResult.message}")
              }
              val volume = baseAreaResult.area * height
              // 计算表面积
              val lateralArea = calculateLateralArea(basePoints, height)
              val totalArea = baseAreaResult.area * 2 + lateralArea
              return VolumeResult.success(
              volume = volume,
              baseArea = baseAreaResult.area,
              height = height,
              surfaceArea = totalArea,
              shapeType = "${baseAreaResult.shapeType}柱体"
              )
              }
              fun measureIrregularVolume(
              bottomPoints: List<Vector3>,
                topPoints: List<Vector3>
                  ): VolumeResult {
                  if (bottomPoints.size != topPoints.size) {
                  return VolumeResult.error("上下底面点数不一致")
                  }
                  if (bottomPoints.size < 3) {
                  return VolumeResult.error("至少需要3个点定义底面")
                  }
                  // 使用棱台体积公式
                  val bottomArea = AreaMeasurer().measurePolygonArea(bottomPoints).area
                  val topArea = AreaMeasurer().measurePolygonArea(topPoints).area
                  // 计算平均高度
                  val heights = bottomPoints.indices.map { i ->
                  bottomPoints[i].distanceTo(topPoints[i])
                  }
                  val avgHeight = heights.average()
                  // 棱台体积公式: V = (h/3) * (A1 + A2 + sqrt(A1*A2))
                  val volume = (avgHeight / 3) *
                  (bottomArea + topArea + sqrt(bottomArea * topArea))
                  return VolumeResult.success(
                  volume = volume.toFloat(),
                  baseArea = bottomArea,
                  topArea = topArea,
                  avgHeight = avgHeight.toFloat(),
                  shapeType = "棱台"
                  )
                  }
                  private fun calculateLateralArea(polygon: List<Vector3>, height: Float): Float {
                    var lateralArea = 0f
                    for (i in polygon.indices) {
                    val current = polygon[i]
                    val next = polygon[(i + 1) % polygon.size]
                    val sideLength = current.distanceTo(next)
                    lateralArea += sideLength * height
                    }
                    return lateralArea
                    }
                    }
                    data class AreaResult(
                    val success: Boolean,
                    val area: Float = 0f,
                    val perimeter: Float = 0f,
                    val centroid: Vector3? = null,
                    val pointCount: Int = 0,
                    val shapeType: String = "",
                    val message: String = ""
                    ) {
                    companion object {
                    fun success(
                    area: Float,
                    perimeter: Float,
                    centroid: Vector3,
                    pointCount: Int,
                    shapeType: String
                    ): AreaResult {
                    return AreaResult(
                    success = true,
                    area = area,
                    perimeter = perimeter,
                    centroid = centroid,
                    pointCount = pointCount,
                    shapeType = shapeType,
                    message = "测量成功"
                    )
                    }
                    fun error(message: String): AreaResult {
                    return AreaResult(
                    success = false,
                    message = message
                    )
                    }
                    }
                    }
                    data class VolumeResult(
                    val success: Boolean,
                    val volume: Float = 0f,
                    val baseArea: Float = 0f,
                    val topArea: Float = 0f,
                    val height: Float = 0f,
                    val avgHeight: Float = 0f,
                    val surfaceArea: Float = 0f,
                    val shapeType: String = "",
                    val message: String = ""
                    ) {
                    companion object {
                    fun success(
                    volume: Float,
                    baseArea: Float = 0f,
                    height: Float = 0f,
                    surfaceArea: Float = 0f,
                    shapeType: String,
                    topArea: Float = 0f,
                    avgHeight: Float = 0f
                    ): VolumeResult {
                    return VolumeResult(
                    success = true,
                    volume = volume,
                    baseArea = baseArea,
                    topArea = topArea,
                    height = height,
                    avgHeight = avgHeight,
                    surfaceArea = surfaceArea,
                    shapeType = shapeType,
                    message = "测量成功"
                    )
                    }
                    fun error(message: String): VolumeResult {
                    return VolumeResult(
                    success = false,
                    message = message
                    )
                    }
                    }
                    }
                    }

实用的应用应该能保存测量记录。我们可以将测量结果(坐标、长度、时间戳)保存到本地数据库(如Room),并允许用户查看、管理和导出历史数据。这里,JavaKotlin的本地存储知识,或者考虑使用Go编写一个简单的同步服务端,都是可行的扩展方向。

class MeasurementHistoryManager(private val context: Context) {
private val measurements = mutableListOf<MeasurementRecord>()
  private val MAX_HISTORY_SIZE = 100
  /**
  * 保存测量记录
  */
  fun saveMeasurement(record: MeasurementRecord): Boolean {
  measurements.add(record)
  // 限制历史记录数量
  if (measurements.size > MAX_HISTORY_SIZE) {
  measurements.removeAt(0)
  }
  // 保存到数据库
  return saveToDatabase(record)
  }
  /**
  * 获取所有测量记录
  */
  fun getAllMeasurements(): List<MeasurementRecord> {
    // 优先从内存读取,如果为空则从数据库加载
    if (measurements.isEmpty()) {
    loadFromDatabase()
    }
    return measurements.toList()
    }
    /**
    * 按类型筛选测量记录
    */
    fun getMeasurementsByType(type: MeasurementType): List<MeasurementRecord> {
      return measurements.filter { it.type == type }
      }
      /**
      * 搜索测量记录
      */
      fun searchMeasurements(query: String): List<MeasurementRecord> {
        return measurements.filter { record ->
        record.name.contains(query, ignoreCase = true) ||
        record.tags.any { it.contains(query, ignoreCase = true) } ||
        record.notes?.contains(query, ignoreCase = true) ?: false
        }
        }
        /**
        * 导出测量数据
        */
        fun exportMeasurements(format: ExportFormat): ExportResult {
        return when (format) {
        ExportFormat.JSON -> exportToJson()
        ExportFormat.CSV -> exportToCsv()
        ExportFormat.PDF -> exportToPdf()
        }
        }
        /**
        * 生成测量报告
        */
        fun generateReport(record: MeasurementRecord): Report {
        return Report(
        title = "测量报告 - ${record.name}",
        timestamp = record.timestamp,
        content = buildReportContent(record),
        summary = generateSummary(record)
        )
        }
        private fun saveToDatabase(record: MeasurementRecord): Boolean {
        val dbHelper = MeasurementDbHelper(context)
        val db = dbHelper.writableDatabase
        return try {
        val values = ContentValues().apply {
        put(MeasurementContract.MeasurementEntry.COLUMN_NAME_NAME, record.name)
        put(MeasurementContract.MeasurementEntry.COLUMN_NAME_TYPE, record.type.name)
        put(MeasurementContract.MeasurementEntry.COLUMN_NAME_DATA,
        Gson().toJson(record.data))
        put(MeasurementContract.MeasurementEntry.COLUMN_NAME_TIMESTAMP,
        record.timestamp)
        put(MeasurementContract.MeasurementEntry.COLUMN_NAME_TAGS,
        record.tags.joinToString(","))
        put(MeasurementContract.MeasurementEntry.COLUMN_NAME_NOTES,
        record.notes)
        put(MeasurementContract.MeasurementEntry.COLUMN_NAME_LOCATION,
        Gson().toJson(record.location))
        }
        db.insert(MeasurementContract.MeasurementEntry.TABLE_NAME, null, values)
        true
        } catch (e: Exception) {
        false
        } finally {
        db.close()
        }
        }
        private fun loadFromDatabase() {
        measurements.clear()
        val dbHelper = MeasurementDbHelper(context)
        val db = dbHelper.readableDatabase
        val projection = arrayOf(
        MeasurementContract.MeasurementEntry.COLUMN_NAME_NAME,
        MeasurementContract.MeasurementEntry.COLUMN_NAME_TYPE,
        MeasurementContract.MeasurementEntry.COLUMN_NAME_DATA,
        MeasurementContract.MeasurementEntry.COLUMN_NAME_TIMESTAMP,
        MeasurementContract.MeasurementEntry.COLUMN_NAME_TAGS,
        MeasurementContract.MeasurementEntry.COLUMN_NAME_NOTES,
        MeasurementContract.MeasurementEntry.COLUMN_NAME_LOCATION
        )
        val cursor = db.query(
        MeasurementContract.MeasurementEntry.TABLE_NAME,
        projection,
        null,
        null,
        null,
        null,
        "${MeasurementContract.MeasurementEntry.COLUMN_NAME_TIMESTAMP} DESC"
        )
        with(cursor) {
        while (moveToNext()) {
        val name = getString(getColumnIndexOrThrow(
        MeasurementContract.MeasurementEntry.COLUMN_NAME_NAME))
        val type = MeasurementType.valueOf(getString(getColumnIndexOrThrow(
        MeasurementContract.MeasurementEntry.COLUMN_NAME_TYPE)))
        val dataJson = getString(getColumnIndexOrThrow(
        MeasurementContract.MeasurementEntry.COLUMN_NAME_DATA))
        val timestamp = getLong(getColumnIndexOrThrow(
        MeasurementContract.MeasurementEntry.COLUMN_NAME_TIMESTAMP))
        val tagsStr = getString(getColumnIndexOrThrow(
        MeasurementContract.MeasurementEntry.COLUMN_NAME_TAGS))
        val notes = getString(getColumnIndexOrThrow(
        MeasurementContract.MeasurementEntry.COLUMN_NAME_NOTES))
        val locationJson = getString(getColumnIndexOrThrow(
        MeasurementContract.MeasurementEntry.COLUMN_NAME_LOCATION))
        val record = MeasurementRecord(
        name = name,
        type = type,
        data = Gson().fromJson(dataJson, MeasurementData::class.java),
        timestamp = timestamp,
        tags = tagsStr.split(","),
        notes = notes,
        location = Gson().fromJson(locationJson, LocationData::class.java)
        )
        measurements.add(record)
        }
        close()
        }
        db.close()
        }
        private fun buildReportContent(record: MeasurementRecord): String {
        return buildString {
        appendLine("=== 测量详情 ===")
        appendLine("名称: ${record.name}")
        appendLine("类型: ${record.type.displayName}")
        appendLine("时间: ${Date(record.timestamp)}")
        appendLine()
        when (record.type) {
        MeasurementType.LENGTH -> {
        val data = record.data as LengthData
        appendLine("长度: ${String.format("%.3f", data.length)} 米")
        appendLine("起点: (${String.format("%.3f", data.start.x)}, " +
        "${String.format("%.3f", data.start.y)}, " +
        "${String.format("%.3f", data.start.z)})")
        appendLine("终点: (${String.format("%.3f", data.end.x)}, " +
        "${String.format("%.3f", data.end.y)}, " +
        "${String.format("%.3f", data.end.z)})")
        }
        MeasurementType.AREA -> {
        val data = record.data as AreaData
        appendLine("面积: ${String.format("%.3f", data.area)} 平方米")
        appendLine("周长: ${String.format("%.3f", data.perimeter)} 米")
        appendLine("形状: ${data.shapeType}")
        appendLine("点数: ${data.pointCount}")
        }
        MeasurementType.VOLUME -> {
        val data = record.data as VolumeData
        appendLine("体积: ${String.format("%.3f", data.volume)} 立方米")
        appendLine("底面积: ${String.format("%.3f", data.baseArea)} 平方米")
        appendLine("高度: ${String.format("%.3f", data.height)} 米")
        appendLine("形状: ${data.shapeType}")
        }
        MeasurementType.ANGLE -> {
        val data = record.data as AngleData
        appendLine("角度: ${String.format("%.1f", data.angle)}°")
        appendLine("顶点: (${String.format("%.3f", data.vertex.x)}, " +
        "${String.format("%.3f", data.vertex.y)}, " +
        "${String.format("%.3f", data.vertex.z)})")
        }
        }
        if (record.notes?.isNotEmpty() == true) {
        appendLine()
        appendLine("备注: ${record.notes}")
        }
        if (record.tags.isNotEmpty()) {
        appendLine()
        appendLine("标签: ${record.tags.joinToString(", ")}")
        }
        }
        }
        sealed class MeasurementType(val displayName: String) {
        object LENGTH : MeasurementType("长度")
        object AREA : MeasurementType("面积")
        object VOLUME : MeasurementType("体积")
        object ANGLE : MeasurementType("角度")
        object MULTI_POINT : MeasurementType("多点")
        }
        data class MeasurementRecord(
        val name: String,
        val type: MeasurementType,
        val data: MeasurementData,
        val timestamp: Long = System.currentTimeMillis(),
        val tags: List<String> = emptyList(),
          val notes: String? = null,
          val location: LocationData? = null
          )
          sealed class MeasurementData
          data class LengthData(
          val length: Float,
          val start: Vector3,
          val end: Vector3,
          val confidence: Float = 1.0f
          ) : MeasurementData()
          data class AreaData(
          val area: Float,
          val perimeter: Float,
          val shapeType: String,
          val pointCount: Int,
          val points: List<Vector3> = emptyList()
            ) : MeasurementData()
            data class VolumeData(
            val volume: Float,
            val baseArea: Float,
            val height: Float,
            val surfaceArea: Float = 0f,
            val shapeType: String
            ) : MeasurementData()
            data class AngleData(
            val angle: Float,
            val vertex: Vector3,
            val arm1: Vector3,
            val arm2: Vector3
            ) : MeasurementData()
            data class LocationData(
            val latitude: Double,
            val longitude: Double,
            val altitude: Double? = null,
            val accuracy: Float? = null
            )
            enum class ExportFormat { JSON, CSV, PDF }
            data class ExportResult(
            val success: Boolean,
            val filePath: String? = null,
            val error: String? = null
            )
            data class Report(
            val title: String,
            val timestamp: Long,
            val content: String,
            val summary: String
            )
            }

AR应用是计算密集型的。必须密切关注性能,确保帧率稳定。我们可以监控AR会话的状态和帧处理时间,在检测到性能下降时(如过多特征点),动态调整AR Core的配置或降低分析分辨率。

class PerformanceMonitor {
private val frameTimes = ArrayDeque<Long>()
  private val measurementTimes = ArrayDeque<Long>()
    private val memoryUsage = ArrayDeque<Long>()
      private var isMonitoring = false
      private val maxSamples = 60  // 保留最近60个样本
      /**
      * 开始性能监控
      */
      fun startMonitoring() {
      isMonitoring = true
      // 启动监控线程
      Thread {
      while (isMonitoring) {
      // 监控帧率
      monitorFrameRate()
      // 监控内存
      monitorMemoryUsage()
      // 监控CPU
      monitorCpuUsage()
      Thread.sleep(1000)  // 每秒采样一次
      }
      }.start()
      }
      /**
      * 记录帧时间
      */
      fun recordFrameTime(frameTime: Long) {
      if (!isMonitoring) return
      frameTimes.addLast(frameTime)
      if (frameTimes.size > maxSamples) {
      frameTimes.removeFirst()
      }
      }
      /**
      * 记录测量时间
      */
      fun recordMeasurementTime(measurementTime: Long) {
      measurementTimes.addLast(measurementTime)
      if (measurementTimes.size > maxSamples) {
      measurementTimes.removeFirst()
      }
      }
      /**
      * 获取性能报告
      */
      fun getPerformanceReport(): PerformanceReport {
      val fps = calculateFPS()
      val avgMeasurementTime = if (measurementTimes.isNotEmpty()) {
      measurementTimes.average().toLong()
      } else 0L
      val currentMemory = getCurrentMemoryUsage()
      val memoryTrend = analyzeMemoryTrend()
      return PerformanceReport(
      fps = fps,
      frameTimeStats = calculateFrameTimeStats(),
      measurementTimeStats = calculateMeasurementTimeStats(),
      memoryUsage = MemoryUsage(
      current = currentMemory,
      max = memoryUsage.maxOrNull() ?: 0L,
      average = if (memoryUsage.isNotEmpty()) memoryUsage.average().toLong() else 0L,
      trend = memoryTrend
      ),
      recommendations = generateRecommendations(fps, currentMemory)
      )
      }
      private fun calculateFPS(): Float {
      if (frameTimes.size < 2) return 0f
      val totalTime = frameTimes.sum()
      val avgFrameTime = totalTime.toFloat() / frameTimes.size
      return 1000f / avgFrameTime  // 转换为FPS
      }
      private fun calculateFrameTimeStats(): FrameTimeStats {
      if (frameTimes.isEmpty()) return FrameTimeStats()
      return FrameTimeStats(
      min = frameTimes.min(),
      max = frameTimes.max(),
      average = frameTimes.average().toLong(),
      percentile95 = calculatePercentile95(frameTimes)
      )
      }
      private fun calculateMeasurementTimeStats(): MeasurementTimeStats {
      if (measurementTimes.isEmpty()) return MeasurementTimeStats()
      return MeasurementTimeStats(
      min = measurementTimes.min(),
      max = measurementTimes.max(),
      average = measurementTimes.average().toLong(),
      percentile95 = calculatePercentile95(measurementTimes)
      )
      }
      private fun calculatePercentile95(times: Deque<Long>): Long {
        if (times.isEmpty()) return 0L
        val sorted = times.sorted()
        val index = (sorted.size * 0.95).toInt()
        return sorted[min(index, sorted.size - 1)]
        }
        private fun monitorMemoryUsage() {
        val runtime = Runtime.getRuntime()
        val usedMemory = runtime.totalMemory() - runtime.freeMemory()
        memoryUsage.addLast(usedMemory)
        if (memoryUsage.size > maxSamples) {
        memoryUsage.removeFirst()
        }
        }
        private fun getCurrentMemoryUsage(): Long {
        val runtime = Runtime.getRuntime()
        return runtime.totalMemory() - runtime.freeMemory()
        }
        private fun analyzeMemoryTrend(): MemoryTrend {
        if (memoryUsage.size < 5) return MemoryTrend.STABLE
        val recent = memoryUsage.takeLast(5).toList()
        val oldest = memoryUsage.take(5).toList()
        val recentAvg = recent.average()
        val oldestAvg = oldest.average()
        return when {
        recentAvg > oldestAvg * 1.2 -> MemoryTrend.INCREASING
        recentAvg < oldestAvg * 0.8 -> MemoryTrend.DECREASING
          else -> MemoryTrend.STABLE
          }
          }
          private fun monitorCpuUsage() {
          // 实现CPU使用率监控
          // 注意:Android上获取准确的CPU使用率比较复杂
          }
          private fun generateRecommendations(fps: Float, memory: Long): List<String> {
            val recommendations = mutableListOf<String>()
              // 帧率建议
              when {
              fps < 20 -> recommendations.add("帧率过低,建议关闭不必要的AR特效")
                fps < 30 -> recommendations.add("帧率较低,建议简化场景")
                  fps >= 60 -> recommendations.add("帧率优秀")
                  }
                  // 内存建议
                  val maxMemory = Runtime.getRuntime().maxMemory()
                  val memoryUsagePercent = memory.toFloat() / maxMemory.toFloat()
                  when {
                  memoryUsagePercent > 0.8 -> recommendations.add("内存使用过高,建议清理缓存")
                  memoryUsagePercent > 0.6 -> recommendations.add("内存使用较高,注意监控")
                  memoryUsagePercent < 0.3 -> recommendations.add("内存使用良好")
                    }
                    return recommendations
                    }
                    /**
                    * 停止监控
                    */
                    fun stopMonitoring() {
                    isMonitoring = false
                    frameTimes.clear()
                    measurementTimes.clear()
                    memoryUsage.clear()
                    }
                    data class PerformanceReport(
                    val fps: Float = 0f,
                    val frameTimeStats: FrameTimeStats = FrameTimeStats(),
                    val measurementTimeStats: MeasurementTimeStats = MeasurementTimeStats(),
                    val memoryUsage: MemoryUsage = MemoryUsage(),
                    val recommendations: List<String> = emptyList()
                      )
                      data class FrameTimeStats(
                      val min: Long = 0L,
                      val max: Long = 0L,
                      val average: Long = 0L,
                      val percentile95: Long = 0L
                      )
                      data class MeasurementTimeStats(
                      val min: Long = 0L,
                      val max: Long = 0L,
                      val average: Long = 0L,
                      val percentile95: Long = 0L
                      )
                      data class MemoryUsage(
                      val current: Long = 0L,
                      val max: Long = 0L,
                      val average: Long = 0L,
                      val trend: MemoryTrend = MemoryTrend.STABLE
                      )
                      enum class MemoryTrend { INCREASING, DECREASING, STABLE }
                      }

最后,严谨的测试不可或缺。需要在不同环境(光照、纹理)、对不同材质和距离的物体进行测量,并与真实值对比,持续优化算法参数。自动化测试脚本可以用Python快速编写,进行批量数据分析。

class MeasurementAccuracyTest {
/**
* 测试已知长度的物体
*/
fun testKnownObject(
referenceObject: ReferenceObject,
measuredLength: Float,
numberOfTrials: Int = 10
): AccuracyTestResult {
val measurements = mutableListOf<Float>()
  val errors = mutableListOf<Float>()
    val errorPercentages = mutableListOf<Float>()
      // 进行多次测量
      repeat(numberOfTrials) { trial ->
      // 模拟测量(实际应用中应从AR测量获取)
      val measurement = simulateMeasurement(referenceObject.actualLength)
      measurements.add(measurement)
      // 计算误差
      val error = abs(measurement - referenceObject.actualLength)
      errors.add(error)
      // 计算误差百分比
      val errorPercentage = (error / referenceObject.actualLength) * 100
      errorPercentages.add(errorPercentage)
      Log.d("AccuracyTest",
      "试验 ${trial + 1}: 测量=${String.format("%.3f", measurement)}m, " +
      "误差=${String.format("%.3f", error)}m (${String.format("%.1f", errorPercentage)}%)")
      }
      // 计算统计指标
      val avgMeasurement = measurements.average().toFloat()
      val avgError = errors.average().toFloat()
      val stdDev = calculateStandardDeviation(measurements)
      // 计算准确度评级
      val accuracyRating = calculateAccuracyRating(avgError, referenceObject.actualLength)
      return AccuracyTestResult(
      referenceObject = referenceObject,
      numberOfTrials = numberOfTrials,
      measurements = measurements,
      averageMeasurement = avgMeasurement,
      averageError = avgError,
      standardDeviation = stdDev,
      errorPercentages = errorPercentages,
      accuracyRating = accuracyRating,
      confidenceLevel = calculateConfidenceLevel(stdDev, numberOfTrials),
      isAcceptable = avgError <= referenceObject.maxAcceptableError
      )
      }
      /**
      * 测试不同距离的测量准确性
      */
      fun testDistanceAccuracy(
      testDistances: List<Float>,  // 测试距离列表(米)
        numberOfTrials: Int = 5
        ): DistanceAccuracyReport {
        val resultsByDistance = mutableMapOf<Float, AccuracyTestResult>()
          testDistances.forEach { distance ->
          val referenceObject = ReferenceObject(
          name = "测试距离 $distance 米",
          actualLength = distance,
          maxAcceptableError = distance * 0.05f  // 5%误差可接受
          )
          val result = testKnownObject(referenceObject, distance, numberOfTrials)
          resultsByDistance[distance] = result
          }
          // 分析距离与误差的关系
          val distanceErrorPairs = resultsByDistance.map { (distance, result) ->
          Pair(distance, result.averageError)
          }
          // 拟合误差曲线:误差 = a * 距离 + b
          val (a, b) = linearRegression(distanceErrorPairs)
          return DistanceAccuracyReport(
          resultsByDistance = resultsByDistance,
          distanceErrorRelationship = DistanceErrorRelationship(a, b),
          overallAccuracy = calculateOverallAccuracy(resultsByDistance.values),
          recommendations = generateDistanceRecommendations(resultsByDistance)
          )
          }
          /**
          * 环境因素影响测试
          */
          fun testEnvironmentalFactors(): EnvironmentalTestReport {
          val testConditions = listOf(
          TestCondition("理想光照", lighting = LightingCondition.GOOD),
          TestCondition("低光照", lighting = LightingCondition.LOW),
          TestCondition("强光照", lighting = LightingCondition.HIGH),
          TestCondition("纹理丰富表面", surface = SurfaceType.TEXTURED),
          TestCondition("光滑表面", surface = SurfaceType.SMOOTH),
          TestCondition("移动环境", stability = Stability.MOVING)
          )
          val results = mutableListOf<ConditionalTestResult>()
            testConditions.forEach { condition ->
            // 在每种条件下进行测试
            val measurement = simulateMeasurementWithCondition(10.0f, condition)
            val reference = ReferenceObject("测试物体", 10.0f, 0.5f)
            val result = testKnownObject(reference, measurement, 3)
            results.add(ConditionalTestResult(condition, result))
            }
            return EnvironmentalTestReport(
            results = results,
            mostFavorableCondition = results.minByOrNull { it.result.averageError }?.condition,
            leastFavorableCondition = results.maxByOrNull { it.result.averageError }?.condition,
            environmentalImpact = analyzeEnvironmentalImpact(results)
            )
            }
            private fun simulateMeasurement(actualLength: Float): Float {
            // 模拟测量误差:实际值 + 随机误差
            val randomError = (Random.nextFloat() - 0.5f) * 0.1f  // ±5cm随机误差
            val systematicError = 0.02f  // 2cm系统误差
            return actualLength + systematicError + randomError
            }
            private fun simulateMeasurementWithCondition(
            actualLength: Float,
            condition: TestCondition
            ): Float {
            var measurement = simulateMeasurement(actualLength)
            // 根据环境条件调整误差
            when (condition.lighting) {
            LightingCondition.LOW -> measurement += 0.05f  // 低光增加5cm误差
            LightingCondition.HIGH -> measurement += 0.03f // 强光增加3cm误差
            else -> {}  // 理想光照不额外增加误差
            }
            when (condition.surface) {
            SurfaceType.SMOOTH -> measurement += 0.08f  // 光滑表面增加8cm误差
            else -> {}  // 纹理丰富表面不额外增加误差
            }
            when (condition.stability) {
            Stability.MOVING -> measurement += 0.10f  // 移动环境增加10cm误差
            else -> {}  // 稳定环境不额外增加误差
            }
            return measurement
            }
            private fun calculateAccuracyRating(avgError: Float, actualLength: Float): String {
            val errorPercentage = (avgError / actualLength) * 100
            return when {
            errorPercentage < 1 -> "优秀 (<1%)"
              errorPercentage < 3 -> "良好 (1-3%)"
                errorPercentage < 5 -> "一般 (3-5%)"
                  errorPercentage < 10 -> "较差 (5-10%)"
                    else -> "很差 (>10%)"
                    }
                    }
                    private fun calculateConfidenceLevel(stdDev: Float, sampleSize: Int): Float {
                    // 简化的置信度计算
                    val confidence = 1.0f - (stdDev / 0.1f)  // 假设0.1m为标准差基准
                    // 考虑样本量
                    val sampleFactor = min(1.0f, sampleSize / 30.0f)
                    return max(0f, confidence * sampleFactor)
                    }
                    data class ReferenceObject(
                    val name: String,
                    val actualLength: Float,          // 实际长度(米)
                    val maxAcceptableError: Float     // 最大可接受误差(米)
                    )
                    data class AccuracyTestResult(
                    val referenceObject: ReferenceObject,
                    val numberOfTrials: Int,
                    val measurements: List<Float>,
                      val averageMeasurement: Float,
                      val averageError: Float,
                      val standardDeviation: Float,
                      val errorPercentages: List<Float>,
                        val accuracyRating: String,
                        val confidenceLevel: Float,
                        val isAcceptable: Boolean
                        ) {
                        val minError = measurements.minOrNull() ?: 0f
                        val maxError = measurements.maxOrNull() ?: 0f
                        }
                        data class DistanceAccuracyReport(
                        val resultsByDistance: Map<Float, AccuracyTestResult>,
                          val distanceErrorRelationship: DistanceErrorRelationship,
                          val overallAccuracy: String,
                          val recommendations: List<String>
                            )
                            data class DistanceErrorRelationship(
                            val slope: Float,  // 误差随距离增长的斜率
                            val intercept: Float  // 距离为0时的误差
                            ) {
                            fun predictError(distance: Float): Float {
                            return slope * distance + intercept
                            }
                            }
                            data class EnvironmentalTestReport(
                            val results: List<ConditionalTestResult>,
                              val mostFavorableCondition: TestCondition?,
                              val leastFavorableCondition: TestCondition?,
                              val environmentalImpact: Map<String, Float>  // 各因素对误差的影响程度
                                )
                                data class ConditionalTestResult(
                                val condition: TestCondition,
                                val result: AccuracyTestResult
                                )
                                data class TestCondition(
                                val name: String,
                                val lighting: LightingCondition = LightingCondition.GOOD,
                                val surface: SurfaceType = SurfaceType.TEXTURED,
                                val stability: Stability = Stability.STABLE
                                )
                                enum class LightingCondition { GOOD, LOW, HIGH }
                                enum class SurfaceType { TEXTURED, SMOOTH }
                                enum class Stability { STABLE, MOVING }
                                }

将所有模块集成到一个完整的应用中,提供清晰的UI流程:启动、平面搜索、开始测量、查看历史。这是从“技术Demo”到“可交付产品”的关键一步。

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var arSessionManager: ARSessionManager
private lateinit var measurementManager: MeasurementManager
private lateinit var calibrationSystem: CalibrationSystem
private var currentMeasurement: Measurement? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 初始化AR组件
initializeARComponents()
// 设置UI监听器
setupUIListeners()
// 检查AR Core可用性
checkARAvailability()
// 加载校准数据
calibrationSystem.loadCalibrationData()
}
private fun initializeARComponents() {
// 初始化AR会话管理器
arSessionManager = ARSessionManager(this, binding.arSceneView)
// 初始化测量管理器
measurementManager = MeasurementManager(this, arSessionManager)
// 初始化校准系统
calibrationSystem = CalibrationSystem(this, arSessionManager)
// 设置测量回调
measurementManager.onMeasurementComplete = { measurement ->
handleMeasurementComplete(measurement)
}
measurementManager.onMeasurementUpdate = { progress ->
updateMeasurementProgress(progress)
}
}
private fun setupUIListeners() {
// 测量模式选择
binding.btnLength.setOnClickListener {
startMeasurement(MeasurementType.LENGTH)
}
binding.btnArea.setOnClickListener {
startMeasurement(MeasurementType.AREA)
}
binding.btnVolume.setOnClickListener {
startMeasurement(MeasurementType.VOLUME)
}
binding.btnAngle.setOnClickListener {
startMeasurement(MeasurementType.ANGLE)
}
// 控制按钮
binding.btnClear.setOnClickListener {
clearCurrentMeasurement()
}
binding.btnSave.setOnClickListener {
saveCurrentMeasurement()
}
binding.btnCalibrate.setOnClickListener {
showCalibrationDialog()
}
binding.btnHistory.setOnClickListener {
showMeasurementHistory()
}
binding.btnSettings.setOnClickListener {
showSettings()
}
// AR SceneView触摸监听
binding.arSceneView.setOnTouchListener { _, event ->
handleARTouch(event)
}
}
private fun handleARTouch(event: MotionEvent): Boolean {
return when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 如果正在测量,处理测量点
currentMeasurement?.let { measurement ->
val screenX = event.x
val screenY = event.y
measurementManager.addMeasurementPoint(screenX, screenY)
true
} ?: false
}
MotionEvent.ACTION_MOVE -> {
// 处理拖动(如移动测量点)
true
}
MotionEvent.ACTION_UP -> {
// 处理点击结束
true
}
else -> false
}
}
private fun startMeasurement(type: MeasurementType) {
// 检查AR会话状态
if (!arSessionManager.isSessionReady()) {
showToast("AR会话未就绪,请等待平面检测")
return
}
// 创建新测量
currentMeasurement = measurementManager.createMeasurement(type)
// 更新UI状态
updateUIForMeasurement(type)
// 显示指导信息
showMeasurementInstructions(type)
}
private fun updateUIForMeasurement(type: MeasurementType) {
// 高亮当前模式按钮
resetButtonColors()
when (type) {
MeasurementType.LENGTH -> binding.btnLength.setBackgroundColor(Color.GREEN)
MeasurementType.AREA -> binding.btnArea.setBackgroundColor(Color.GREEN)
MeasurementType.VOLUME -> binding.btnVolume.setBackgroundColor(Color.GREEN)
MeasurementType.ANGLE -> binding.btnAngle.setBackgroundColor(Color.GREEN)
}
// 显示测量进度
binding.progressBar.visibility = View.VISIBLE
binding.tvInstruction.visibility = View.VISIBLE
}
private fun resetButtonColors() {
val defaultColor = ContextCompat.getColor(this, R.color.button_default)
binding.btnLength.setBackgroundColor(defaultColor)
binding.btnArea.setBackgroundColor(defaultColor)
binding.btnVolume.setBackgroundColor(defaultColor)
binding.btnAngle.setBackgroundColor(defaultColor)
}
private fun handleMeasurementComplete(measurement: Measurement) {
// 应用校准
val calibratedValue = calibrationSystem.applyCalibration(measurement.value)
// 显示结果
showMeasurementResult(measurement.type, calibratedValue)
// 保存到历史
saveMeasurementToHistory(measurement.copy(value = calibratedValue))
// 重置当前测量
currentMeasurement = null
resetUIAfterMeasurement()
}
private fun showMeasurementResult(type: MeasurementType, value: Float) {
val unit = when (type) {
MeasurementType.LENGTH -> "米"
MeasurementType.AREA -> "平方米"
MeasurementType.VOLUME -> "立方米"
MeasurementType.ANGLE -> "°"
}
val message = "${type.displayName}: ${String.format("%.3f", value)} $unit"
AlertDialog.Builder(this)
.setTitle("测量完成")
.setMessage(message)
.setPositiveButton("确定") { _, _ -> }
.setNegativeButton("重新测量") { _, _ ->
startMeasurement(type)
}
.show()
}
private fun showMeasurementInstructions(type: MeasurementType) {
val instructions = when (type) {
MeasurementType.LENGTH -> "请点击起点和终点"
MeasurementType.AREA -> "请点击三个点定义矩形"
MeasurementType.VOLUME -> "请点击四个点定义长方体"
MeasurementType.ANGLE -> "请点击三个点测量角度"
}
binding.tvInstruction.text = instructions
binding.tvInstruction.visibility = View.VISIBLE
}
private fun updateMeasurementProgress(progress: MeasurementProgress) {
binding.progressBar.progress = progress.percentage
binding.tvProgress.text = "进度: ${progress.step}/${progress.totalSteps}"
}
private fun clearCurrentMeasurement() {
measurementManager.clearCurrentMeasurement()
currentMeasurement = null
resetUIAfterMeasurement()
showToast("测量已清除")
}
private fun resetUIAfterMeasurement() {
binding.progressBar.visibility = View.GONE
binding.tvInstruction.visibility = View.GONE
binding.tvProgress.text = ""
resetButtonColors()
}
override fun onResume() {
super.onResume()
arSessionManager.resume()
}
override fun onPause() {
super.onPause()
arSessionManager.pause()
}
override fun onDestroy() {
super.onDestroy()
arSessionManager.destroy()
}
}

总结与展望
通过本文的旅程,我们看到了如何将CameraX的稳健与AR Core的智能相结合,构建一个实用的AR测量应用。关键在于理解坐标转换原理、处理好双框架的生命周期协同,并持续在精度和用户体验上优化。未来,随着深度摄像头(ToF)的普及和AI增强算法的引入,AR测量的精度和易用性将再上新台阶。开发者现在掌握这套技术栈,不仅能打造测量工具,更能为更复杂的AR交互、3D重建等应用打下坚实基础。立即动手,将你的创意融入这虚实结合的世界吧!

posted on 2026-03-05 11:08  mthoutai  阅读(182)  评论(0)    收藏  举报