做者:郑童宇ios
GitHub:https://github.com/CrazyZtygit
如今市面上有很多Android手机支持敲击屏幕解锁,敲击屏幕解锁是一项很实用的功能,但一来只支持敲击屏幕,二来只能用于解锁或锁屏,再者咱们应用层的开发者切不进去,彻底没法玩起来。开发者,开发者,咱们既然身为开发者何不搞点大新闻,那么此次我来教教各位如何用代码来实现手机的敲击识别,听起来是否是颇有趣,有些跃跃欲试呢。事实上在ios上已经有实现这个功能的应用:Knock,一款敲击来解锁Mac电脑的应用,售价4.99美圆,约为33人民币。有时候真想去作ios开发,能够开心的为本身的应用订价,愉悦的挣外快。言归正传,既然ios能够实现,那咱们Android天然不能落伍,如今我就带领你们来用代码实现手机的敲击识别吧。github
本篇博文以Java为示例语言,以Android为示例平台。算法
说到敲击识别,大家会考虑使用什么来实现呢,传感器?对,没错,做为手机手势姿态识别的惟一途径,咱们天然须要使用传感器来实现对敲击的识别,但Android传感器种类繁多,咱们应该选择哪个呢?app
在Android2.3的时代,Android系统就已经定义了11个传感器,到了如今Android6.0的时代,系统定义的传感器数目已经达到26个,这么多传感器咱们到底用哪个呢,事实上咱们只须要考虑2.3时代提供的那11个传感器便可,由于一方面后期加入的传感器部分如心跳传感器等须要硬件支持,致使不少手机没法支持此类传感器,另外一方面2.3时代的11个传感器功能已经至关强大,能够支持绝大多数手势姿态的识别,那么如今我来列举一下上述11个传感器:iphone
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轴的线性加速度。
本次实现的功能是识别对手机屏幕或背面的敲击操做,功能实现流程: 注册传感器,采集数据,投入指定数目的数据校准以获取较精准的线性加速度,校准结束后判断当前是否稳态,若是为非稳态,则等待下次数据,若是为稳态,则调用方法判断是否存在敲击操做,在进行敲击识别的同时也将处理获得的线性加速度和最近敲击次数,稳态状态显示到界面上去,
注册传感器的方法属于系统原生的方法,就不过多讲解,不过须要注意一点,在注册加速度传感器时标识传感器数据采样间隔的参数最好使用SENSOR_DELAY_GAME,由于敲击致使的加速度数据变化很短暂,若是使用SENSOR_DELAY_UI或SENSOR_DELAY_NORMAL每每采集不到敲击引起的加速度变化,固然若是使用SENSOR_DELAY_FASTEST天然不会有这个问题,但性能消耗会比较大。
注册传感器后就能够在回调方法里等待处理数据, 下面我给出实现代码,综合代码讲解实现过程。
1 public void onSensorChanged(SensorEvent sensorEvent) { 2 if (sensorEvent.sensor == null) { 3 return; 4 } 5 6 if (sensorEvent.sensor.getType() == accelerometerSensorType) { 7 float accelerationZ = sensorEvent.values[2]; 8 9 if (accelerationZ > 0) { 10 recognitionKnockRatio = 20; 11 recognitionUniqueRatio = 10; 12 13 smoothSectionMaxRatio = 5f; 14 } else { 15 recognitionKnockRatio = 7.5f; 16 recognitionUniqueRatio = 6; 17 18 smoothSectionMaxRatio = 2.5f; 19 } 20 21 gravityZ = alpha * gravityZ + (1 - alpha) * accelerationZ; 22 23 linearAccelerationZ = accelerationZ - gravityZ; 24 25 if (calibrateLinearAcceleration) { 26 calibrateLinearAccelerationIndex++; 27 28 if (calibrateLinearAccelerationIndex <= calibrateLinearAccelerationSectionNumber) { 29 return; 30 } 31 32 calibrateLinearAcceleration = false; 33 } 34 35 if (sensorDataShowIndex >= sensorDataShowNumber) { 36 sensorDataShowIndex = sensorDataShowNumber - sensorDataShowDurationNumber; 37 38 Iterator<?> it = linearAccelerationZShowList.listIterator(0); 39 for (int i = 0; i < sensorDataShowDurationNumber; i++) { 40 it.next(); 41 it.remove(); 42 } 43 44 MainActivity.UpdateSensorData(linearAccelerationZShowList); 45 } 46 47 linearAccelerationZShowList.add(linearAccelerationZ); 48 49 sensorDataShowIndex++; 50 51 if (!stable) { 52 linearAccelerationZList.add(linearAccelerationZ); 53 54 if (linearAccelerationZList.size() >= stableSectionNumber) { 55 stableRecognition(); 56 57 linearAccelerationZList.clear(); 58 } 59 60 return; 61 } 62 63 knockRecognition(linearAccelerationZ); 64 } 65 }
传感器数据回调的方法中对加速度传感器获取的数据分别进行了处理,首先,根据z轴加速度的正负,为recognitionKnockRatio,recognitionUniqueRatio,smoothSectionMaxRatio三个变量赋予不一样的数值,至于为何要进行这样处理,是由于对Android手机实际进行敲击操做发现,加速度传感器对正面敲击操做反馈敏感,对背面敲击操做反馈相对迟钝,反馈到数据层面就是,敲击正面致使的加速度传感器数据变化相比敲击背面明显不少,故而针对敲击屏幕和敲击背面要分配不一样的数值,然而事实上站在手机的角度,运用如今的数据是彻底没法分析敲击操做致使的加速度明显变化来源于敲击正面仍是敲击背面,因此就使用z轴加速度的正负来简单判断,毕竟绝大多数状况下z轴加速度为正,那就是手机背面偏向地面,用户更可能敲击手机屏幕,而为负就是手机屏幕偏向地面,用户更可能敲击手机背面。至于致使敲击屏幕和敲击背面加速度传感器反馈敏感程度不一样这种状况的缘由不外乎两个,一是加速度传感器相比于背面距离屏幕更近,再者就是Android手机外壳的问题了,这一点在LG G3上尤其明显,LG G3的是有必定弧度的塑料外壳,在背面敲击引起的传感器数据变化相比于敲击屏幕要低不少,而金属外壳的三星S6,在背面敲击引起的传感器数据变化接近于敲击屏幕。事实上上述三个系数属于经验系数,而且对于不一样类型手机尽可能提供不一样的数值,缘由可参见刚才所说的LG G3和三星S6,再一次感慨Android手机的多样性,Android手机种类太多,硬件设计的不一样致使在一款手机上适用的系数在另外一款手机上可能彻底没法适用,要是如iphone同样只有那几款机型的话无疑好处理不少。
接着对加速度进行滤波处理以获取线性加速度,获取线性加速度的方法参考了Android SensorEvent源码中建议的方法:
1 * <p> 2 * It should be apparent that in order to measure the real acceleration of 3 * the device, the contribution of the force of gravity must be eliminated. 4 * This can be achieved by applying a <i>high-pass</i> filter. Conversely, a 5 * <i>low-pass</i> filter can be used to isolate the force of gravity. 6 * </p> 7 * 8 * <pre class="prettyprint"> 9 * 10 * public void onSensorChanged(SensorEvent event) 11 * { 12 * // alpha is calculated as t / (t + dT) 13 * // with t, the low-pass filter's time-constant 14 * // and dT, the event delivery rate 15 * 16 * final float alpha = 0.8; 17 * 18 * gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0]; 19 * gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1]; 20 * gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2]; 21 * 22 * linear_acceleration[0] = event.values[0] - gravity[0]; 23 * linear_acceleration[1] = event.values[1] - gravity[1]; 24 * linear_acceleration[2] = event.values[2] - gravity[2]; 25 * } 26 * </pre>
经过高通滤波和低通滤波对加速度进行处理排除重力影响以获取线性加速度,但在此过程当中是须要传入必定数量的数据进行校准以获取较精准的线性加速度,在这里咱们设定calibrateLinearAccelerationSectionNumber做为用以校准数据的数据长度,用calibrateLinearAccelerationIndex和calibrateLinearAcceleration来控制什么时候校准结束。
校准结束后使用linearAccelerationZShowList存储显示到应用界面上的传感器线性加速度,接着若是处于非稳态,则开始稳态识别,判断当前状态是否稳态,若是处于稳态状态则开始敲击识别。
如上文提到的,用户若是进行摇动手机之类的操做,是会产生明显的加速度变化,颇有可能致使误识别的状况,因此在此提出了稳态的概念,即为手机加速度没有长时间明显变化的状态,延伸到现实场景就是用户没有对手机进行明显移动的状态,严格来讲,通常用户在对手机进行明显移动如摇动手机的同时进行敲击操做的可能性极低,因此能够将稳态这个概念正式运用到功能实现中。
已经了解稳态这个概念,那咱们应该如何定义什么状况属于稳态,什么状况属于非稳态,下面我给出实现代码,综合代码进行讲解。
1 private void stableRecognition() { 2 int exceptionNumber = 0; 3 4 float accelerationZValue; 5 float minAccelerationZValue = Integer.MAX_VALUE; 6 float maxAccelerationZValue = Integer.MIN_VALUE; 7 8 for (int i = stableSectionNumber - 1; i >= 0; i--) { 9 accelerationZValue = linearAccelerationZList.get(i); 10 11 if (Math.abs(accelerationZValue) > maxStableOffset) { 12 exceptionNumber++; 13 } else { 14 if (accelerationZValue > maxAccelerationZValue) { 15 maxAccelerationZValue = accelerationZValue; 16 } else { 17 if (accelerationZValue < minAccelerationZValue) { 18 minAccelerationZValue = accelerationZValue; 19 } 20 } 21 } 22 } 23 24 stable = exceptionNumber <= maxExceptionNumber; 25 26 if (stable) { 27 if (linearAccelerationZStableSection == 0) { 28 linearAccelerationZStableSection = 29 (maxAccelerationZValue - minAccelerationZValue) / 2; 30 } 31 32 if (linearAccelerationZStableSection > maxStableOffset) { 33 linearAccelerationZStableSection = maxStableOffset; 34 } 35 } 36 37 MainActivity.UpdateStable(stable); 38 39 LogFunction.log("stable", "" + stable); 40 LogFunction.log("exceptionNumber", "" + exceptionNumber); 41 LogFunction.log("linearAccelerationZStableSection", "" + linearAccelerationZStableSection); 42 }
在这次功能实现过程当中,判断稳态的方式是采样50个点,而后计算每一个点的绝对值,若是大于最大误差maxStableOffset就视为异常点,异常点大于最大异常点数目maxExceptionNumber就视为非稳态,反之视为稳态。判断稳态结束后,若是处于稳态则将剔除异常点数据后的Z轴最大加速度和最小加速度之间差值的一半视为波动区间linearAccelerationZStableSection。maxStableOffset与maxExceptionNumber相同都是经验系数,是对Android手机实际提供的不一样场景下的线性加速度分析得出的。如今存在一个问题,那就是若是本来状态处于稳态,而后用户忽然对手机进行操做,将手机状态转变为非稳态那要如何处理,不要着急,这个问题会在敲击识别的过程当中进行处理的。
如今到了整个功能实现最核心的地方:敲击识别,如上文所说敲击会引发加速度传感器数据的明显变化,可是咱们要如何使用代码进行检测敲击,以及如何排除用户对手机其余操做引起的误识别问题,事实上这些问题都会在这里进行处理,如今我给出实现代码,综合代码进行讲解。
1 private void knockRecognition(float linearAccelerationZ) { 2 float linearAccelerationZAbsolute = Math.abs(linearAccelerationZ); 3 4 float linearAccelerationZAbsoluteRadio = 5 linearAccelerationZAbsolute / linearAccelerationZStableSection; 6 7 if (linearAccelerationZAbsoluteRadio > recognitionUniqueRatio) { 8 uniqueLinearAccelerationZList.add(linearAccelerationZ); 9 10 currentForecastNumber = forecastNumber; 11 } else { 12 if (uniqueLinearAccelerationZList.size() > 0) { 13 if (currentForecastNumber > 0) { 14 currentForecastNumber--; 15 } else { 16 handleUniqueLinearAccelerationZ(); 17 } 18 } 19 } 20 21 if (linearAccelerationZAbsoluteRadio < smoothSectionMaxRatio) { 22 float offsetWeight = 0.001f; 23 24 linearAccelerationZStableSection = 25 weightedMean(offsetWeight, linearAccelerationZAbsolute, 26 linearAccelerationZStableSection); 27 } 28 }
knockRecognition就是用来处理线性加速度进而确认是否有敲击操做的方法,首先对传入参数线性加速度进行处理,获取线性加速度绝对值,接着若是线性加速度绝对值与波动区间的比值大于recognitionUniqueRatio,那就认为手机正在受到力的做用,为肯定是敲击操做仍是用户其余操做,先将线性加速度加入到独特线性加速度列表中, 反之若是小于等于recognitionUniqueRatio,那就认为手机处于相对稳定状态,在此时若是此时独特线性加速度列表长度大于0,若是currentForecastNumber大于0,则currentForecastNumber减1,若是currentForecastNumber小于等于0,则开始处理独特线性加速度列表,而在处理独特线性加速度列表的过程当中正式开始识别是否敲击,以及当前状态是否转变为非稳态。在进行上述操做的同时,若是线性加速度绝对值与波动区间的比值小于smoothSectionMaxRatio则用线性加速度绝对值来平滑波动区间。
在这里,你们确定对currentForecastNumber有疑问,这个变量表明什么含义,为何会有这个变量,缘由是这样的,一次敲击可能致使两个接近但不连续的独特线性加速度。若是没有currentForecastNumber这个变量就会致使现实的一次敲击可能被识别为两次敲击操做。
而若是线性加速度绝对值与波动区间的比值小于smoothOffsetMaxRatio则用线性加速度绝对值来平滑波动区间,是由于一方面手机的状态可能随时改变,波动区间应该随着手机状态的改变跟着改变,另外一方面稳态识别时计算的波动区间可能存在问题,并不能正确的反映当前手机的加速度波动,这个时候就须要根据最新的数据进行学习以平滑波动区间,而为何比值要小于smoothSectionMaxRatio是由于比值大于smoothSectionMaxRatio的基本是非正常状况的线性加速度,不适合用于平滑波动区间,而若是现实状况中的线性加速度与波动区间比值基本都超过smoothSectionMaxRatio,那说明如今手机多半处于非稳态状态了,等待新的稳态识别重置波动区间便可,另外如上文所说,recognitionUniqueRatio,smoothOffsetMaxRatio属于经验系数,彻底能够自主设定。
1 private void handleUniqueLinearAccelerationZ() { 2 LogFunction.log("linearAccelerationZStableSection", "" + linearAccelerationZStableSection); 3 4 int recognitionKnockNumber = 1; 5 6 int uniqueLinearAccelerationZListLength = uniqueLinearAccelerationZList.size(); 7 8 float accelerationZOffsetAbsolute; 9 float maxAccelerationZOffsetAbsolute = 0; 10 11 for (int i = 0; i < uniqueLinearAccelerationZListLength; i++) { 12 accelerationZOffsetAbsolute = Math.abs(uniqueLinearAccelerationZList.get(i)); 13 14 if (maxAccelerationZOffsetAbsolute < accelerationZOffsetAbsolute) { 15 maxAccelerationZOffsetAbsolute = accelerationZOffsetAbsolute; 16 } 17 18 LogFunction.log("uniqueLinearAccelerationZList index" + i, 19 "" + uniqueLinearAccelerationZList.get(i)); 20 } 21 22 uniqueLinearAccelerationZList.clear(); 23 24 LogFunction.log("uniqueLinearAccelerationZListLength", 25 "" + uniqueLinearAccelerationZListLength); 26 27 if (uniqueLinearAccelerationZListLength > unstableListLength) { 28 stable = false; 29 MainActivity.UpdateStable(stable); 30 return; 31 } 32 33 LogFunction.log("maxAccelerationZOffsetAbsolute / linearAccelerationZStableSection", 34 "" + (maxAccelerationZOffsetAbsolute / linearAccelerationZStableSection)); 35 36 if (maxAccelerationZOffsetAbsolute > 37 linearAccelerationZStableSection * recognitionKnockRatio) { 38 LogFunction.log("recognitionKnockRatio", "" + recognitionKnockRatio); 39 LogFunction.log("recognitionUniqueRatio", "" + recognitionUniqueRatio); 40 41 knockRecognitionSuccess(recognitionKnockNumber); 42 } 43 }
终于到了最后的handleUniqueLinearAccelerationZ方法,顾名思义,就是用来处理独特线性加速度列表的,在这个方法内,进行了敲击识别和稳态状态是否转变的断定,若是独特线性加速度列表长度超过非稳态独特线性加速度列表长度,则认为如今手机状态此刻状态转变为非稳态并结束方法,若是发现加速度偏移数据列表中最大偏移值超过波动区间必定倍数则识别为敲击。
至此,敲击识别的流程咱们算是走完了。事实上我提供的敲击识别方法仍是存在着误识别的状况,ios的Knock我使用过,拥有着符合价格的能力,识别率至关的好,不知道他们是经过机器学习仍是别的方法归结了一套他们的识别系数,固然我在此提供的敲击识别仅仅是一种敲击识别的方法,我也没法说它成熟,由于并无通过真正用户的考验,你们彻底能够按照本身的思想更换算法甚至更换传感器来实现本身的敲击识别,而我在此其实至关于提供一个实现思路。
这是第三篇博客了,第一篇博客属于试水就选择了作过的一个比较偏门但并很差处理的一个小模块:为MP3文件写入ID3标签,第二篇博客选择了一个很严谨的实用模块:音频合成,前两个模块都有一个共同点就是各类规范已经很明确,虽然代码实现上可能有所不一样但实现思路必然相同,而第三篇的博客的敲击检测无疑宽松不少,因此我也是第一次写了实现思路这一小节,由于我也不肯定个人实现思路是否彻底正确,做为传感器的实际应用是存在着无数的可能性,咱们彻底能够按照本身的想法去尝试,错了大不了换一个方向罢了。
另外博客或者代码中若是存在什么问题,欢迎各位朋友们提出来。
这篇博文就到这里结束了,本文全部代码已经托管到https://github.com/CrazyZty/KnockDetect,你们能够自由下载。