简介:
大多数 Android 应用开发人员使用的 Android 软件开发工具包(SDK)所要求使用
Java? 编程语言。但是,网上有很多 C 语言代码可供使用。Android 原生开发工具包(NDK)允许
Android 开发人员重用 Android 应用程序中的现有 C 源代码。在本教程中,您将在 Java
中创建一个图像处理应用程序,并通过 NDK 使用 C 代码执行基本的图像处理操作。
开始之前
首先,了解 Android 原生开发工具包(NDK)的动机之一是得以利用开源项目,大多数项目都是用
C 语言编写的。完成本教程后,您将了解到如何创建 Java 本地接口(JNI)库,它使用 C 语言编写,使用原生开发工具包(NDK)进行编译,并将该库包含到了使用
Java 语言编写的 Android 应用程序中。应用程序演示了如何根据原始图像数据执行基本的图像处理操作。您还将学习如何扩展
Eclipse 构建环境以将 NDK 项目集成到 Android SDK 项目文件中。以此为基础,您可以更好地将现有开源代码移植到
Android 平台。
关于本教程
本教程介绍了 Eclipse 环境中的 Android NDK。NDK
过去常常使用 C 编程语言为 Android 应用程序添加功能。本教程以 NDK 及其常用场景的概述开始。然后,介绍了图像处理,以及本教程的应用程序
IBM Photo Phun 的简介和演示。本应用程序混合使用了基于 SDK 的 Java 代码和 NDK
编译的 C 代码。随后,本教程介绍了 Java 本地接口(JNI),这是使用 NDK 时您会感兴趣的一种技术。对完整项目源文件的预先了解,可以为本文所构建的应用程序提供一个路线图。然后您将逐步构建此应用程序。本文为您阐述了所有涉及的
Java 类和 C 源文件。最后,自定义 Eclipse 构建环境来将 NDK 工具链直接集成到易用的
Eclipse 构建流程中。
先决条件
要学习本教程,您应该熟悉使用 Android SDK 构建 Android
应用程序,并对 C 编程语言有基本的了解。此外,您需要了解下列内容:
Eclipse 和 Android Developer Tools (ADT)
— Primary code editor、Java Compiler 和 Android Development
Tools 插件
Android 软件开发工具包(SDK)
Android 原生开发工具包(NDK)
PNG 图像 — 用于测试图像处理操作的图像
我在 MacBook Pro 上使用 Eclipse V3.4.2 和
Android SDK V8(支持名为 Android 2.2 (Froyo) 版本)创建了本教程的代码示例。本教程使用的
NDK 版本是 r4b。代码要求使用版本 r4b 或更高版本,因为在之前的 NDK 版本,Android
NDK 没有图像处理功能。
参见 参考资料 获得这些工具的链接。
Android NDK
我们首先介绍 Android NDK,以及如何使用它来改进 Android
平台。尽管 Android SDK 提供了非常丰富的编程环境,但 Android NDK 扩大了其范围,并且通过引入现有源代码(一些是专有代码,另一些是开源代码)加速了所需功能的交付。
NDK
可从 Android 网站免费下载 NDK 版本。NDK 包括将以 C
语言编写的功能纳入 Android 应用程序所需的所有组件。NDK 的初始版本仅提供多数基本功能且有很多限制。随后发布的每个
NDK 版本,都扩展了其功能。对于 r5 或 NDK 应用程序,编写者可直接以 C 语言编写应用程序的大多数内容,包括用户接口和事件处理功能。此处演示的图像处理功能包含在
NDK 的 r4b 版本中。
NDK 的两种常用方式是提高应用程序性能和将其移植到 Android 来利用现有
C 代码。首先介绍性能改进。以 C 语言编写代码并不保证可大幅提升性能。事实上,与编写良好的 Java
应用程序相比,糟糕的本机代码会降低应用程序性能。当以 C 语言精心编写功能来执行基于内存或计算密集型操作(像本教程中演示的那样)时,可提升应用程序性能。具体地讲,利用指针的算法特别适用于
NDK。NDK 的第二种常用用例是移植到为另一个平台(比如 Linux?)编写的现有 C 代码部分。本教程演示了
NDK,强调了性能和重用。
NDK 包含一个编译器和一些构建脚本,支持您关注 C 源文件并保留 NDK
安装的构建。可轻松将 NDK 构建流程包含到 Eclipse 开发环境中,在 自定义 Eclipse 部分进行了演示。
在开始应用程序之前,先简单介绍一些数字图像处理的基础知识。
数字图像处理的基础知识
现代计算机技术的一个很好的方面是数码摄影的出现和普及。数码摄影不仅仅是捕捉孩子正在玩耍的数码影像。可在随手拍摄的手机照片、高端的婚纱照相簿、宇宙深空图像和许多其他应用场景中找到数字图像。修改数字图像是我们的兴趣,并且是本教程的示例应用程序的核心功能。
数字图像操作有很多方式,包括但不限于以下操作:
裁剪 — 选择图像的一部分
缩放 — 更改图像的大小
旋转 — 更改图像的方向
转换 — 从一种格式转换为另一种格式
取样 — 更改图像的密度
混合/变形 — 更改图像的外观
筛选 — 提取图像的元素,比如颜色或频率
边缘检测 — 用于机器视觉应用程序来标识图像中的对象
压缩 — 降低图像的存储大小
通过像素操作增强图像:
直方图均衡化
对比度
亮度
其中一些操作是逐个像素进行的,而其他一些操作包含每次对图像的一小部分进行操作的矩阵数学模型。无论是什么操作,所有图像处理算法都包含处理原始图像数据。此教程演示了像素和矩阵操作的使用,以
C 编程语言编写,在 Android 设备上运行。
应用程序架构
本部分介绍此教程的示例应用程序的架构,从概述完成的项目开始,然后介绍其构建的每个主要步骤。您可以自己逐步重建应用程序,或者是从
参考资料 部分下载完整的项目。
完整项目
本教程演示了简单的图像处理应用程序 IBM Photo Phun 的构建。图
1 显示了 Eclipse IDE 的一个屏幕截图,通过展开项目来查看源文件和输出文件。
图 1. Eclipse 项目视图
应用程序的 UI 是使用传统 Android 开发技术构建的,使用了一个布局文件(main.xml)和一个
Activity,在 IBMPhotoPhun.java 中实现。位于项目主文件夹下方名为 jni 文件夹的一个
C 源文件中,包含图像处理例程。NDK 工具链将 C 源文件编译到一个名为 libibmphotophun.so
的共享库文件中。编译的库文件都存储在 libs 文件夹中。每个目标硬件平台或处理器架构都会创建一个库文件。表
1 枚举了应用程序的源文件。
表 1. 所需的应用程序源文件
如果您没有一个正在运行的 Android 开发环境,现在是安装 Android
工具的好时机。有关如何设置 Android 开发环境的更多信息,请参见 参考资料 获得所需工具的链接,以及其他一些关于开发
Android 应用程序的文章和教程。熟悉 Android 有助于了解本教程。
既然您已经了解了架构和应用程序,现在可以查看它在 Android 设备上运行时的外观。
演示应用程序
有时在开始时牢记结尾很有帮助,因此在您了解创建此应用程序的逐步过程之前,可快速操作一下。以下屏幕截图来自运行
Android 2.2 (Froyo) 的 Nexus One。图像是使用 Dalvik Debug Monitor
Service (DDMS) 工具捕获的,该工具作为 Android Developer Tools Eclipse
插件的一部分安装。
图 2 显示了装有示例图像的应用程序的主界面。快速查看以下图像,您会了解我如何得到一个程序设计器而不是其他一些电视节目,这归功于我这张“很上镜”的脸。在构建应用程序时可使用自己的图像替换此图像。
图 2. IBM Photo Phun 应用程序的主界面
屏幕顶部的按钮支持您更改图像。第一个按钮 Reset 将图像恢复为原始彩色图像。选择
Convert Image 按钮会将图像转换为灰度图像,如图 3 所示。
图 3. 灰度图像
Find Edges 按钮从原始彩色图像开始,将其转换为灰度图像,然后执行
Sobel Edge Detection 算法。图 4 显示了边缘检测算法的结果。
图 4. 检测边缘
边缘检测算法通常在机器视觉应用程序中使用,并且作为多步骤图像处理操作的第一个步骤。从这点来说,最后的两个按钮支持您通过更改每个像素的亮度来使图像变暗或变亮。图
5 显示了灰度图像的较亮版本。
图 5. 增加了亮度
图 6 显示的图像边缘较暗,降低了亮度
现在您可以开始处理图像了。
创建应用程序
在本部分中,我们将利用 Eclipse ADT 插件中提供的工具创建应用程序。即使不熟悉针对
Android 创建应用程序,您也能够轻松地从本部分中学到东西。参考资料 部分包含对创建 Android
应用程序很有用文章和教程。
ADT 新项目向导
在 Eclipse IDE 内部创建应用程序非常简单,这是因为有 ADT
新项目向导,如图 7 所示。
图 7. 创建新 Android 项目
填写新项目向导时,提供以下信息:
1.有效的项目名称。
2.目标版本。注意,对于此项目,您必须使用 Android V2.2 或
Android V2.3 作为目标 SDK 平台级别。
3.有效的应用程序名称。
4.软件包名称。
5.活动名称。
完成向导界面填写后,选择 Finish。单击 Next 按钮提示创建“Test”项目伴随此项目,这一步很有用,不过我们不进行介绍了。
将项目导入到 Eclipse 后,您可以实现此应用程序所需的源文件。可从应用程序的
UI 元素开始。
实现用户界面
此应用程序的 UI 非常简单。它包括一个 Activity,并具有一些
Button 小部件和一个 ImageView 小部件来显示所选图像。与许多 Android 应用程序一样,在
main.xml 文件中定义 UI,如清单 1 所示。
清单 1. UI 布局文件 main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ffffffff" > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnReset" android:text="Reset" android:visibility="visible" android:onClick="onResetImage" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnConvert" android:text="Convert Image" android:visibility="visible" android:onClick="onConvertToGray" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnFindEdges" android:text="Find Edges" android:visibility="visible" android:onClick="onFindEdges" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnDimmer" android:text="- " android:visibility="visible" android:onClick="onDimmer" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnBrighter" android:text=" +" android:visibility="visible" android:onClick="onBrighter" /> </LinearLayout> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:layout_gravity="center_vertical|center_horizontal" android:id="@+id/ivDisplay" /> </LinearLayout> |
注意两个 LinearLayout 元素的使用。外部元素控制 UI 的垂直流动,而内部元素
LinearLayout 设置其子元素的水平管理。水平布局元素包括界面顶部的所有 Button 小部件。ImageView
设置为使包含的图像居中,并且有一个 id 属性,支持您在运行时处理其内容。
每个 Button 小部件都有一个 onClick 属性。此属性的值必须对应于包含的
Activity 类中的公共无效方法,它具有一个 View 参数。此方法是设置单击处理函数的一种快速简单的方法,无需在运行时定义匿名处理函数或访问元素。参见
参考资料 了解使用此方法处理点击 Button 的更多信息。
在布局文件中定义了 UI 后,必须编写 Activity 代码以使用 UI。这在文件
IBMPhotoPhun.java 中实现,其中扩展了 Activity 类。您可以在清单 2 中查看代码。
清单 2. IBM Photo Phun 导入和类说明
/* * IBMPhotoPhun.java * * Author: Frank Ableson * Contact Info: fableson@navitend.com */
package com.msi.ibm.ndk;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.graphics.BitmapFactory;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.view.View;
import android.widget.ImageView;
public class IBMPhotoPhun extends Activity {
private String tag = "IBMPhotoPhun";
private Bitmap bitmapOrig = null;
private Bitmap bitmapGray = null;
private Bitmap bitmapWip = null;
private ImageView ivDisplay = null;
// NDK STUFF
static {
System.loadLibrary("ibmphotophun");
}
public native void convertToGray(Bitmap bitmapIn,Bitmap
bitmapOut);
public native void changeBrightness(int direction,Bitmap
bitmap);
public native void findEdges(Bitmap bitmapIn,Bitmap
bitmapOut);
// END NDK STUFF
/** Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Log.i(tag,"before image stuff");
ivDisplay = (ImageView) findViewById(R.id.ivDisplay);
// load bitmap from resources
BitmapFactory.Options options = new BitmapFactory.Options();
// Make sure it is 24 bit color as our image processing
algorithm
// expects this format
options.inPreferredConfig = Config.ARGB_8888;
bitmapOrig = BitmapFactory.decodeResource(this.getResources(),
R.drawable.sampleimage,options);
if (bitmapOrig != null)
ivDisplay.setImageBitmap(bitmapOrig);
}
public void onResetImage(View v) {
Log.i(tag,"onResetImage");
ivDisplay.setImageBitmap(bitmapOrig);
}
public void onFindEdges(View v) {
Log.i(tag,"onFindEdges");
// make sure our target bitmaps are happy
bitmapGray = Bitmap.createBitmap(bitmapOrig.getWidth(),bitmapOrig.getHeight(),
Config.ALPHA_8);
bitmapWip = Bitmap.createBitmap(bitmapOrig.getWidth(),bitmapOrig.getHeight(),
Config.ALPHA_8);
// before finding edges, we need to convert this
image to gray
convertToGray(bitmapOrig,bitmapGray);
// find edges in the image
findEdges(bitmapGray,bitmapWip);
ivDisplay.setImageBitmap(bitmapWip);
}
public void onConvertToGray(View v) {
Log.i(tag,"onConvertToGray");
bitmapWip = Bitmap.createBitmap(bitmapOrig.getWidth(),bitmapOrig.getHeight(),
Config.ALPHA_8);
convertToGray(bitmapOrig,bitmapWip);
ivDisplay.setImageBitmap(bitmapWip);
}
public void onDimmer(View v) {
Log.i(tag,"onDimmer");
changeBrightness(2,bitmapWip);
ivDisplay.setImageBitmap(bitmapWip);
}
public void onBrighter(View v) {
Log.i(tag,"onBrighter");
changeBrightness(1,bitmapWip);
ivDisplay.setImageBitmap(bitmapWip);
}
} |
可将此分为一些有名的注释:
1.有一些成员变量:
tag — 这在所有日志记录语句中使用,以在调试期间筛选 LogCat。
bitmapOrig — 此 Bitmap 保存原始彩色图像。
bitmapGray — 此 Bitmap 保存图像的灰度副本,并且仅在
findEdges 例程中暂时使用。
bitmapWip — 此 Bitmap 保存修改亮度值时的灰度图像。
ivDisplay — 此 ImageView 是对 main.xml
布局文件中定义的 ImageView 的引用。
2.“NDK Stuff”部分包括 4 行内容:
使用 System.loadLibrary 加载了包含本机代码的库。注意,此代码包含在名为
"static" 的代码块中。这将在应用程序启动时加载库。
convertToGray 的原型声明 — 此函数有两个参数。第一个是颜色
Bitmap,第二个是使用第一个参数的灰度版本填充的 Bitmap。
changeBrightness 的原型声明 — 此函数有两个参数。第一个是表示
up 或 down 的整数。第二个是逐个像素进行修改的 Bitmap。
findEdges 的原型声明。它有两个参数。第一个是灰度 Bitmap,第二个是接收图像“仅显示边缘”版本的
Bitmap。
3.onCreate 方法扩大了 R.layout.main 标识的布局,获得了对
ImageView (ivDisplay) 小部件的引用,然后加载了资源中的彩色图像。
BitmapFactory 方法有一个 options 参数,支持您加载
ARGB 格式的图像。“A”表示 alpha 通道,“RGB”表示红色、绿色、蓝色。许多开源图像处理库需要
24 位彩色图像,红色、绿色和蓝色各 8 位,并且每个像素由 RGB 三元组成。每个值的范围为 0 至
255。Android 平台上的图像保存为 32 位整数(alpha、红色、绿色和蓝色)。
加载了图像之后,会在 ImageView 中显示。
4.此类中的方法均衡对应于 Button 小部件的“click handlers”:
onResetImage 将原始彩色图像加载到 ImageView。
onConvertToGray 将目标 Bitmap 创建为 8 位图像,并调用
convertToGray 本机函数。生成的图像(bitmapWip)在 ImageView 中显示。
onFindEdges 创建两个中间 Bitmap 对象,将彩色图像转换为灰度图像并调用
findEdges 本机函数。生成的图像(bitmapWip)在 ImageView 中显示。
onDimmer 和 onBrighter 都调用 changeBrightness
方法来修改图像。生成的图像(bitmapWip)在 ImageView 中显示。
结束了 UI 代码。现在是您实现图像处理例程的时候了,但是首先我们需要创建库。
创建 NDK 文件
现在已经具备了 Android 应用程序的 UI 和应用逻辑,您需要实现图像处理函数。为此,您需要使用
NDK 创建 Java 本地库。在本例中,您将使用一些公共域 C 代码来实现图像处理函数并将它们打包到
Android 应用程序可用的库中。
构建本地库
NDK 创建了共享库并依赖于 makefile 系统。要为本项目构建本地库,您需要执行以下步骤:
在项目文件下创建一个名为 jni 的新文件夹。
在 jni 文件夹内,创建名为 Android.mk 的文件,该文件包含正确构建和命名库的
makefile 说明。
在 jni 文件夹内,创建源文件,会在 Android.mk 文件中引用该源文件。在本教程中,C
源文件的名称为 ibmphotophun.c。
清单 3 包含 Android.mk 文件的内容。
清单 3. Android.mk 文件
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := ibmphotophun LOCAL_SRC_FILES := ibmphotophun.c LOCAL_LDLIBS := -llog -ljnigraphics include $(BUILD_SHARED_LIBRARY) |
其中,该 makefile(片段)指示 NDK:
1.将 ibmphotophun.c 源文件编译到共享库中。
2.命名共享库。默认情况下,共享库命名约定为 lib<modulename>.so。因此,在这里将生成文件命名为
libibmphotophun.so。
3.指定所需的 "input" 库。共享库依赖于两个用于日志记录(liblog.so)和
jni 图形(libjnigraphics.so)的内置库文件,日志记录库允许您为 LogCat 添加条目,这在项目的开发阶段很有用。图形库为使用
Android 位图及其图像数据提供例程。
ibmphotophun.c 源文件包含一些 C 包含语句和 argb
类型(对应于 Android SDK 中的彩色数据类型)的定义。清单 4 显示了没有图像例程的 ibmphotophun.c,图像例程将在下一个清单中展示。
清单 4. Ibmphotophun.c 宏命令和包含命令
/* * ibmphotophun.c * * Author: Frank Ableson * Contact Info: fableson@msiservices.com */
#include <jni.h>
#include <android/log.h>
#include <android/bitmap.h>
#define LOG_TAG "libibmphotophun"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
typedef struct
{
uint8_t alpha;
uint8_t red;
uint8_t green;
uint8_t blue;
} argb; |
LOGI 和 LOGE 宏对 Logging 工具进行调用,并且在功能上分别相当于
Android SDK 中的 Log.i() 和 Log.e()。具有 typedef struct 关键字的
argb 数据类型支持 C 代码评估以 32 位整数存储的单个像素的 4 个数据元素。3 个包含语句为
C 编译器提供必要的声明,以进行 jni 粘合、日志记录和位图处理。
现在可以实现一些图像处理例程了,但是在我们检查代码之前,您需要了解 JNI
函数的命名约定。
当 Java 代码调用本机函数时,它将函数名称映射到一个展开或修饰函数,该函数由
JNI 共享库导出。下面是约定:Java_fully_qualified_classname_functionname。
例如,convertToGray 函数在 C 代码中实现为 Java_com_msi_ibm_ndk_IBMPhotoPhun_convertToGray。
JNI 函数的前两个参数包含一个 JNI 环境指针和调用类对象实例。有关
JNI 的更多信息,请参见 参考资料 部分。
构建库非常简单。打开终端(或 DOS)窗口并将目录更改到存储这些文件的 jni 文件夹。确保路径中有 NDK
并执行 ndk-build 脚本。此脚本包含构建库所需的所有粘合代码。生成的库放置在与 jni 文件夹级别相同的
libs 文件夹(例如,<project folder>/libs/)。
Eclipse 的 ADT 插件打包了 Android 应用程序后,会自动包含和传输库文件。会为每个支持的硬件平台都生成一个库文件。在运行时加载正确的库。
现在来看一下如何实现图像处理算法。
实现图像处理算法
此应用程序使用的图像处理例程根据各种公共域和学术例程以及我在图像处理方面的经验进行改编。两个函数使用像素操作,第三个函数使用最小矩阵方法。我们首先看一下清单
5 中的 convertToGray 函数。
清单 5. convertToGray 函数
/* convertToGray Pixel operation */ JNIEXPORT void JNICALL Java_com_msi_ibm_ndk_IBMPhotoPhun_convertToGray(JNIEnv * env, jobject obj, jobject bitmapcolor,jobject bitmapgray) { AndroidBitmapInfo infocolor; void* pixelscolor; AndroidBitmapInfo infogray; void* pixelsgray; int ret; int y; int x;
LOGI("convertToGray");
if ((ret = AndroidBitmap_getInfo(env, bitmapcolor,
&infocolor)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d",
ret);
return;
}
if ((ret = AndroidBitmap_getInfo(env, bitmapgray,
&infogray)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d",
ret);
return;
}
LOGI("color image :: width is %d; height
is %d; stride is %d; format is %d;flags is
%d",infocolor.width,infocolor.height,infocolor.stride,infocolor.format,infocolor.flags);
if (infocolor.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
{
LOGE("Bitmap format is not RGBA_8888 !");
return;
}
LOGI("gray image :: width is %d; height is
%d; stride is %d; format is %d;flags is
%d",infogray.width,infogray.height,infogray.stride,infogray.format,infogray.flags);
if (infogray.format != ANDROID_BITMAP_FORMAT_A_8)
{
LOGE("Bitmap format is not A_8 !");
return;
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapcolor,
&pixelscolor)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed !
error=%d", ret);
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapgray,
&pixelsgray)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed !
error=%d", ret);
}
// modify pixels with image processing algorithm
for (y=0;y<infocolor.height;y++) {
argb * line = (argb *) pixelscolor;
uint8_t * grayline = (uint8_t *) pixelsgray;
for (x=0;x<infocolor.width;x++) {
grayline[x] = 0.3 * line[x].red + 0.59 * line[x].green
+ 0.11*line[x].blue;
}
pixelscolor = (char *)pixelscolor + infocolor.stride;
pixelsgray = (char *) pixelsgray + infogray.stride;
}
LOGI("unlocking pixels");
AndroidBitmap_unlockPixels(env, bitmapcolor);
AndroidBitmap_unlockPixels(env, bitmapgray);
} |
此函数有两个调用 Java 代码的参数:ARGB 格式中的彩色 Bitmap
和接收彩色图像的灰度版本的 8 位灰度 Bitmap。下面是代码的简单介绍:
1.AndroidBitmapInfo 结构,在 bitmap.h 中定义,有助于了解
Bitmap 对象。
2.AndroidBitmap_getInfo 函数,在 jnigraphics
库中,获取有关具体 Bitmap 对象的信息。
3.下一步是确保传入到 convertToGray 函数的位图是想要的格式。
4.AndroidBitmap_lockPixels 函数锁定图像数据,这样您就可以直接在数据上执行操作。
5.AndroidBitmap_unlockPixels 函数解锁之前锁定的像素数据。这些函数应该被称为“锁定/解锁对”。
6.在锁定和解锁函数之前,您会看到像素操作。
指针 fun
以 C 语言编写的图像处理应用程序通常涉及指针的使用。指针是“指向”存储地址的变量。变量的数据类型指定您使用的存储类型和大小。例如,char
表示带符号的 8 位值,因此 char 指针 (char *) 支持您引用 8 位值,并通过该指针执行操作。图像数据表示为
uint8_t,这表示未带符号的 8 位值,其中每个字节的值范围为 0 至 255。3 个未带符号的 8
位值集合表示 24 位图像的图像数据的一个像素。
完成图像设计处理各行数据和在列之前移动。Bitmap 结构包含名为“跨距”的成员。跨距表示单个图像数据行的宽度(以字节为单位)。例如,带有
alpha 通道的 24 位彩色图像每个像素为 32 位或 4 字节。因此,宽度为 320 像素的图像,其跨距为
320*4 或 1,280 字节。8 位灰度图像的每个像素为 8 位或 1 字节。宽度为 320 像素的灰度图像,其跨距为
320*1 或 320 字节。记住此信息,我们看一下将彩色图像转换为灰度图像的图像处理算法:
1.当“锁定”图像数据时,对于输入彩色图像,名为 pixelscolor
的指针引用图像数据的基准地址;对于输出灰度图像,名为 pixelsgray 的指针引用图像数据的基准地址。
2.两个 for-next 循环支持您迭代整个图像。
1)首先,迭代图像的高,每次一“行”。使用 infocolor.height
值获得行的计数。
2)每迭代一行,指针就设置对应于行的图像数据第一“列”的存储位置。
3)当您迭代某一行的列时,就将彩色数据的每个像素转换为表示灰度值的单个值。
4)当转换了完成行时,您需要将指针指向下一行。这通过跳过跨距值来完成。
对于所有像素导向的图像处理操作,您需要按照上述格式进行。例如,考虑清单
6 所示的 changeBrightness 函数。
清单 6. changeBrightness 函数
/* changeBrightness Pixel Operation */ JNIEXPORT void JNICALL Java_com_msi_ibm_ndk_IBMPhotoPhun_changeBrightness(JNIEnv * env, jobject obj, int direction,jobject bitmap) { AndroidBitmapInfo infogray; void* pixelsgray; int ret; int y; int x; uint8_t save;
if ((ret = AndroidBitmap_getInfo(env, bitmap,
&infogray)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d",
ret);
return;
}
LOGI("gray image :: width is %d; height is
%d; stride is %d; format is %d;flags is
%d",infogray.width,infogray.height,infogray.stride,infogray.format,infogray.flags);
if (infogray.format != ANDROID_BITMAP_FORMAT_A_8)
{
LOGE("Bitmap format is not A_8 !");
return;
}
if ((ret = AndroidBitmap_lockPixels(env, bitmap,
&pixelsgray)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed !
error=%d", ret);
}
// modify pixels with image processing algorithm
LOGI("time to modify pixels....");
for (y=0;y<infogray.height;y++) {
uint8_t * grayline = (uint8_t *) pixelsgray;
int v;
for (x=0;x<infogray.width;x++) {
v = (int) grayline[x];
if (direction == 1)
v -=5;
else
v += 5;
if (v >= 255) {
grayline[x] = 255;
} else if (v <= 0) {
grayline[x] = 0;
} else {
grayline[x] = (uint8_t) v;
}
}
pixelsgray = (char *) pixelsgray + infogray.stride;
}
AndroidBitmap_unlockPixels(env, bitmap);
} |
此函数的操作方式与 convertToGray 函数非常相似,但具有以下不同之处:
1.此函数仅需要一个灰度位图。已修改了传入的图像。
2.此函数每次对每个像素增加或减少 5。此常量可以更改。我使用 5 是因为每次进行时会明显更改图像,无需总是按加号或减号按钮。
3.像素值被限制为 0 至 255。在直接使用未带符号的变量进行这些操作时要小心,因为很容易会“越界”。例如,当我使用
changeBrightness 函数为一个值(比如 252)增加了 5,而最终得到的实际有效值却是 2。得到的效果很有意思,但并不是我想要的。这就是我使用名为
v 的整数,并将像素数据映射到带符号整数,然后再将该值与 0 和 255 比较的原因。
还有一个图像处理算法需要检查:findEdges 函数,它的工作方式与之前的两个像素导向的函数有些不同。清单
7 展示了 findEdges 函数。
清单 7. findEdges 函数检测到图像中的轮廓
/* findEdges Matrix operation */ JNIEXPORT void JNICALL Java_com_msi_ibm_ndk_IBMPhotoPhun_findEdges(JNIEnv * env, jobject obj, jobject bitmapgray,jobject bitmapedges) { AndroidBitmapInfo infogray; void* pixelsgray; AndroidBitmapInfo infoedges; void* pixelsedge; int ret; int y; int x; int sumX,sumY,sum; int i,j; int Gx[3][3]; int Gy[3][3]; uint8_t *graydata; uint8_t *edgedata;
LOGI("findEdges running");
Gx[0][0] = -1;Gx[0][1] = 0;Gx[0][2] = 1;
Gx[1][0] = -2;Gx[1][1] = 0;Gx[1][2] = 2;
Gx[2][0] = -1;Gx[2][1] = 0;Gx[2][2] = 1;
Gy[0][0] = 1;Gy[0][1] = 2;Gy[0][2] = 1;
Gy[1][0] = 0;Gy[1][1] = 0;Gy[1][2] = 0;
Gy[2][0] = -1;Gy[2][1] = -2;Gy[2][2] = -1;
if ((ret = AndroidBitmap_getInfo(env, bitmapgray,
&infogray)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d",
ret);
return;
}
if ((ret = AndroidBitmap_getInfo(env, bitmapedges,
&infoedges)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d",
ret);
return;
}
LOGI("gray image :: width is %d; height is
%d; stride is %d; format is %d;flags is
%d",infogray.width,infogray.height,infogray.stride,infogray.format,infogray.flags);
if (infogray.format != ANDROID_BITMAP_FORMAT_A_8)
{
LOGE("Bitmap format is not A_8 !");
return;
}
LOGI("color image :: width is %d; height
is %d; stride is %d; format is %d;flags is
%d",infoedges.width,infoedges.height,infoedges.stride,infoedges.format,infoedges.flags);
if (infoedges.format != ANDROID_BITMAP_FORMAT_A_8)
{
LOGE("Bitmap format is not A_8 !");
return;
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapgray,
&pixelsgray)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed !
error=%d", ret);
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapedges,
&pixelsedge)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed !
error=%d", ret);
}
// modify pixels with image processing algorithm
LOGI("time to modify pixels....");
graydata = (uint8_t *) pixelsgray;
edgedata = (uint8_t *) pixelsedge;
for (y=0;y<=infogray.height - 1;y++) {
for (x=0;x<infogray.width -1;x++) {
sumX = 0;
sumY = 0;
// check boundaries
if (y==0 || y == infogray.height-1) {
sum = 0;
} else if (x == 0 || x == infogray.width -1) {
sum = 0;
} else {
// calc X gradient
for (i=-1;i<=1;i++) {
for (j=-1;j<=1;j++) {
sumX += (int) ( (*(graydata + x + i + (y + j)
* infogray.stride)) * Gx[i+1][j+1]);
}
}
// calc Y gradient
for (i=-1;i<=1;i++) {
for (j=-1;j<=1;j++) {
sumY += (int) ( (*(graydata + x + i + (y + j)
* infogray.stride)) * Gy[i+1][j+1]);
}
}
sum = abs(sumX) + abs(sumY);
}
if (sum>255) sum = 255;
if (sum<0) sum = 0;
*(edgedata + x + y*infogray.width) = 255 - (uint8_t)
sum;
}
}
AndroidBitmap_unlockPixels(env, bitmapgray);
AndroidBitmap_unlockPixels(env, bitmapedges);
} |
findEdges 例程与之前的两个函数有很多相同之处:
1.与 convertToGray 函数一样,此函数有两个位图参数,但是在本例中,两个位图参数都是灰度图像。
2.询问了位图以保证它们为所需的格式。
3.会相应地锁定和解锁位图像素。
4.该算法迭代源图像的行和列。
与之前的两个函数不同,此函数将每个像素与其周围的像素进行比较,而不是简单地对像素值本身进行数学操作。此函数中实现的算法是
Sobel Edge Detection 算法的变体。在本实现中,我将每个像素与其周围的一个像素进行了比较。此算法和其他算法的变体可使用更大的“边界”获得不同的结果。将每个像素与其周围的像素进行比较突出了像素之间的对比度,而且这样做强调了“边缘”。
我不会深入介绍此算法涉及的数学原理,这有两个原因。第一,它超出了本教程的讨论范围。第二,本教程的确切目的
— 重用现有 C 源代码— 由使用现有图像处理算法进行演示。您能够获得所需的结果,无需重新开始或将此代码移到
Java 技术。由于指针算法,C 是处理图像数据的理想环境。
自定义 Eclipse
使用 Eclipse IDE 的好处之一是很少需要编译。每次在 Eclipse
IDE 中保存文件时,就会自动构建项目。这非常适用于 Android SDK(即 Java)文件和 Android
XML 文件,但是对于 NDK 构建的库来说怎么样呢?我们来了解一下。
扩展 Eclipse 环境
如前所述,构建本地库和运行 ndk-build 命令一样简单。但是,除了简单的练习外,当处理其他任何项目时,像下面这样做会很麻烦,即跳出终端或命令窗口并执行
ndk-build 命令,返回到 Eclipse 环境,然后通过“单击”一个项目文件执行刷新,这会强制重新编译和重新打包完成的应用程序。解决方案是根据您的
Android 项目自定义构建设置以扩展 Eclipse 环境。
要修改构建设置,首先查看 Android 项目的属性并选择列表中的 Builders。添加一个新
Builder 并将其移动到列表顶部,如图 8 所示。
图 8. 修改构建设置
每个生成器都有 4 个配置选项卡。为您的生成器命名,比如将其命名为 Build
NDK Library,然后填充选项卡。第一个选项卡("Main")指定可执行工具的位置和工作目录。浏览到您
jni 文件夹中的 ndk-build 文件和工作目录,如图 9 所示。
图 9. 设置 NDK 的 Builder
属性
您只想使用 ndk-build 而不是 Eclipse 工作空间的其他内容操作此项目,因此设置
Refresh 选项卡,如图 10 所示。
图 10. 设置 Refresh 选项卡
只有在修改 Android.mk 文件或 ibmphotophun.c
文件时,才会想要重新构建库。为此,在 Build Options 选项卡中 Specify Resources
按钮的下方选择 jni 文件夹。此外,通过核对适当的时间指定何时想要构建工具,如图 11 所示。
图 11. 设置 build options
单击 OK 确认设置后,确保此 NDK 构建工具设置为列表中的第一个条目,方法是选择
Up 按钮,直到其位于 Builders 列表的顶部,如图 7 所示。
要测试是否正确设置了您的 Builder,打开 Eclipse 中的 ibmphotophun.c
源文件,方法是右键单击源文件,然后使用 Text Editor 打开它。进行简单的更改,然后保存文件。您应在控制台窗口中看到
NDK 工具链输出,如图 12 所示。如果您的 C 代码有错误,则它们显示为红色。
图 12. NDK 输出在 Eclipse
IDE 的控制台中显示
将 NDK 结合到您的构建流程中,您可以侧重于编写代码而不用了解构建环境太多。需要对应用逻辑进行更改?没问题,修改
Java 代码并保存文件。需要调整图像处理算法?不用担心,只需修改 C 例程并保存文件即可。Eclipse
和 ADT 插件会完成其他操作。
结束语
本教程展示了一个示例,解析如何使用 Android NDK 将由 C 编程语言编写的功能纳入到应用程序当中。本文所使用的这些功能示范了如何利用开源/公共领域图像处理算法。以相似的方式,可借助
Android NDK 使用任何与 Android 平台兼容的有效 C 代码。除了在 Eclipse 中使用
NDK 的机制的内容之外,您还学习了有关图像处理的一些基本概念。
|