標籤:
鄭童宇
GitHub:https://github.com/CrazyZty
1.前言 現在市面上有不少Android手機支援敲擊螢幕解鎖,敲擊螢幕解鎖是一項很實用的功能,但一來只支援敲擊螢幕,二來只能用於解鎖或鎖屏,再者我們應用程式層的開發人員切不進去,完全無法玩起來。開發人員,開發人員,我們既然身為開發人員何不搞點大新聞,那麼這次我來教教各位如何用代碼來實現手機的敲擊識別,聽起來是不是很有趣,有些躍躍欲試呢。事實上在ios上已經有實現這個功能的應用:Knock,一款敲擊來解鎖Mac電腦的應用,售價4.99美元,約為33人民幣。有時候真想去做ios開發,可以開心的為自己的應用定價,愉悅的掙外快。言歸正傳,既然ios可以實現,那我們Android自然不能落伍,現在我就帶領大家來用代碼實現手機的敲擊識別吧。
本篇博文以Java為樣本語言,以Android為樣本平台。
2.功能實現2.1.實現思路 說到敲擊識別,你們會考慮使用什麼來實現呢,感應器?對,沒錯,作為手機手勢姿態識別的唯一途徑,我們自然需要使用感應器來實現對敲擊的識別,但Android感應器種類繁多,我們應該選擇哪一個呢?
在Android2.3的時代,Android系統就已經定義了11個感應器,到了現在Android6.0的時代,系統定義的感應器數目已經達到26個,這麼多感應器我們到底用哪一個呢,事實上我們只需要考慮2.3時代提供的那11個感應器即可,因為一方面後期加入的感應器部分如心跳感應器等需要硬體支援,導致很多手機無法支援此類感應器,另一方面2.3時代的11個感應器功能已經相當強大,可以支援絕大多數手勢姿態的識別,那麼現在我來列舉一下上述11個感應器:
SENSOR_TYPE_ACCELEROMETER 加速度
SENSOR_TYPE_MAGNETIC_FIELD 磁力
SENSOR_TYPE_ORIENTATION 方向
SENSOR_TYPE_GYROSCOPE 陀螺儀
SENSOR_TYPE_LIGHT 光線感應
SENSOR_TYPE_PRESSURE 壓力
SENSOR_TYPE_TEMPERATURE 溫度
SENSOR_TYPE_PROXIMITY 接近
SENSOR_TYPE_GRAVITY 重力
SENSOR_TYPE_LINEAR_ACCELERATION 線性加速度
SENSOR_TYPE_ROTATION_VECTOR 旋轉向量
關於這11個感應器的詳細描述,各位可以去http://www.oschina.net/question/163910_28354查看,事實上我一直懷疑LG G3的敲擊解鎖與光線感應器或接近感應器有關,因為我用手指懸浮在LG G3的頭部正上方時一直無法敲擊解鎖,移開後恢複正常,而敲擊鎖屏應該只和觸控螢幕相關,因為無論我怎麼遮擋感應器,敲擊鎖屏的功能完全不受影響。
言歸正傳,對這11個感應器有所瞭解後,我們需要選擇哪個或哪些感應器來實現功能呢,我們來類比一下手機敲擊的情況,將手機平放在案頭上,手指敲擊手機的時候,手指給了手機一個力,同時案頭給予手機一個反作用力,考慮案頭不形變的情況下,手機受力平衡加速度為0,但這時手機的加速度感應器資料是否會有變化呢,答案是會的,手機加速度感應器的資料會有一段短暫但明顯的變化,為什麼呢,手機受力平衡加速度為0是因為它是一個整體,但內部構件還是會受到相互之間複雜的力的左右,並非受力的同時就達到受力平衡的,其實換個思路。用一個和手機形狀相似內部光滑的容器,容器裡面放幾個玻璃球,敲擊幾下,容器不會移動,但玻璃球是不是移動了呢。雖然手機內部的構件遠比玻璃球穩定,但也得遵循基本法,老老實實接受力的作用。
上述情境是平放於案頭的情境,實際生活的情境往往更加複雜多樣,但無論處於哪種情境,毫無疑問對手機的敲擊操作都應該導致加速度感應器傳出資料的明顯變化,那麼我們現在就明白了應該選擇什麼感應器作為我們敲擊識別的工具了吧,但加速度相關的感應器有兩個,加速度感應器和線性加速度感應器,我們應該選擇哪一個呢,加速度感應器提供的資料是重力影響下的手機加速度,線性加速感應器提供的資料是排除重力影響的手機加速度,可以直觀的反映排除重力後手機的受力情況,很合適用以敲擊識別,那我們是否就應該選擇線性加速度感應器呢,恰恰相反,我們要選擇加速度感應器,Android提供的線性加速度感應器基於軟體的,不同平台對於線性加速感應器的處理未必相同,事實上,在敲擊三星S4,LG G3中一款機型的背面,就出現線性加速度感應器傳出的資料沒有較大變化的情況,保險起見,我們還是選用基於硬體的加速度感應器更合適一些。順便吐槽一句,當時看到壓力感應器的時候,我還以為監測作用於手機的壓力的感應器,那無疑是很適合用於識別敲擊,後面看到描述才知道是監測壓強的。
如上所說,對手機的敲擊操作會導致加速度感應器傳出資料的明顯變化,故而本次功能實現中,判斷是否有敲擊操作的方法是檢測手機線性加速度相比正常情況是否有明顯變化。在功能實現過程中為排除重力的影響,需要對加速度感應器的資料進行處理將其轉化為線性加速度,因為轉化為線性加速度是一個需要校準的過程,所以需要先投入一定數目的資料用於校準以獲得更精確的線性加速度,同時考慮到現實生活存在可能導致誤識別的情境,比如搖動手機會帶給手機一個較長時間且明顯的線性加速度變化,所以提出穩態的概念,將手機處於相對穩定,沒有長時間出現明顯線性加速變化的狀況視為穩態,在穩態的情況下才會進行對敲擊的識別,另外此次敲擊識別考慮到對手機邊框的敲擊使用可能性過低,因此僅考慮識別對手機螢幕或背面的敲擊,這樣在識別的過程中可忽略X,Y軸的資料,僅考慮Z軸的線性加速度。
2.2.功能簡介 本次實現的功能是識別對手機螢幕或背面的敲擊操作,功能實現流程: 註冊感應器,採集資料,投入指定數目的資料校準以擷取較精準的線性加速度,校準結束後判斷當前是否穩態,如果為非穩態,則等待下次資料,如果為穩態,則調用方法判斷是否存在敲擊操作,在進行敲擊識別的同時也將處理得到的線性加速度和最近敲擊次數,穩態狀態顯示到介面上去,
2.3.功能實現2.3.1.擷取感應器資料 註冊感應器的方法屬於系統原生的方法,就不過多講解,不過需要注意一點,在註冊加速度感應器時標識感應器資料採樣間隔的參數最好使用SENSOR_DELAY_GAME,因為敲擊導致的加速度資料變化很短暫,如果使用SENSOR_DELAY_UI或SENSOR_DELAY_NORMAL往往採集不到敲擊引發的加速度變化,當然如果使用SENSOR_DELAY_FASTEST自然不會有這個問題,但效能消耗會比較大。
註冊感應器後就可以在回調方法裡等待處理資料, 下面我給出實現代碼,綜合代碼講解實現過程。
<span style="font-size:14px;"> public void onSensorChanged(SensorEvent sensorEvent) { if (sensorEvent.sensor == null) { return; } if (sensorEvent.sensor.getType() == accelerometerSensorType) { float accelerationZ = sensorEvent.values[2]; if (accelerationZ > 0) { recognitionKnockRatio = 20; recognitionUniqueRatio = 10; smoothSectionMaxRatio = 5f; } else { recognitionKnockRatio = 7.5f; recognitionUniqueRatio = 6; smoothSectionMaxRatio = 2.5f; } gravityZ = alpha * gravityZ + (1 - alpha) * accelerationZ; linearAccelerationZ = accelerationZ - gravityZ; if (calibrateLinearAcceleration) { calibrateLinearAccelerationIndex++; if (calibrateLinearAccelerationIndex <= calibrateLinearAccelerationSectionNumber) { return; } calibrateLinearAcceleration = false; } if (sensorDataShowIndex >= sensorDataShowNumber) { sensorDataShowIndex = sensorDataShowNumber - sensorDataShowDurationNumber; Iterator<?> it = linearAccelerationZShowList.listIterator(0); for (int i = 0; i < sensorDataShowDurationNumber; i++) { it.next(); it.remove(); } MainActivity.UpdateSensorData(linearAccelerationZShowList); } linearAccelerationZShowList.add(linearAccelerationZ); sensorDataShowIndex++; if (!stable) { linearAccelerationZList.add(linearAccelerationZ); if (linearAccelerationZList.size() >= stableSectionNumber) { stableRecognition(); linearAccelerationZList.clear(); } return; } knockRecognition(linearAccelerationZ); } }</span> 感應器資料回調的方法中對加速度感應器擷取的資料分別進行了處理,首先,根據z軸加速度的正負,為recognitionKnockRatio,recognitionUniqueRatio,smoothSectionMaxRatio三個變數賦予不同的數值,至於為什麼要進行這樣處理,是因為對Android手機實際進行敲擊操作發現,加速度感應器對正面敲擊操作反饋敏感,對背面敲擊操作反饋相對遲鈍,反饋到資料層面就是,敲擊正面導致的加速度感應器資料變化相比敲擊背面明顯很多,故而針對敲擊螢幕和敲擊背面要分配不同的數值,然而事實上站在手機的角度,運用現在的資料是完全無法分析敲擊操作導致的加速度明顯變化來源於敲擊正面還是敲擊背面,所以就使用z軸加速度的正負來簡單判斷,畢竟絕大多數情況下z軸加速度為正,那就是手機背面偏向地面,使用者更可能敲擊手機螢幕,而為負就是手機螢幕偏向地面,使用者更可能敲擊手機背面。至於導致敲擊螢幕和敲擊背面加速度感應器反饋敏感程度不同這種情況的原因不外乎兩個,一是加速度感應器相比於背面距離螢幕更近,再者就是Android手機外殼的問題了,這一點在LG G3上尤為明顯,LG G3的是有一定弧度的塑料外殼,在背面敲擊引發的感應器資料變化相比於敲擊螢幕要低很多,而金屬外殼的三星S6,在背面敲擊引發的感應器資料變化接近於敲擊螢幕。事實上上述三個係數屬於經驗係數,並且對於不同類型手機盡量提供不同的數值,原因可參見剛才所說的LG G3和三星S6,再一次感慨Android手機的多樣性,Android手機種類太多,硬體設計的不同導致在一款手機上適用的係數在另一款手機上可能完全無法適用,要是如iphone一樣只有那幾款機型的話無疑好處理很多。
接著對加速度進行濾波處理以擷取線性加速度,擷取線性加速度的方法參考了Android SensorEvent源碼中建議的方法:
<span style="font-size:14px;"> * <p> * It should be apparent that in order to measure the real acceleration of * the device, the contribution of the force of gravity must be eliminated. * This can be achieved by applying a <i>high-pass</i> filter. Conversely, a * <i>low-pass</i> filter can be used to isolate the force of gravity. * </p> * * <pre class="prettyprint"> * * public void onSensorChanged(SensorEvent event) * { * // alpha is calculated as t / (t + dT) * // with t, the low-pass filter's time-constant * // and dT, the event delivery rate * * final float alpha = 0.8; * * gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0]; * gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1]; * gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2]; * * linear_acceleration[0] = event.values[0] - gravity[0]; * linear_acceleration[1] = event.values[1] - gravity[1]; * linear_acceleration[2] = event.values[2] - gravity[2]; * } *</span> 通過高通濾波和低通濾波對加速度進行處理排除重力影響以擷取線性加速度,但在此過程中是需要傳入一定數量的資料進行校準以擷取較精準的線性加速度,在這裡我們設定calibrateLinearAccelerationSectionNumber作為用以校準資料的資料長度,用calibrateLinearAccelerationIndex和calibrateLinearAcceleration來控制何時校準結束。
校準結束後使用linearAccelerationZShowList儲存顯示到應用介面上的感應器線性加速度,接著如果處於非穩態,則開始穩態識別,判斷目前狀態是否穩態,如果處於穩態狀態則開始敲擊識別。
2.3.2.穩態識別 如上文提到的,使用者如果進行搖動手機之類的操作,是會產生明顯的加速度變化,很有可能導致誤識別的情況,所以在此提出了穩態的概念,即為手機加速度沒有長時間明顯變化的狀態,延伸到現實情境就是使用者沒有對手機進行明顯移動的狀態,嚴格來說,一般使用者在對手機進行明顯移動如搖動手機的同時進行敲擊操作的可能性極低,所以可以將穩態這個概念正式運用到功能實現中。
已經瞭解穩態這個概念,那我們應該如何定義什麼情況屬於穩態,什麼情況屬於非穩態,下面我給出實現代碼,綜合代碼進行講解。
<span style="font-size:14px;"> private void stableRecognition() { int exceptionNumber = 0; float accelerationZValue; float minAccelerationZValue = Integer.MAX_VALUE; float maxAccelerationZValue = Integer.MIN_VALUE; for (int i = stableSectionNumber - 1; i >= 0; i--) { accelerationZValue = linearAccelerationZList.get(i); if (Math.abs(accelerationZValue) > maxStableOffset) { exceptionNumber++; } else { if (accelerationZValue > maxAccelerationZValue) { maxAccelerationZValue = accelerationZValue; } else { if (accelerationZValue < minAccelerationZValue) { minAccelerationZValue = accelerationZValue; } } } } stable = exceptionNumber <= maxExceptionNumber; if (stable) { if (linearAccelerationZStableSection == 0) { linearAccelerationZStableSection = (maxAccelerationZValue - minAccelerationZValue) / 2; } if (linearAccelerationZStableSection > maxStableOffset) { linearAccelerationZStableSection = maxStableOffset; } } MainActivity.UpdateStable(stable); LogFunction.log("stable", "" + stable); LogFunction.log("exceptionNumber", "" + exceptionNumber); LogFunction.log("linearAccelerationZStableSection", "" + linearAccelerationZStableSection); }</span> 在此次功能實現過程中,判斷穩態的方式是採樣50個點,然後計算每個點的絕對值,如果大於最大偏差maxStableOffset就視為異常點,異常點大於最大異常點數目maxExceptionNumber就視為非穩態,反之視為穩態。判斷穩態結束後,如果處於穩態則將剔除異常點資料後的Z軸最大加速度和最小加速度之間差值的一半視為波動區間linearAccelerationZStableSection。maxStableOffset與maxExceptionNumber相同都是經驗係數,是對Android手機實際提供的不同情境下的線性加速度分析得出的。現在存在一個問題,那就是如果原本狀態處於穩態,然後使用者突然對手機進行操作,將手機狀態轉變為非穩態那要如何處理,不要著急,這個問題會在敲擊識別的過程中進行處理的。
2.3.3.敲擊識別 現在到了整個功能實現最核心的地方:敲擊識別,如上文所說敲擊會引起加速度感應器資料的明顯變化,但是我們要如何使用代碼進行檢測敲擊,以及如何排除使用者對手機其他動作引發的誤識別問題,事實上這些問題都會在這裡進行處理,現在我給出實現代碼,綜合代碼進行講解。
<span style="font-size:14px;"> private void knockRecognition(float linearAccelerationZ) { float linearAccelerationZAbsolute = Math.abs(linearAccelerationZ); float linearAccelerationZAbsoluteRadio = linearAccelerationZAbsolute / linearAccelerationZStableSection; if (linearAccelerationZAbsoluteRadio > recognitionUniqueRatio) { uniqueLinearAccelerationZList.add(linearAccelerationZ); currentForecastNumber = forecastNumber; } else { if (uniqueLinearAccelerationZList.size() > 0) { if (currentForecastNumber > 0) { currentForecastNumber--; } else { handleUniqueLinearAccelerationZ(); } } } if (linearAccelerationZAbsoluteRadio < smoothSectionMaxRatio) { float offsetWeight = 0.001f; linearAccelerationZStableSection = weightedMean(offsetWeight, linearAccelerationZAbsolute, linearAccelerationZStableSection); } }</span>
knockRecognition就是用來處理線性加速度進而確認是否有敲擊操作的方法,首先對傳入參數線性加速度進行處理,擷取線性加速度絕對值,接著如果線性加速度絕對值與波動區間的比值大於recognitionUniqueRatio,那就認為手機正在受到力的作用,為確定是敲擊操作還是使用者其他動作,先將線性加速度加入到獨特線性加速度列表中, 反之如果小於等於recognitionUniqueRatio,那就認為手機處於相對穩定點,在此時如果此時獨特線性加速度列表長度大於0,如果currentForecastNumber大於0,則currentForecastNumber減1,如果currentForecastNumber小於等於0,則開始處理獨特線性加速度列表,而在處理獨特線性加速度列表的過程中正式開始識別是否敲擊,以及目前狀態是否轉變為非穩態。在進行上述操作的同時,如果線性加速度絕對值與波動區間的比值小於smoothSectionMaxRatio則用線性加速度絕對值來平滑波動區間。
在這裡,大家肯定對currentForecastNumber有疑問,這個變數代表什麼含義,為什麼會有這個變數,原因是這樣的,一次敲擊可能導致兩個接近但不連續的獨特線性加速度。如果沒有currentForecastNumber這個變數就會導致現實的一次敲擊可能被識別為兩次敲擊操作。
而如果線性加速度絕對值與波動區間的比值小於smoothOffsetMaxRatio則用線性加速度絕對值來平滑波動區間,是因為一方面手機的狀態可能隨時改變,波動區間應該隨著手機狀態的改變跟著改變,另一方面穩態識別時計算的波動區間可能存在問題,並不能正確的反映當前手機的加速度波動,這個時候就需要根據最新的資料進行學習以平滑波動區間,而為什麼比值要小於smoothSectionMaxRatio是因為比值大於smoothSectionMaxRatio的基本是非正常情況的線性加速度,不適合用於平滑波動區間,而如果現實情況中的線性加速度與波動區間比值基本都超過smoothSectionMaxRatio,那說明現在手機多半處於非穩態狀態了,等待新的穩態識別重設波動區間即可,另外如上文所說,recognitionUniqueRatio,smoothOffsetMaxRatio屬於經驗係數,完全可以自主設定。
<span style="font-size:14px;"> private void handleUniqueLinearAccelerationZ() { LogFunction.log("linearAccelerationZStableSection", "" + linearAccelerationZStableSection); int recognitionKnockNumber = 1; int uniqueLinearAccelerationZListLength = uniqueLinearAccelerationZList.size(); float accelerationZOffsetAbsolute; float maxAccelerationZOffsetAbsolute = 0; for (int i = 0; i < uniqueLinearAccelerationZListLength; i++) { accelerationZOffsetAbsolute = Math.abs(uniqueLinearAccelerationZList.get(i)); if (maxAccelerationZOffsetAbsolute < accelerationZOffsetAbsolute) { maxAccelerationZOffsetAbsolute = accelerationZOffsetAbsolute; } LogFunction.log("uniqueLinearAccelerationZList index" + i, "" + uniqueLinearAccelerationZList.get(i)); } uniqueLinearAccelerationZList.clear(); LogFunction.log("uniqueLinearAccelerationZListLength", "" + uniqueLinearAccelerationZListLength); if (uniqueLinearAccelerationZListLength > unstableListLength) { stable = false; MainActivity.UpdateStable(stable); return; } LogFunction.log("maxAccelerationZOffsetAbsolute / linearAccelerationZStableSection", "" + (maxAccelerationZOffsetAbsolute / linearAccelerationZStableSection)); if (maxAccelerationZOffsetAbsolute > linearAccelerationZStableSection * recognitionKnockRatio) { LogFunction.log("recognitionKnockRatio", "" + recognitionKnockRatio); LogFunction.log("recognitionUniqueRatio", "" + recognitionUniqueRatio); knockRecognitionSuccess(recognitionKnockNumber); } }</span> 終於到了最後的handleUniqueLinearAccelerationZ方法,顧名思義,就是用來處理獨特線性加速度列表的,在這個方法內,進行了敲擊識別和穩態狀態是否轉變的判定,如果獨特線性加速度列表長度超過非穩態獨特線性加速度列表長度,則認為現在手機狀態此刻狀態轉變為非穩態並結束方法,如果發現加速度位移資料列表中最大位移值超過波動區間一定倍數則識別為敲擊。
3.總結 至此,敲擊識別的流程我們算是走完了。事實上我提供的敲擊識別方法還是存在著誤識別的情況,ios的Knock我使用過,擁有著符合價格的能力,識別率相當的好,不知道他們是通過機器學習還是別的方法歸結了一套他們的識別係數,當然我在此提供的敲擊識別僅僅是一種敲擊識別的方法,我也無法說它成熟,因為並沒有經過真正使用者的考驗,大家完全可以按照自己的思想更換演算法甚至更換感應器來實現自己的敲擊識別,而我在此其實相當於提供一個實現思路。
這是第三篇部落格了,第一篇部落格屬於試水就選擇了做過的一個比較偏門但並不好處理的一個小模組:為MP3檔案寫入ID3標籤,第二篇部落格選擇了一個很嚴謹的實用模組:音頻合成,前兩個模組都有一個共同點就是各種規範已經很明確,雖然代碼實現上可能有所不同但實現思路必然相同,而第三篇的部落格的敲擊檢測無疑寬鬆很多,所以我也是第一次寫了實現思路這一小節,因為我也不確定我的實現思路是否完全正確,作為感應器的實際應用是存在著無數的可能性,我們完全可以按照自己的想法去嘗試,錯了大不了換一個方向罷了。
另外部落格或者代碼中如果存在什麼問題,歡迎各位朋友們提出來。
這篇博文就到這裡結束了,本文所有代碼已經託管到https://github.com/CrazyZty/KnockDetect,大家可以自由下載。
淺談如何使用代碼實現手機敲擊識別