室內定位的解決方案有很多種,但是由於信標(Beacons)的信號是不斷變化的,所以想要獲取准確的定位信息減小誤差,最有效的方式就是盡可能多的鋪設信標,除此之外就是要有行之有效的算法。室內定位最常用的就是指紋法和三邊測距法:指紋法就是在信標覆蓋范圍內獲取多個信標的信號強度進行匹配,三邊測距法則是通過三個信號點(Beacons)和分別對應的距離,形成三個圓並相交於一點。
本篇主要講述三邊測距法的進一步優化,因為三邊測距法在實際情況中並沒有那么理想化,有可能會出現兩圓不相交、圓包含圓、只有兩個信號點或者多個信號點排成一列的情況(過道里),這都是一些比較常見的場景。所以我們需要一個能同時解決上面這些問題的計算方法--分步定位。
分步定位
在iOS開發中,使用CLLocationManager
的startRangingBeaconsSatisfyingContraint:
方法監聽Beacons,並通過代理回調中獲得Beacons列表。取出rssi
信號值最強的三個點,取accuracy
值作為圓半徑(需要減去高度差),用major
、minor
值從后台返回的數據中取出對應的坐標點數據即為三個圓的圓心。

不相交時,按比例取中點(和
)。當兩圓相交時,就是拆分成幾個三角形,通過一系列三級函數計算出未知的兩個交點。最后將三點連成三角形,此三角形的重心(即點M)就是最終定位點,步驟如下:
- 通過勾股定律用a、b長度計算出線段AB長度(即點A到點B距離),使用 ra + rb 與AB對比即可得知兩圓的對應情況,一共有三種情況:兩圓相離ra + rb < AB、兩圓相切ra + rb == AB、兩圓相交ra + rb > AB。
- 兩圓相離:按照兩圓半徑的比例在線段AZ上求
點,即
;因為“兩圓相切ra + rb == AB”在實際程序中出現的幾率太小,所以直接使用“兩圓相離”相同的求法。
- 兩圓相交:求出相交點C的坐標 {Cx, Cy},可通過
得出Q1,通過
得出Q2,最后計算出點C的坐標:
;
,同理可求出點D的坐標。得到C、D兩交點后取距離圓心Z點近的交點作為最后三個參考點中的一點。
- 將最后求得的三個參考點連接成一個三角形,該三角形的重心即為最后的定位點M:
;
采用分步定位法測量一個移動節點的位置,只需要3個參考節點。該定位法還避免了采用三邊測量法可能無解的情況,使得該方法的適應性更強。
相關代碼
1. 信標(坐標)點准備:
(1)信標使用自建坐標系(以米為單位)
//注意:x1, y1, r1, x2, y2, r2, x3, y3都是以米為單位 let pointA = sidePointCalculation(with: x1, y1, r1, x2, y2, r2, x3, y3) let pointB = sidePointCalculation(with: x2, y2, r2, x3, y3, r3, x1, y1) let pointC = sidePointCalculation(with: x1, y1, r1, x3, y3, r3, x2, y2) let Mx = Double((pointA.x + pointB.x + pointC.x) / 3) let My = Double((pointA.y + pointB.y + pointC.y) / 3)
(2)使用經緯度坐標系
如果信標的坐標使用的是經緯度坐標系需要將經緯度坐標系轉換成墨卡托坐標系(墨卡托坐標是將經緯度轉換成以米為單位),計算出點坐標后再將墨卡托坐標轉換成經緯度坐標系。
//注意:x1, y1, r1, x2, y2, r2, x3, y3都是以米為單位 let pointA = sidePointCalculation(with: lat2Meters(location1.latitude), lon2Meters(location1.longitude), r1, lat2Meters(location2.latitude), lon2Meters(location2.longitude), r2, lat2Meters(location3.latitude), lon2Meters(location3.longitude)) let pointB = sidePointCalculation(with: x2..., y2..., r2..., x3..., y3..., r3..., x1..., y1...) let pointC = sidePointCalculation(with: x1..., y1..., r1..., x3..., y3..., r3..., x2..., y2...) let Mx = Double((pointA.x + pointB.x + pointC.x) / 3) let My = Double((pointA.y + pointB.y + pointC.y) / 3) let lat = meters2Lat(Mx) let lon = meters2Lon(My)
extension MapController { //////添加坐標轉換相應的方法 /** * X米轉經緯度 */ func meters2Lon(_ mx: Double) -> Double { let lon = mx * (180.0/20037508.342789244)//2*Math.PI*6378137/2.0=20037508.342789244 return lon } /** * Y米轉經緯度 */ func meters2Lat(_ my: Double) -> Double { var lat = my * (180.0/20037508.342789244) lat = 180.0 / .pi * (2 * atan(exp(lat * (.pi/180.0))) - .pi/2.0) return lat } /** * X經緯度轉米 */ func lon2Meters(_ lon: Double) -> Double { let mx = lon * (20037508.342789244/180.0) return mx } /** * Y經緯度轉米 */ func lat2Meters(_ lat: Double) -> Double { var my = log(tan((90 + lat) * (.pi/360.0)))/(.pi/180.0) my = my * (20037508.342789244/180.0) return my } }
2. :計算點坐標
extension MapController { //計算邊點 func sidePointCalculation(with x1: Double, _ y1: Double, _ r1: Double, _ x2: Double, _ y2: Double, _ r2: Double, _ x3: Double, _ y3: Double) -> CGPoint { //勾股定理 sqrt(X)是X開根號 pow(X,n)是X的n次方 //取beacon1圓心A 與 beacon2圓心B的距離 let AB = sqrt(pow(x1 - x2, 2) + pow(y1 - y2, 2)) let rAB = r1 + r2 if rAB > AB && (r1 < AB && r2 < AB) { //兩圓有相交點,兩圓相交點為C、D。兩圓與AB的相交點為E、F。o是EF的中點。 let EF = rAB - AB let Eo = EF * 0.5 let AE = r1 - EF let Ao = AE + Eo let AQ1 = acos((x2 - x1) / AB) let AQ2 = acos(Ao / r1) let BF = r2 - EF let Bo = BF + Eo //let BQ1 = acos(fabs(x1 - x2) / AB); let BQ2 = acos(Bo / r2) //原點{0,0}在左上角的情況下 let Cx = x1 + (r1 * cos(AQ1 + AQ2)) var Cy = 0.0 var Dx = x2 - (r2 * cos(AQ1 + BQ2)) var Dy = 0.0 if x1 < x2 { Dx = x2 - (r2 * cos(AQ1 + BQ2)) if y1 < y2 { Cy = y1 + (r1 * sin(AQ1 + AQ2)) Dy = y2 - (r2 * sin(AQ1 + BQ2)) } else { Cy = y1 - (r1 * sin(AQ1 + AQ2)) Dy = y2 + (r2 * sin(AQ1 + BQ2)) } } else { Cy = y1 + (r1 * sin(AQ1 + AQ2)) if y1 < y2 { Dy = y2 - (r2 * sin(AQ1 + BQ2)) } else { Dy = y2 + (r2 * sin(AQ1 + BQ2)) } } let Cc = sqrt(pow(Cx - x3, 2) + pow(Cy - y3, 2)) let Dc = sqrt(pow(Dx - x3, 2) + pow(Dy - y3, 2)) return Cc < Dc ? CGPoint(x: CGFloat(Cx), y: CGFloat(Cy)) : CGPoint(x: CGFloat(Dx), y: CGFloat(Dy)) } else { //兩圓無相交點 return midpointCalculation(with: x1, y1, r1, x2, y2, r2) } } //兩圓無相交點 func midpointCalculation(with x1: Double, _ y1: Double, _ r1: Double, _ x2: Double, _ y2: Double, _ r2: Double) -> CGPoint { let a = y1 - y2//豎邊 let b = x1 - x2//橫邊 let rr = r1 + r2 let s = r1 / rr let x = Double(abs(Float(x1 - (b * s)))) let y = Double(abs(Float(y1 - (a * s)))) return CGPoint(x: CGFloat(x), y: CGFloat(y)) } }
拓展⚠️:如果沒有使用CLLocationManager
的startRangingBeaconsSatisfyingContraint:而是通過import CoreBluetooth獲取的藍牙信號需要計算距離
(1)如果使用CoreBluetooth獲取的藍牙信號,需要遵守代理協議CBCentralManagerDelegate,通過centralManagerDidUpdateState(_ central: CBCentralManager)方法掃描信標
func centralManagerDidUpdateState(_ central: CBCentralManager) {
//確保本中心設備支持藍牙低能耗(BLE)並開啟時才能繼續操作
switch central.state{ case .unknown: print("未知") case .resetting: print("藍牙重置中") case .unsupported: print("本機不支持BLE") case .unauthorized: print("未授權") case .poweredOff: print("藍牙未開啟") case .poweredOn: //掃描正在廣播的外設--每當發現外設時都會調用didDiscover peripheral方法 //withServices:[xx]--只掃描正在廣播xx服務的外設,若nil則掃描所有外設(費電,不推薦)let serviceUUIDS = [CBUUID(string: "FFE0"),CBUUID(string: "FEE7")]//[CBUUID(string: "")] central.scanForPeripherals(withServices: serviceUUIDS, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true]) @unknown default: print("來自未來的錯誤") } } /// 發現外設:獲取RSSI信號和信標信息 /// - Parameters: /// - central: 提供此更新的中央管理器 /// - peripheral: 外設 /// - advertisementData: 包含任何廣告和掃描響應數據的字典 /// - RSSI: 當前RSSI,以dBm為單位 func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { #if DEBUG print("發現設備:peripheral = \(peripheral),advertisementData = \(advertisementData),RSSI = \(RSSI)") #endif }
(2)根據RSSI計算距離
/*
計算公式: d = 10^((abs(RSSI) - A) / (10 * n))
d - 計算所得距離
RSSI - 接收信號強度(負值)
A - 發射端和接收端相隔1米時的信號強度
n - 環境衰減因子
*/
func calcDistByRSSI(_ rssi: Int) -> Double { //TODO:需要多次測試確定A和n的值 let dis = pow(10.0, (Double((abs(rssi)) - 65)/(10 * 0.8))) return dis }