14、碰撞检测:加速度突变与冲击力估算

说到碰撞检测,很多人的第一反应是游戏里的物理引擎。但在Android开发中,我们用加速度传感器也能做碰撞检测。说白了,就是捕捉加速度的突变。

我记得有一次做运动监测项目,用户把手机摔了,结果数据里出现了一个巨大的尖峰。那一刻我突然意识到——这不就是碰撞信号吗?从那以后,我就开始认真研究怎么用加速度传感器做碰撞检测。

14.1 碰撞的本质:加速度突变

先想一个问题:碰撞发生时,传感器会记录到什么?

答案是——加速度的剧烈变化。正常运动时,加速度变化是平滑的。但碰撞瞬间,加速度会在几毫秒内从几m/s²飙升到几十甚至上百m/s²。

我习惯把这个突变叫做「冲击尖峰」。它的特征很明显:

  • 幅值大:远超正常运动范围
  • 时间短:通常持续10-50毫秒
  • 变化快:斜率极大,几乎是垂直上升

你想想看,手机从1米高度自由落体,撞击地面时的加速度峰值能达到100-200 m/s²。而正常走路时,加速度变化一般不超过±5 m/s²。这个差距,足够我们做检测了。

14.2 碰撞检测的两种思路

实际开发中,我常用两种方法。它们各有适用场景。

方法一:阈值检测法

最简单直接的方法。设定一个阈值,当加速度超过这个值就判定为碰撞。

// 阈值检测法示例
public class CollisionDetector {
    private static final float COLLISION_THRESHOLD = 30.0f; // 30 m/s²
    private static final long MIN_INTERVAL_MS = 200; // 防抖间隔
    
    private long lastCollisionTime = 0;
    
    public boolean detectCollision(SensorEvent event) {
        float x = event.values[0];
        float y = event.values[1];
        float z = event.values[2];
        
        // 计算合加速度
        float magnitude = (float) Math.sqrt(x*x + y*y + z*z);
        
        // 减去重力(约9.8 m/s²)
        float netAccel = magnitude - 9.8f;
        
        long now = System.currentTimeMillis();
        
        if (Math.abs(netAccel) > COLLISION_THRESHOLD 
            && (now - lastCollisionTime) > MIN_INTERVAL_MS) {
            lastCollisionTime = now;
            return true;
        }
        return false;
    }
}
我的经验:阈值不要设得太低。我曾经设了20 m/s²,结果用户用力拍了一下桌子就触发了。建议先收集数据,看看实际场景的峰值范围再定阈值。

方法二:差分检测法

阈值法有个问题——不同场景下,正常运动的峰值可能不一样。比如跑步时加速度变化就比走路大得多。

这时候我更喜欢用差分法。它检测的是加速度的变化率,而不是绝对值。

// 差分检测法示例
public class DifferentialCollisionDetector {
    private float prevMagnitude = 9.8f;
    private static final float DIFF_THRESHOLD = 50.0f; // 变化率阈值
    
    public boolean detectCollision(SensorEvent event) {
        float x = event.values[0];
        float y = event.values[1];
        float z = event.values[2];
        
        float magnitude = (float) Math.sqrt(x*x + y*y + z*z);
        
        // 计算变化率(差分)
        float delta = Math.abs(magnitude - prevMagnitude);
        prevMagnitude = magnitude;
        
        return delta > DIFF_THRESHOLD;
    }
}

差分法的好处是——它不受重力影响,也不受静态加速度干扰。你拿着手机静止不动,突然被撞一下,差分值会很大。而阈值法可能因为重力分量已经接近阈值而误判。

14.3 冲击力估算:从加速度到力

检测到碰撞之后,下一个问题就是——这个碰撞有多猛?

根据牛顿第二定律:F = ma。知道加速度和质量,就能估算冲击力。

但这里有个坑:传感器测的是手机本身的加速度,不是碰撞物体的加速度。而且碰撞时间极短,我们需要的是峰值力。

冲击力估算公式:

F_peak = m × a_peak

其中:

  • m:手机质量(kg),一般0.15-0.2kg
  • a_peak:碰撞瞬间的峰值加速度(m/s²)

举个例子:手机质量0.17kg,碰撞时加速度峰值120 m/s²。

F_peak = 0.17 × 120 = 20.4 N

这个力大概相当于2公斤物体静止时的重力。嗯,不算小。

注意:这个估算很粗糙。实际冲击力还跟碰撞时间、接触面积、材料特性有关。但作为工程估算,它已经够用了。我在做跌落测试时,就用这个公式估算冲击力,跟压力传感器的实测值误差在15%以内。

14.4 碰撞检测的实战要点

做了几个碰撞检测项目后,我总结了一些经验。分享给你:

  1. 采样率要够高:碰撞信号持续时间短,建议用SENSOR_DELAY_FASTEST或至少SENSOR_DELAY_GAME。我一般用200Hz以上。
  2. 低通滤波不能少:原始数据噪声大,先滤波再检测。我用的是移动平均滤波,窗口大小3-5个采样点。
  3. 防抖机制必须有:一次碰撞可能产生多次触发。加个时间窗口,比如200ms内只触发一次。
  4. 区分碰撞类型:跌落、撞击、振动,它们的波形特征不同。我习惯记录碰撞前后的波形,用模式识别做分类。

14.5 碰撞检测的完整流程

下面这张图是我做碰撞检测时的标准流程。你可以参考一下:

碰撞检测完整流程 ① 数据采集 200Hz以上采样率 ② 预处理 低通滤波+去重力 ③ 碰撞检测 阈值法/差分法 ④ 防抖 200ms窗口 是否碰撞? 继续采集 ⑤ 冲击力估算 F = m × a_peak ⑥ 记录/报警 保存波形+触发动作 整个流程从采集到输出,延迟控制在50ms以内

14.6 一个完整的碰撞检测实现

最后,我把上面讲的东西整合成一个完整的实现。这个类我在项目里用过,效果还不错。

public class CollisionAnalyzer implements SensorEventListener {
    private static final float GRAVITY = 9.8f;
    private static final float COLLISION_THRESHOLD = 35.0f;
    private static final long DEBOUNCE_MS = 300;
    private static final int FILTER_WINDOW = 5;
    
    private float[] buffer = new float[FILTER_WINDOW];
    private int bufferIndex = 0;
    private long lastCollisionTime = 0;
    private float phoneMass = 0.17f; // kg
    
    private OnCollisionListener listener;
    
    public interface OnCollisionListener {
        void onCollision(float peakAccel, float estimatedForce, long timestamp);
    }
    
    @Override
    public void onSensorChanged(SensorEvent event) {
        float x = event.values[0];
        float y = event.values[1];
        float z = event.values[2];
        
        // 计算合加速度
        float rawMagnitude = (float) Math.sqrt(x*x + y*y + z*z);
        
        // 移动平均滤波
        buffer[bufferIndex % FILTER_WINDOW] = rawMagnitude;
        bufferIndex++;
        
        float sum = 0;
        int count = Math.min(bufferIndex, FILTER_WINDOW);
        for (int i = 0; i < count; i++) {
            sum += buffer[i];
        }
        float filtered = sum / count;
        
        // 减去重力
        float netAccel = Math.abs(filtered - GRAVITY);
        
        long now = System.currentTimeMillis();
        
        // 碰撞检测
        if (netAccel > COLLISION_THRESHOLD 
            && (now - lastCollisionTime) > DEBOUNCE_MS) {
            
            lastCollisionTime = now;
            
            // 估算冲击力
            float force = phoneMass * netAccel;
            
            if (listener != null) {
                listener.onCollision(netAccel, force, now);
            }
        }
    }
    
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // 精度变化时可以做些处理
    }
    
    public void setOnCollisionListener(OnCollisionListener listener) {
        this.listener = listener;
    }
}
使用建议:这个类可以直接用在运动监测、跌落检测、碰撞计数等场景。记得在Activity的onResume中注册传感器,onPause中注销。另外,阈值可以根据实际测试数据调整,不要照搬我的数值。

好了,碰撞检测的核心内容就这些。从检测原理到冲击力估算,再到完整实现,你应该能上手了。记住一点——传感器数据永远是脏的,滤波和防抖是必不可少的。我在早期项目里吃过这个亏,数据没处理好,误报率高达30%。后来加了滤波和防抖,误报率降到了5%以下。

做碰撞检测,耐心比技巧更重要。多收集数据,多调参数,你也能做出稳定的检测系统。