引言:AR测量如何改变我们对现实的认知
在现实世界中,我们经常需要测量物体的大小:装修时需要量家具尺寸,购物时需要知道包装大小,工作中需要精确测量距离。传统的测量工具(卷尺、激光测距仪)都有局限性。现在,通过手机摄像头和AR技术,我们可以实现"所见即所得"的智能测量。本文将带你深入理解AR测量的原理,并实现一个完整的AR测量应用。
第一章:技术基础 - 理解AR测量的核心原理
1.1 AR测量技术栈架构
text
┌─────────────────────────────────────┐
│ 用户界面与交互层 │
│ 测量标注、手势识别、结果展示 │
├─────────────────────────────────────┤
│ AR引擎层 (AR Core) │
│ 运动跟踪│环境理解│光照估计│点云生成 │
├─────────────────────────────────────┤
│ 相机控制层 (CameraX) │
│ 图像采集│实时预览│图像分析│自动对焦 │
├─────────────────────────────────────┤
│ 传感器融合层 │
│ 陀螺仪│加速度计│磁力计│深度传感器 │
├─────────────────────────────────────┤
│ 计算机视觉算法层 │
│ 特征点检测│平面检测│距离计算│3D重建 │
└─────────────────────────────────────┘
1.2 AR测量与普通测量的对比
| 测量方式 | 传统卷尺 | 激光测距仪 | AR测量 |
|---|---|---|---|
| 精度 | ±1-2mm | ±1-2mm | ±2-5cm |
| 测量范围 | 0-10m | 0-100m | 0-10m |
| 操作难度 | 中等 | 简单 | 非常简单 |
| 功能扩展 | 单一 | 单一 | 长度、面积、体积、角度 |
| 环境要求 | 无 | 需要反射面 | 需要纹理丰富的平面 |
| 成本 | 低 | 中等 | 只需手机 |
1.3 AR测量的核心挑战与解决方案
挑战1:如何将2D屏幕坐标转换为3D世界坐标?
- 解决方案:光线投射(Ray Casting) + 平面检测
挑战2:如何保证测量的准确性?
- 解决方案:多帧优化 + 传感器校准 + 环境光补偿
挑战3:如何处理动态环境(如光照变化)?
- 解决方案:自适应特征点跟踪 + 实时重定位
第二章:环境搭建与基础配置
2.1 项目依赖配置
gradle
// 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'
}
2.2 权限与特性声明
xml
<!-- 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的协同工作
3.1 CameraX相机初始化
kotlin
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)
}
}
}
3.2 AR Core会话管理
kotlin
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测量核心算法实现
4.1 屏幕到世界坐标转换算法
kotlin
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
)
}
}
4.2 多点测量与几何计算
kotlin
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)
}
}
}
第五章:用户界面与交互设计
5.1 测量界面实现
kotlin
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
)
}
5.2 测量标注视图
kotlin
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)
}
第六章:精度优化与校准
6.1 测量精度优化算法
kotlin
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)
}
6.2 校准系统实现
kotlin
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
)
}
第七章:高级功能扩展
7.1 面积与体积测量
kotlin
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
)
}
}
}
}
7.2 测量历史与数据管理
kotlin
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
)
}
第八章:性能优化与用户体验
8.1 实时性能监控
kotlin
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 }
}
第九章:测试与验证
9.1 测量准确性测试
kotlin
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 }
}
第十章:完整应用集成
10.1 应用主界面集成
kotlin
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()
}
}
结语:AR测量的未来展望
AR测量技术正在快速发展,从简单的长度测量到复杂的3D重建,从消费级应用到工业级解决方案。通过结合CameraX的高质量图像采集和AR Core的环境理解能力,我们能够创建出真正实用的测量工具。
技术发展趋势:
- 精度提升 - 深度传感器和AI算法的结合将极大提高测量精度
- 实时性增强 - 5G和边缘计算将实现实时的大范围测量
- 多设备协同 - 多台设备协同工作,实现更复杂的测量任务
- 行业融合 - 与BIM、CAD等专业软件的无缝对接
给开发者的建议:
- 关注用户体验 - 技术再先进,如果用户不会用也是徒劳
- 持续优化精度 - 测量工具的核心价值在于准确性
- 考虑实际场景 - 在真实环境中测试和优化
- 保持开放心态 - AR技术仍在快速发展,保持学习和适应
通过本文的完整实现方案,你已经掌握了AR测量应用的核心技术。现在,是时候动手实践,创造出属于你自己的AR测量应用了!
资源链接:
问题反馈 :
如果您在实现过程中遇到任何问题,或者有改进建议,欢迎在评论区交流哦。
代码示例可自由使用,但需保留署名哈。