跳到主要内容
AR Core 与 CameraX 融合:测量应用原理与实现 | 极客日志
Kotlin AI 大前端 算法
AR Core 与 CameraX 融合:测量应用原理与实现 AR 测量技术结合手机摄像头与 AR 引擎实现非接触式测量。基于 AR Core 和 CameraX 框架,详解了从环境搭建、相机初始化、坐标转换算法到 UI 交互的完整实现流程。涵盖长度、面积、体积及角度测量功能,包含精度优化校准系统及性能监控方案。通过光线投射与平面检测技术,解决了 2D 屏幕坐标至 3D 世界坐标的映射难题,并提供多帧平均与卡尔曼滤波等误差修正策略,最终构建出具备实用价值的 AR 测量应用架构。
漫步 发布于 2026/4/8 更新于 2026/7/21 45 浏览引言
在现实世界中,我们经常需要测量物体的大小。通过手机摄像头和 AR 技术,可以实现所见即所得的智能测量。本文将阐述 AR 测量的核心原理,并实现一个完整的 AR 测量应用。
技术基础 - 理解 AR 测量的核心原理
AR 测量技术栈架构
┌─────────────────────────────────────┐
│ 用户界面与交互层 │
│ 测量标注、手势识别、结果展示 │
├─────────────────────────────────────┤
│ AR 引擎层 (AR Core) │
│ 运动跟踪、环境理解、光照估计、点云生成 │
├─────────────────────────────────────┤
│ 相机控制层 (CameraX) │
│ 图像采集、实时预览、图像分析、自动对焦 │
├─────────────────────────────────────┤
│ 传感器融合层 │
│ 陀螺仪、加速度计、磁力计、深度传感器 │
├─────────────────────────────────────┤
│ 计算机视觉算法层 │
│ 特征点检测、平面检测、距离计算、3 D 重建 │
└─────────────────────────────────────┘
AR 测量与普通测量的对比
测量方式 传统卷尺 激光测距仪 AR 测量 精度 ±1-2mm ±1-2mm ±2-5cm 测量范围 0-10m 0-100m 0-10m 操作难度 中等 简单 非常简单 功能扩展 单一 单一 长度、面积、体积、角度 环境要求 无 需要反射面 需要纹理丰富的平面 成本 低 中等 只需手机
AR 测量的核心挑战与解决方案
挑战 1:如何将 2D 屏幕坐标转换为 3D 世界坐标?
解决方案:光线投射(Ray Casting) + 平面检测
挑战 2:如何保证测量的准确性?
解决方案:多帧优化 + 传感器校准 + 环境光补偿
挑战 3:如何处理动态环境(如光照变化)?
环境搭建与基础配置
项目依赖配置
// 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'
}
权限与特性声明
<manifest xmlns:android ="http://schemas.android.com/apk/res/android"
package ="com.example.armeasure" >
<uses-permission android:name ="android.permission.CAMERA" />
<uses-permission android:name ="android.permission.INTERNET" />
<uses-permission android:name ="android.permission.ACCESS_NETWORK_STATE" />
<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" >
<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
android:name =".MeasureActivity"
android:configChanges ="orientation|screenSize|keyboardHidden"
android:exported ="false"
android:screenOrientation ="portrait"
android:theme ="@style/Theme.ARMeasure.Fullscreen" />
<meta-data android:name ="com.google.ar.core" android:value ="required" />
<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 的协同工作
CameraX 相机初始化 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
)
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
}
private fun setupAutoFocus () {
camera?.cameraControl?.setLinearFocus(0f )
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 -> {
}
}
}
}
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 会话管理 class ARSessionManager (
private val context: Context,
private val arSceneView: ArSceneView
) {
private var arSession: Session? = null
private var arConfig: Config? = null
private var isSessionCreated = false
enum class SessionState {
NOT_INITIALIZED, INITIALIZING, TRACKING, PAUSED, STOPPED, ERROR
}
private var currentState = SessionState.NOT_INITIALIZED
fun createARSession () : SessionState {
if (isSessionCreated) return currentState
try {
currentState = SessionState.INITIALIZING
val availability = ArCoreApk.getInstance().checkAvailability(context)
if (!availability.isSupported) {
throw ARNotSupportedException("设备不支持 AR Core" )
}
if (availability.isTransient) {
ArCoreApk.getInstance().requestInstall(context, true )
}
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)
}
arSceneView.setupSession(arSession!!)
isSessionCreated = true
currentState = SessionState.TRACKING
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)
}
}
}
}
}
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
}
}
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)
}
fun pause () {
arSession?.pause()
currentState = SessionState.PAUSED
}
fun resume () {
arSession?.resume()
currentState = SessionState.TRACKING
}
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 测量核心算法实现
屏幕到世界坐标转换算法 class CoordinateTransformer (private val cameraIntrinsics: ARCameraManager.CameraIntrinsics) {
fun screenToNDC (screenX: Float , screenY: Float , screenWidth: Int , screenHeight: Int ) : Pair<Float , Float > {
val ndcX = (2.0f * screenX / screenWidth) - 1.0f
val ndcY = 1.0f - (2.0f * screenY / screenHeight)
return Pair(ndcX, ndcY)
}
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
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 {
val (ndcX, ndcY) = screenToNDC(screenX, screenY, screenWidth, screenHeight)
var cameraPoint = ndcToCamera(ndcX, ndcY)
hitDepth?.let { cameraPoint = cameraPoint.normalize().multiply(it) }
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
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 {
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
)
}
}
多点测量与几何计算 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
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.01 f
) : 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()
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)
}
}
用户界面与交互设计
测量界面实现 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()
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)
arSceneView.apply {
planeRenderer.isVisible = true
planeRenderer.isShadowReceiver = true
planeRenderer.material.setFloat3("color" , Color.colorToFloatArray(Color.argb(100 , 0 , 120 , 255 )))
}
}
private fun initARComponents () {
arCameraManager = ARCameraManager(context = this , surfaceProvider = cameraPreviewView.surfaceProvider)
arSessionManager = ARSessionManager(this , arSceneView)
arCameraManager.getCameraIntrinsics()?.let { intrinsics ->
coordinateTransformer = CoordinateTransformer(intrinsics)
}
arSessionManager.onPlaneTapped = { hitResult, plane, motionEvent ->
handlePlaneTap(hitResult, plane, motionEvent)
}
arSessionManager.onPlaneUpdated = { plane ->
updatePlaneVisualization(plane)
}
}
private fun setupInteractionListeners () {
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)
}
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,
measurementPoints[2 ].worldPosition
)
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
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.0 f
) : 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 {
val movementCompensation = calculateMovementCompensation(gyroData, accelerometerData)
val orientationCompensation = calculateOrientationCompensation(accelerometerData, magneticData)
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 ),
"信用卡" to Pair(0.0856f , 0.0539f ),
"iPhone 14" to Pair(0.1467f , 0.0715f )
)
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)
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 )
val calibrationTime = sharedPrefs.getLong("calibration_time" , 0L )
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.0 f,
confidence: Float = 0 f,
expectedLength: Float = 0 f,
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
)
高级功能扩展
面积与体积测量 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.01 f) : 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 >,
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()
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 = 0 f,
height: Float = 0 f,
surfaceArea: Float = 0 f,
shapeType: String ,
topArea: Float = 0 f,
avgHeight: Float = 0 f
) : 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)
}
}
}
测量历史与数据管理 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
)
性能优化与用户体验
实时性能监控 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
fun startMonitoring () {
isMonitoring = true
Thread {
while (isMonitoring) {
monitorFrameRate()
monitorMemoryUsage()
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
}
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 () {
}
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 }
测试与验证
测量准确性测试 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 ->
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
)
val result = testKnownObject(referenceObject, distance, numberOfTrials)
resultsByDistance[distance] = result
}
val distanceErrorPairs = resultsByDistance.map { (distance, result) -> Pair(distance, result.averageError) }
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
val systematicError = 0.02f
return actualLength + systematicError + randomError
}
private fun simulateMeasurementWithCondition (actualLength: Float , condition: TestCondition ) : Float {
var measurement = simulateMeasurement(actualLength)
when (condition.lighting) {
LightingCondition.LOW -> measurement += 0.05f
LightingCondition.HIGH -> measurement += 0.03f
else -> {}
}
when (condition.surface) {
SurfaceType.SMOOTH -> measurement += 0.08f
else -> {}
}
when (condition.stability) {
Stability.MOVING -> measurement += 0.10f
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 )
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
) {
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 }
结语 AR 测量技术正在快速发展,从简单的长度测量到复杂的 3D 重建,从消费级应用到工业级解决方案。通过结合 CameraX 的高质量图像采集和 AR Core 的环境理解能力,能够创建出真正实用的测量工具。
精度提升 - 深度传感器和 AI 算法的结合将极大提高测量精度
实时性增强 - 5G 和边缘计算将实现实时的大范围测量
多设备协同 - 多台设备协同工作,实现更复杂的测量任务
行业融合 - 与 BIM、CAD 等专业软件的无缝对接
关注用户体验 - 技术再先进,如果用户不会用也是徒劳
持续优化精度 - 测量工具的核心价值在于准确性
考虑实际场景 - 在真实环境中测试和优化
保持开放心态 - AR 技术仍在快速发展,保持学习和适应
通过本文的完整实现方案,已经掌握了 AR 测量应用的核心技术。现在,是时候动手实践,创造出属于自己的 AR 测量应用了!
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
随机西班牙地址生成器 随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online