95、iOS 设备运动与加速度传感器的应用详解

iOS 设备运动与加速度传感器的应用详解

1. 航向与路线的区别

在设备的运动监测中,航向(Heading)和路线(Course)是两个不同的概念。例如,一艘船可能正朝向北方(航向),但实际上却在向东北方向移动(路线)。在某些情况下,我们更关心的是路线。比如在行驶的汽车中,用户握持设备的方式通常并不重要,我们询问航向时,实际上想知道的是汽车的行驶方向。如果系统根据设备的运动情况判断,当我们请求航向时可能想要的是路线,那么它会将路线作为航向提供。

若这并非我们所需,那么可以不使用 Core Location 来确定航向,而是使用 Core Motion 框架,通过获取 CMDeviceMotion 对象的 heading 属性来得到设备在空间中的方向。

2. 加速度、姿态与活动监测

加速度是由于对设备施加力而产生的,通过设备的加速度计来检测,如果设备有陀螺仪,也会辅助检测。重力也是一种力,所以即使用户没有有意识地对设备施加力,加速度计也总有数据可测,设备可以利用加速度检测来报告其相对于垂直方向的姿态。

加速度信息的获取方式有两种:
- 预封装的 UIEvent :可以接收一个 UIEvent,它会通知我们设备因加速而执行的预定义手势。目前,这种手势只有用户摇晃设备这一种。
- 使用 Core Motion 框架 :实例化 CMMotionManager,然后获取所需类型的信息。可以请求加速度计信息、陀螺仪信息或设备运动信息(还可以使用 Core Motion 来获取磁力计和航向信息),设备运动信息结合了陀螺仪数据和其他传感器的数据,能最好地描述设备在空间中的姿态。

3. 摇晃事件的处理

摇晃事件是一种 UIEvent。要接收摇晃事件,应用程序中必须有一个 UIResponder,它需要满足以下条件:
- 从 canBecomeFirstResponder 返回 true
- 实际上是第一响应者。

这个响应者或响应者链中更上层的 UIResponder 应该实现以下部分或全部方法:
- motionBegan(_:with:) :表示可能开始了一个摇晃动作,但不一定最终会成为摇晃。
- motionEnded(_:with:) :表示 motionBegan 中报告的动作结束,且最终确定为摇晃。
- motionCancelled(_:with:) :表示 motionBegan 中报告的动作最终不是摇晃。

通常,实现 motionEnded(_:with:) 就足够了,因为只有当用户执行摇晃手势时才会触发该方法。其第一个参数是事件子类型,但可以保证是 .motionShake ,所以对其进行测试没有意义。

以下是一个简单的实现示例:

override var canBecomeFirstResponder : Bool {
    return true
}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.becomeFirstResponder()
}
override func motionEnded(_ motion: UIEvent.EventSubtype, with e: UIEvent?) {
    print("hey, you shook me!")
}

为了避免抢夺视图中其他响应者的撤销功能,可以在 motionEnded 方法中进行判断:

override func motionEnded(_ motion: UIEvent.EventSubtype, with e: UIEvent?) {
    if self.isFirstResponder {
        print("hey, you shook me!")
    } else {
        super.motionEnded(motion, with: e)
    }
}
4. 使用 Core Motion 框架

使用 Core Motion 框架( import CoreMotion )读取加速度计、磁力计、陀螺仪和组合设备运动信息的标准模式在某些方面与使用 Core Location 类似,具体步骤如下:
1. 实例化 CMMotionManager ,通常将其作为实例属性保留。可以直接初始化该属性:

let motman = CMMotionManager()
  1. 通过检查相应的实例属性(如 isAccelerometerAvailable )来确认所需硬件是否可用。
  2. 通过设置相应的属性(如 accelerometerUpdateInterval )来设置运动管理器更新新传感器读数的间隔。
  3. 调用相应的启动方法(如 startAccelerometerUpdates )。
  4. 运动管理器没有委托,有两种获取数据的方式:
    • 拉取(Pull) :在需要数据时轮询运动管理器,请求相应的数据属性。轮询间隔不必与运动管理器的更新间隔相同,轮询时将获取运动管理器的当前数据,即其最近一次更新生成的数据。
    • 推送(Push) :如果目的是收集所有数据,可以调用一个相关方法,该方法接受一个回调函数,最好在由 OperationQueue 管理的后台线程上调用。方法名的形式为 start...Updates(to:withHandler:) 。例如,对于加速度计更新,应调用 startAccelerometerUpdates(to:withHandler:)
  5. 当不再需要数据时,别忘了调用相应的停止方法(如 stopAccelerometerUpdates )。

以下是使用 Core Motion 框架的流程图:

graph TD;
    A[实例化 CMMotionManager] --> B[检查硬件可用性];
    B --> C[设置更新间隔];
    C --> D[调用启动方法];
    D --> E{选择获取数据方式};
    E -->|拉取| F[轮询运动管理器];
    E -->|推送| G[设置回调函数];
    F --> H[获取当前数据];
    G --> I[接收回调数据];
    H --> J{是否需要更多数据};
    I --> J;
    J -->|是| F;
    J -->|否| K[调用停止方法];
5. 原始加速度分析

如果设备只有加速度计而没有陀螺仪,虽然可以了解施加在设备上的力,但需要做出一些妥协。主要问题是,即使设备完全静止,其加速度值也会构成一个指向地球中心的归一化向量,即重力。因此,加速度计不断报告的是重力和用户引起的加速度的组合。

这种情况有好处也有坏处:好处是在一定限制下,可以使用加速度计检测设备在空间中的姿态;坏处是重力值和用户引起的加速度值混合在一起。不过,可以通过数学方法分离这些值:
- 低通滤波器 :低通滤波器会抑制用户加速度,只报告重力。
- 高通滤波器 :高通滤波器会抑制重力的影响,只检测用户加速度,将静止的设备报告为加速度为零。

在某些情况下,可能需要同时应用低通滤波器和高通滤波器,以了解重力值和用户加速度值。常见的额外技术是将高通滤波器的输出再通过低通滤波器,以减少噪声和小抖动。

以下是一个简单的示例,用于判断设备是否平放在背面:

guard self.motman.isAccelerometerAvailable else { return }
self.motman.accelerometerUpdateInterval = 1.0 / 30.0
self.motman.startAccelerometerUpdates()
self.timer = Timer.scheduledTimer(
    timeInterval:self.motman.accelerometerUpdateInterval,
    target: self, selector: #selector(pollAccel),
    userInfo: nil, repeats: true)

@objc func pollAccel() {
    guard let data = self.motman.accelerometerData else {return}
    let acc = data.acceleration
    let x = acc.x
    let y = acc.y
    let z = acc.z
    let accu = 0.08
    if abs(x) < accu && abs(y) < accu && z < -0.5 {
        if self.state == .unknown || self.state == .notLyingDown {
            self.state = .lyingDown
            self.label.text = "I'm lying on my back... ahhh..."
        }
    } else {
        if self.state == .unknown || self.state == .lyingDown {
            self.state = .notLyingDown
            self.label.text = "Hey, put me back down on the table!"
        }
    }
}

为了减少对设备在桌子上微小移动的敏感性,可以使用低通滤波器:

func add(acceleration accel:CMAcceleration) {
    let alpha = 0.1
    self.oldX = accel.x * alpha + self.oldX * (1.0 - alpha)
    self.oldY = accel.y * alpha + self.oldY * (1.0 - alpha)
    self.oldZ = accel.z * alpha + self.oldZ * (1.0 - alpha)
}

@objc func pollAccel() {
    guard let data = self.motman.accelerometerData else {return}
    self.add(acceleration: data.acceleration)
    let x = self.oldX
    let y = self.oldY
    let z = self.oldZ
    // ... 后续代码与之前相同 ...
}

也可以使用推送方式获取加速度数据:

self.motman.startAccelerometerUpdates(to: .main) { data, err in
    guard let data = data else {
        print(err)
        self.stopAccelerometer()
        return
    }
    self.receive(acceleration:data)
}

func receive(acceleration data:CMAccelerometerData) {
    self.add(acceleration: data.acceleration)
    let x = self.oldX
    let y = self.oldY
    let z = self.oldZ
    // ... 后续代码与之前相同 ...
}

还有一个检测设备侧面拍打手势的示例,使用高通滤波器消除重力影响:

func add(acceleration accel:CMAcceleration) {
    let alpha = 0.1
    self.oldX = accel.x - ((accel.x * alpha) + (self.oldX * (1.0 - alpha)))
    self.oldY = accel.y - ((accel.y * alpha) + (self.oldY * (1.0 - alpha)))
    self.oldZ = accel.z - ((accel.z * alpha) + (self.oldZ * (1.0 - alpha)))
}

@objc func pollAccel(_: Any) {
    guard let data = self.motman.accelerometerData else {return}
    self.add(acceleration: data.acceleration)
    let x = self.oldX
    let thresh = 1.0
    if x < -thresh {
        if data.timestamp - self.oldTime > 0.5 || self.lastSlap == .right {
            self.oldTime = data.timestamp
            self.lastSlap = .left
            self.canceltimer?.invalidate()
            self.canceltimer = .scheduledTimer(
                withTimeInterval:0.5, repeats: false) { _ in
                    print("left")
            }
        }
    } else if x > thresh {
        if data.timestamp - self.oldTime > 0.5 || self.lastSlap == .left {
            self.oldTime = data.timestamp
            self.lastSlap = .right
            self.canceltimer?.invalidate()
            self.canceltimer = .scheduledTimer(
                withTimeInterval:0.5, repeats: false) { _ in
                    print("right")
            }
        }
    }
}

这个手势检测有点棘手,用户必须将设备拍进张开的手中并握住,否则可能会导致错误报告,而且手势检测的延迟较高。可以通过调整代码中的一些参数来提高准确性和性能,更复杂的分析可能需要将最近的 CMAccelerometerData 对象存储在循环缓冲区中,并研究缓冲区内容以确定总体趋势。

iOS 设备运动与加速度传感器的应用详解

6. 陀螺仪的作用

一些设备配备了电子陀螺仪,这对重力和姿态报告的准确性和速度有很大影响。陀螺仪的姿态在空间中保持恒定,因此可以检测到设备姿态的任何变化,这对加速度计测量有两个重要影响:
- 加速度计可以由陀螺仪辅助,从而快速检测出重力和用户引起的加速度之间的差异。
- 陀螺仪可以检测纯旋转,在这种情况下几乎没有加速度,加速度计可能无法提供有用信息。极端情况是绕重力轴的恒定姿态旋转,仅靠加速度计完全无法检测到(因为没有用户引起的力,且重力保持不变)。

可以跟踪原始陀螺仪数据,确保设备有陀螺仪( isGyroAvailable ),然后调用 startGyroUpdates 。从运动管理器获取的是 CMGyroData 对象,它结合了时间戳和 CMRotationRate ,报告绕每个轴的旋转速率,单位是弧度每秒,正数值表示从正轴方向看为逆时针旋转。但陀螺仪值存在缩放和偏差问题,即值基于任意比例,且随时间以大致恒定的速率逐渐增加(或减少),因此处理原始陀螺仪数据意义不大。

通常,我们更感兴趣的是至少结合陀螺仪和加速度计的数据。Core Motion 会将这些传感器的数据计算组合成一个设备运动实例( CMDeviceMotion ),并消除传感器的内部偏差和缩放影响。

7. CMDeviceMotion 详解

CMDeviceMotion 包含以下属性,这些属性对应设备的自然 3D 框架(x 轴向右增加,y 轴向上增加,z 轴从屏幕前方增加):
| 属性 | 描述 |
| ---- | ---- |
| gravity | 一个 CMAcceleration ,表示一个指向地球中心、值为 1 的向量,单位是 G。 |
| userAcceleration | 一个 CMAcceleration ,描述用户引起的加速度,不包含重力分量,单位是 G。 |
| rotationRate | 一个 CMRotationRate ,描述设备绕自身中心的旋转速率,本质上是 CMGyroData rotationRate 并考虑了缩放和偏差。 |
| magneticField | 一个 CMCalibratedMagneticField ,描述作用在设备上的磁力,单位是微特斯拉。传感器的内部偏差已被消除,精度有以下几种( CMMagneticFieldCalibrationAccuracy ):
- .uncalibrated
- .low
- .medium
- .high |
| attitude | 一个 CMAttitude ,描述设备在空间中的瞬时姿态。姿态是相对于一个参考框架( CMAttitudeReferenceFrame )测量的,在请求运动管理器开始生成更新时指定,需要先调用类方法 availableAttitudeReferenceFrames 确认所需参考框架在该设备上可用。不同参考框架的区别在于 x 轴的指向(y 轴与其他两轴正交):
- .xArbitraryZVertical :x 轴可以指向任意方向。
- .xArbitraryCorrectedZVertical :与上一个选项相同,但使用磁力计保持准确性(防止参考框架随时间漂移)。
- .xMagneticNorthZVertical :x 轴指向磁北。
- .xTrueNorthZVertical :x 轴指向真北。除非同时使用 Core Location 获取设备位置,否则该值可能不准确。 |
| heading | 一个 Double ,表示设备的方向,是从北开始顺时针的度数(不是弧度),参考框架必须是 .xMagneticNorthZVertical .xTrueNorthZVertical (否则值为 -1)。与 Core Location 的 CLHeading 不同,它是纯方向读数,不包含路线信息。不仅使用了磁力计,还使用了加速度计和陀螺仪,有助于消除局部磁异常引起的误差。 |

CMAttitude 的值可以通过以下几种属性访问,每种属性对应不同的系统,适用于不同的目的:
- pitch, roll, yaw :设备相对于参考框架绕自然 x 轴、y 轴和 z 轴的偏移角度,单位是弧度(也称为欧拉角)。
- rotationMatrix :一个 CMRotationMatrix 结构体,包含一个 3×3 矩阵,表示在参考框架中的旋转。
- quaternion :一个 CMQuaternion ,描述姿态。四元数常用于 3D 场景,如 SceneKit 和 Metal。

8. 利用 CMDeviceMotion 实现简单指南针/测斜仪

将设备变成一个简单的指南针/测斜仪,只需请求相对于磁北的姿态,并获取其 pitch、roll 和 yaw。具体步骤如下:
1. 确认设备支持设备运动监测:

guard self.motman.isDeviceMotionAvailable else { return }
  1. 选择参考框架并确认其可用性:
let r = CMAttitudeReferenceFrame.xMagneticNorthZVertical
guard CMMotionManager.availableAttitudeReferenceFrames().contains(r) else {
    return
}
  1. 设置运动管理器的属性:
self.motman.showsDeviceMovementDisplay = true
self.motman.deviceMotionUpdateInterval = 1.0 / 30.0
  1. 启动设备运动更新:
self.motman.startDeviceMotionUpdates(using: r)
  1. 设置定时器进行轮询:
let t = self.motman.deviceMotionUpdateInterval * 10
self.timer = Timer.scheduledTimer(timeInterval:t,
    target:self, selector:#selector(pollAttitude),
    userInfo:nil, repeats:true)
  1. pollAttitude 方法中获取姿态信息:
@objc func pollAttitude() {
    guard let mot = self.motman.deviceMotion else {return}
    let acc = mot.magneticField.accuracy.rawValue
    if acc <= CMMagneticFieldCalibrationAccuracy.low.rawValue {
        return // 磁力计未准备好
    }
    let att = mot.attitude
    let to_deg = 180.0 / .pi
    print("\(att.pitch * to_deg), \(att.roll * to_deg), \(att.yaw * to_deg)")
    let g = mot.gravity
    let whichway = g.z > 0 ? "forward" : "back"
    print("pitch is tilted \(whichway)")
}

当设备水平(平放在背面)且 x 轴(右边缘)指向磁北时,这些值都接近零,设备相对于正轴逆时针旋转时,每个值都会增加。例如,设备直立(顶部指向天空)时,pitch 接近 90;设备右侧边缘着地时,roll 接近 90;设备平放在背面且顶部指向北时,yaw 接近 -90。

需要注意的是,欧拉角在数学上存在一些问题:
- roll 和 yaw 从 0 到 π(180 度)逆时针旋转增加,然后跳到 -π(-180 度),继续增加到 0;而 pitch 增加到 π/2(90 度),然后减小到 0,再减小到 -π/2(-90 度),最后增加到 0。这意味着仅通过 pitch、roll 和 yaw 探索姿态时,姿态信息可能不足以描述设备的姿态,需要结合重力的 z 分量来区分不同情况。
- 在某些方向上,值会变得不准确。特别是当 pitch 接近 ±90 度(设备直立或倒置)时,roll 和 yaw 会变得不稳定,这种现象被称为“奇点”或“万向节锁”。可以使用不同的姿态表示方式,如 rotationMatrix 来解决这个问题。

9. 利用 CMAttitude 的 rotationMatrix 实现图层旋转

以下示例展示了如何使用 CMAttitude rotationMatrix 属性使 CALayer 响应设备的当前姿态进行旋转:
1. 进行常规准备,选择参考框架为 .xArbitraryZVertical

guard self.motman.isDeviceMotionAvailable else { return }
let r = CMAttitudeReferenceFrame.xArbitraryZVertical
guard CMMotionManager.availableAttitudeReferenceFrames().contains(r) else {
    return
}
self.motman.deviceMotionUpdateInterval = 1.0 / 30.0
self.motman.startDeviceMotionUpdates(using: r)
let t = self.motman.deviceMotionUpdateInterval * 10
self.timer = Timer.scheduledTimer(timeInterval:t,
    target:self, selector:#selector(pollAttitude),
    userInfo:nil, repeats:true)
  1. pollAttitude 方法中存储初始姿态并进行姿态转换:
@objc func pollAttitude() {
    guard let mot = self.motman.deviceMotion else {return}
    let att = mot.attitude
    if self.ref == nil {
        self.ref = att
        return
    }
    att.multiply(byInverseOf: self.ref)
    let r = att.rotationMatrix
    var t = CATransform3DIdentity
    t.m11 = CGFloat(r.m11)
    t.m12 = CGFloat(r.m12)
    t.m13 = CGFloat(r.m13)
    t.m21 = CGFloat(r.m21)
    t.m22 = CGFloat(r.m22)
    t.m23 = CGFloat(r.m23)
    t.m31 = CGFloat(r.m31)
    t.m32 = CGFloat(r.m32)
    t.m33 = CGFloat(r.m33)
    let lay = // 获取相应的 CALayer
    CATransaction.setAnimationDuration(1.0/10.0)
    lay.transform = t
}

这个示例中,图层会随着设备旋转而尝试保持静止。但需要注意的是,随着时间推移,变换可能会出现漂移,即使设备静止,图层也会逐渐旋转。 .xArbitraryCorrectedZVertical 参考框架就是为解决这个问题而设计的。

综上所述,通过合理利用 iOS 设备的加速度计、陀螺仪和磁力计等传感器,结合 Core Motion 框架,我们可以实现多种有趣且实用的功能,如姿态检测、手势识别、指南针和测斜仪等。在实际应用中,需要根据具体需求选择合适的传感器和数据处理方法,并注意处理传感器数据中的各种问题,以确保功能的准确性和稳定性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值