Android 触摸手势捕获界面
在本文中,我们将探讨针对 Android 设备的单笔画手势识别。
引言
在本文中,我们将研究一个用于捕捉触摸手势的 Android 应用程序。此模块是通用触摸式手势识别库的第一部分。
背景
手势是预先录制的触摸屏运动序列。手势识别是模式识别、图像分析和计算机视觉领域一个活跃的研究领域。
我们将有几种应用程序可以运行的模式。用户可以选择的选项之一是捕捉和存储候选手势。
目标是构建一个通用的 C/C++ 库,该库可以以用户定义的格式存储手势。
手势注册 Android 接口
捕捉和存储候选手势类别信息的这个过程称为手势注册。
在本文中,我们将使用 GestureOverlay
方法。手势覆盖区就像一个简单的绘图板,用户可以在上面绘制手势。用户可以修改几个视觉属性,例如用于绘制手势的笔触的颜色和宽度,并注册各种监听器来跟踪用户的操作。
要捕捉和处理手势,第一步是将 GestureOverlayView
添加到 store_gesture.xml XML 布局文件中。
<?xml version="1.0" encoding="utf-8"?>
......
......
<android.gesture.GestureOverlayView
android:id="@+id/gestures"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:eventsInterceptionEnabled="true"
android:gestureStrokeType="multiple"
android:orientation="vertical" >
<LinearLayout
style="@android:style/ButtonBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/done"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:enabled="false"
android:onClick="addGesture"
android:text="@string/button_done" />
<Button
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="cancelGesture"
android:text="@string/button_discard" />
</LinearLayout>
</android.gesture.GestureOverlayView>
指定了一些手势覆盖区的属性,例如手势笔触类型是单笔画,表示单笔画手势。
现在,在主活动文件中,我们只需要将内容视图设置为布局文件。在当前的应用程序中,布局文件的名称是“activity_open_vision_gesture.xml”。
由于我们还需要捕捉或处理手势一旦被执行,我们向覆盖区添加一个手势监听器。最常用的监听器是 GestureOverlayView.OnGesturePerformedListener
,它会在用户完成绘制手势时触发。我们使用一个名为 GesturesProcessor
的类,该类实现了 GestureOverlayListener
。
一旦用户绘制完手势,控制流就会进入 onGestureEnded
方法。在这里,我们可以复制手势并执行一系列活动,例如存储、预测等。
下面是 UI 界面的一张图片
private class GesturesProcessor implements GestureOverlayView.OnGestureListener {
public void onGestureStarted(GestureOverlayView overlay, MotionEvent event) {
mDoneButton.setEnabled(false);
mGesture = null;
}
public void onGesture(GestureOverlayView overlay, MotionEvent event) {
}
//callback function entered when the gesture registration is completed
public void onGestureEnded(GestureOverlayView overlay, MotionEvent event) {
//copy the gesture to local variable
mGesture = overlay.getGesture();
//ignore the gesture if length is below a threshold
if (mGesture.getLength() < LENGTH_THRESHOLD) {
overlay.clear(false);
}
//enable the store button
mDoneButton.setEnabled(true);
}
public void onGestureCancelled(GestureOverlayView overlay, MotionEvent event) {
}
}
点击存储按钮后,程序进入“onStore
”回调函数。
public void addGesture(View v) {
Log.e("CreateGestureActivity","Adding Gestures");
//function which extracts information from the Android Gesture Objects
//like locations and then make native library function calls to store the gesture
extractGestureInfo();
}
我们在 GestureLibraryInterface
类中定义了所有的 JNI 接口函数。我们定义了 2 个 JNI 接口调用到原生的 C/C++ 手势库。
public class GestureLibraryInterface {
static{Loader.load();}
//makes native calls to GestureLibrary to store gesture information in local filesystem
public native static void addGesture(ArrayList%lt;Float> location,ArrayList<Long> time,String name);
//make native calls to GestureLibrary to set the gesture directory
public native static void setDirectory(String name);
}
第一步是通过调用“setDirectory
”来设置存储手势的目录。这在“onCreate
”函数中初始化 AndroidActivity
时完成。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.store_gesture);
mDoneButton = findViewById(R.id.done);
eText = (EditText) findViewById(R.id.gesture_name);
GestureOverlayView overlay = (GestureOverlayView) findViewById(R.id.gestures);
overlay.addOnGestureListener(new GesturesProcessor());
GestureLibraryInterface.setDirectory(DIR);
}
extractGestureInfo
读取手势笔触并将位置存储在 ArrayList
中,该列表通过 JNI 接口传递到原生的 C/C++。
.....
private static final String DIR=Environment.getExternalStorageDirectory().getPath()+"/AndroidGesture/v1";
....
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//load the store activity GUI layoyt files
setContentView(R.layout.store_gesture);
//gets the store button object
mDoneButton = findViewById(R.id.done);
//get the EditText object
eText = (EditText) findViewById(R.id.gesture_name);
//configures the gestureOverLay Listener
GestureOverlayView overlay = (GestureOverlayView) findViewById(R.id.gestures);
overlay.addOnGestureListener(new GesturesProcessor());
//gets the gesture directory
GestureLibraryInterface.setDirectory(DIR);
}
与 Java 类相关的 JNI C/C++ 代码定义在 GestureLibraryInterface.cpp 和 GestureLibraryInterface.hpp 文件中。
//function calls the GetureRegognizer methods to add gesture to class path
JNIEXPORT void JNICALL Java_com_openvision_androidgesture_GestureLibraryInterface_addGesture(JNIEnv *, jobject, jobject, jobject, jstring);
//function calls the GestureRecognizer methods to set the main gesture directory path
JNIEXPORT void JNICALL Java_com_openvision_androidgesture_GestureLibraryInterface_setDirectory(JNIEnv *, jobject, jstring);
//Utility functions to convert from jobject datatype to float and Long
float getFloat(JNIEnv *env,jobject value);
long getLong(JNIEnv *env,jobject value);
UniStrokeGesture
库包含以下文件
- UniStrokeGestureLibrary
- UniStrokeGesture
- GesturePoint
UniStrokeGestureLibrary
类封装了单笔画手势的所有属性。它包含用于存储、检索和预测手势等方法。
“addGesture
”JNI 方法调用类中实现的保存例程来存储手势。
UniStrokeGestureLibrary
由一系列 UniStrokeGesture
类型的对象组成。
UniStrokeGesture
类的对象封装了单个手势类的所有属性。UniStrokeGesture
类包含存储多个样本手势实例的设施,因为 **单笔画** 手势可以由多个候选实例表示。
UniStrokeGesture
包含一系列 GesturePoint
类型的对象。每个 GesturePoint 代表 UniStrokeGesture
的一个元素,并以其在 2D 网格中的位置为特征。
/**
* function that stores the Gesture to a specified directory
*/
void UniStrokeGestureRecognizer::save(string dir,vector<gesturepoint> points)
{
char abspath1[1000],abspath2[1000];
sprintf(abspath1,"%s/%s",_path.c_str(),dir.c_str());
int count=0;
//check if directory exists else create it
int ret=ImgUtils::createDir((const char *)abspath1);
count=ImgUtils::getFileCount(abspath1);
sprintf(abspath2,"%s/%d.csv",abspath1,count);
//writing contents to file in csv format
ofstream file(abspath2,std::ofstream::out);
for(int i=0;i<points.size();i++)
{
GesturePoint p=points[i];
file << p.position.x <<",";
file << p.position.y<< ","
}
file.close();
//creating a bitmap while storing the CSV file
generateBitmap(abspath2);
}
考虑一个以 CSV 格式存储的手势示例
generateBitMap
函数从输入 CSV 文件加载手势点,并生成适合显示的位图图像。
void UniStrokeGestureRecognizer::generateBitmap(string file)
{
string basedir=ImgUtils::getBaseDir(file);
string name=ImgUtils::getBaseName(file);
string line;
//ifstream classFile(file.c_str());
float x,y;
cv::Mat image=cv::Mat(640,480,CV_8UC3);
image.setTo(cv::Scalar::all(0));
Point x1,x2,x3;
int first=-1;
int delta=20;
vector<gesturepoint> points;
//loading the gesture from CSV file
points=loadTemplateFile(file.c_str(),"AA");
//getting the bounding box
Rect R=boundingBox(points);
//drawing the gesture
int i=0;
cv::circle(image,cv::Point((int)points[i].position.x,(int)points[i].position.y),3,cv::Scalar(255,255,0),-1,CV_AA);
for(i=1;i<points.size();i++) x2="cv::Point((int)points[i-1].position.x,
(int)points[i-1].position.y);" x1="cv::Point((int)points[i].position.x,
(int)points[i].position.y);" r="Rect(max(0,R.x-delta),max(0,R.y-delta),
R.width+2*delta,R.height+2*delta);">image.cols)
R.width=image.cols-R.x-1;
if(R.y+R.height>image.rows)
R.height=image.rows-R.y-1;
//extract the ROI
Mat roi=image(R);
Mat dst;
cv::resize(roi,dst,Size(640,480));
string bmpname=basedir+"/"+name+".bmp";
//save the bitmap
cv::imwrite(bmpname,dst);
}
显示手势列表
应用程序的下一部分处理在 Android UI 上显示上面部分创建的手势。
启动应用程序后,将加载模板目录中的所有 bmp 文件。
加载手势位图的活动以异步方式在后台进行。
在 Android 中,ListView
用于显示手势位图和相关文本。
列表中每个项目的布局定义在 gesture_item.xml 文件中。
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight"
android:drawablePadding="12dip"
android:paddingLeft="6dip"
android:paddingRight="6dip"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge" />
ListView
的布局在主布局文件 activity_open_vision.xml 中定义。
displayGestures
函数定义在 OpenVisionGesture.java
中。
在主类文件中定义了一个名为 GesturesLoadTask
的“AsyncTask
”对象。这些类的方法从 displayGestures
函数调用,该函数在后台加载手势列表。
ArrayTask
类的对象需要定义这些方法
doInBackground
- 在后台执行的主函数onPreExecute
- 在后台任务执行之前调用的函数onPostExecute
- 在后台任务执行之后调用的函数- onProgressUpdate - 函数可用于在后台任务执行期间更新 UI 内容
在后台任务中,代码解析手势模板目录并读取所有位图文件。
ArrayAdapter
接受一个数组并将项目转换为 View 对象以加载到 ListView
容器中。我们定义一个适配器,它维护一个 NamedGesture
对象,该对象包含手势名称和标识符。“getView
”函数在 ArrayAdapter
类中负责将 Java 对象转换为 View。
我们维护一个由 ID 标识的位图列表以及由 SameID
表示的手势名称列表。
每当读取一个位图时,我们都会更新列表和 GUI,以便通过调用“publishProgress
”函数进行显示,该函数会导致在主 UI 线程中调用 onProgressUpdate
函数。
使用这种方法,我们可以看到位图随着时间的推移而填充
@Override
protected Integer doInBackground(Void... params) {
if (isCancelled()) return STATUS_CANCELLED;
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
return STATUS_NO_STORAGE;
}
Long id=new Long(0);
File list = new File(CreateGestureActivity.DIR);
//get list of template classes
File[] files = list.listFiles(new DirFilter());
for(int i=0;i<files.length;i++)
{
//get list of image files in the template folder
File[] list1=files[i].listFiles(new ImageFileFilter());
for(int k=0;k<list1.length;k++)
{
//load the image files
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap bitmap = BitmapFactory.decodeFile(list1[k].getPath(), options);
Bitmap ThumbImage = ThumbnailUtils.extractThumbnail(bitmap, mThumbnailSize, mThumbnailSize);
final NamedGesture namedGesture = new NamedGesture();
namedGesture.id=id;
namedGesture.name = files[i].getName()+"_"+list1[k].getName();
//add bitmap to hashtable
mAdapter.addBitmap((Long)id, ThumbImage);
id=id+1;
//update the GUI
publishProgress(namedGesture);
bitmap.recycle();
}
}
return STATUS_SUCCESS;
}
一旦调用了适配器中的 add 函数,UI ListView
就会通过在“getView
”函数中显示手势名称和关联的位图来更新。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
//view associated with individual gesture item
convertView = mInflater.inflate(R.layout.gestures_item, parent, false);
}
//get the gesture at specified position in the listView
final NamedGesture gesture = getItem(position);
final TextView label = (TextView) convertView;
//set the gesture names
label.setTag(gesture);
label.setText(gesture.name);
//get the bitmap from hashtable identified by id and display bitmap to left of text
label.setCompoundDrawablesWithIntrinsicBounds(mThumbnails.get(gesture.id),null, null, null);
return convertView;
}
代码
ImgApp 目录中的文件来自 OpenVision 存储库,可在 github 存储库 www.github.com/pi19404/OpenVision. 中找到。
完整的 Android 项目可以在 OpenVision 存储库的 samples/Android/AndroidGestureCapture 目录中找到。 这是 Android 项目源包,可以直接导入到 Eclipse 中运行。该应用程序已在 Android 版本 4.1.2 的移动设备上进行了测试。在开发应用程序时,并未测试或考虑与其他 Android OS 版本的兼容性。
您需要在系统上安装 OpenCV。本应用程序是在 Ubuntu 12.04 OS 上开发的。Android.mk 中的路径是基于此指定的。对于 Windows 或其他 OS,或者如果 OpenCV 路径不同,请相应地修改 make 文件。
可以在文章顶部的链接处下载 apk 文件。