浅谈如何使用代码实现手机敲击识别
现在市面上有不少Android手机支持敲击屏幕解锁,敲击屏幕解锁是一项很实用的功能,但一来只支持敲击屏幕,二来只能用于解锁或锁屏,再者我们应用层的开发者切不进去,完全无法玩起来。开发者,开发者,我们既然身为开发者何不搞点大新闻,那么这次我来教教各位如何用代码来实现手机的敲击识别,听起来是不是很有趣,有些跃跃欲试呢。事实上在ios上已经有实现这个功能的应用:Knock,一款敲击来解锁Mac电脑的应用,售价4.99美元,约为33人民币。有时候真想去做ios开发,可以开心的为自己的应用定价,愉悦的挣外快。言归正传,既然ios可以实现,那我们Android自然不能落伍,现在我就带领大家来用代码实现手机的敲击识别吧。
作者:郑童宇
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自然不会有这个问题,但性能消耗会比较大。
注册传感器后就可以在回调方法里等待处理数据, 下面我给出实现代码,综合代码讲解实现过程。
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存储显示到应用界面上的传感器线性加速度,接着如果处于非稳态,则开始稳态识别,判断当前状态是否稳态,如果处于稳态状态则开始敲击识别。
2.3.2.稳态识别
如上文提到的,用户如果进行摇动手机之类的操作,是会产生明显的加速度变化,很有可能导致误识别的情况,所以在此提出了稳态的概念,即为手机加速度没有长时间明显变化的状态,延伸到现实场景就是用户没有对手机进行明显移动的状态,严格来说,一般用户在对手机进行明显移动如摇动手机的同时进行敲击操作的可能性极低,所以可以将稳态这个概念正式运用到功能实现中。
已经了解稳态这个概念,那我们应该如何定义什么情况属于稳态,什么情况属于非稳态,下面我给出实现代码,综合代码进行讲解。
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手机实际提供的不同场景下的线性加速度分析得出的。现在存在一个问题,那就是如果原本状态处于稳态,然后用户突然对手机进行操作,将手机状态转变为非稳态那要如何处理,不要着急,这个问题会在敲击识别的过程中进行处理的。
2.3.3.敲击识别
现在到了整个功能实现最核心的地方:敲击识别,如上文所说敲击会引起加速度传感器数据的明显变化,但是我们要如何使用代码进行检测敲击,以及如何排除用户对手机其他操作引发的误识别问题,事实上这些问题都会在这里进行处理,现在我给出实现代码,综合代码进行讲解。
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方法,顾名思义,就是用来处理独特线性加速度列表的,在这个方法内,进行了敲击识别和稳态状态是否转变的判定,如果独特线性加速度列表长度超过非稳态独特线性加速度列表长度,则认为现在手机状态此刻状态转变为非稳态并结束方法,如果发现加速度偏移数据列表中最大偏移值超过波动区间一定倍数则识别为敲击。
3.总结
至此,敲击识别的流程我们算是走完了。事实上我提供的敲击识别方法还是存在着误识别的情况,ios的Knock我使用过,拥有着符合价格的能力,识别率相当的好,不知道他们是通过机器学习还是别的方法归结了一套他们的识别系数,当然我在此提供的敲击识别仅仅是一种敲击识别的方法,我也无法说它成熟,因为并没有经过真正用户的考验,大家完全可以按照自己的思想更换算法甚至更换传感器来实现自己的敲击识别,而我在此其实相当于提供一个实现思路。
这是第三篇博客了,第一篇博客属于试水就选择了做过的一个比较偏门但并不好处理的一个小模块:为MP3文件写入ID3标签,第二篇博客选择了一个很严谨的实用模块:音频合成,前两个模块都有一个共同点就是各种规范已经很明确,虽然代码实现上可能有所不同但实现思路必然相同,而第三篇的博客的敲击检测无疑宽松很多,所以我也是第一次写了实现思路这一小节,因为我也不确定我的实现思路是否完全正确,作为传感器的实际应用是存在着无数的可能性,我们完全可以按照自己的想法去尝试,错了大不了换一个方向罢了。
另外博客或者代码中如果存在什么问题,欢迎各位朋友们提出来。
这篇博文就到这里结束了,本文所有代码已经托管到https://github.com/CrazyZty/KnockDetect,大家可以自由下载。