开始之前
首先,了解 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 项目视图
![屏幕截图显示了一个展开视图,显示了 Eclipse 中 IBM Photo Phun 项目的所有元素](https://i-blog.csdnimg.cn/blog_migrate/ecbf4e079cfaed44e79b9d48a851e9f7.png)
应用程序的 UI 是使用传统 Android 开发技术构建的,使用了一个布局文件(main.xml)和一个 Activity,在 IBMPhotoPhun.java 中实现。位于项目主文件夹下方名为 jni 文件夹的一个 C 源文件中,包含图像处理例程。NDK 工具链将 C 源文件编译到一个名为 libibmphotophun.so 的共享库文件中。编译的库文件都存储在 libs 文件夹中。每个目标硬件平台或处理器架构都会创建一个库文件。表 1 枚举了应用程序的源文件。
表 1. 所需的应用程序源文件
文件 | 注释 |
---|---|
IBMPhotoPhun.java | 扩展 UI 的 Android Activity 类和应用逻辑 |
Ibmphotophun.c | 实现图像处理例程 |
main.xml | 应用程序 UI 的主页 |
AndroidManifest.xml | Android 应用程序的部署描述符 |
sampleimage.png | 用于演示目的的图像(可使用自己的图像替换此图像) |
Android.mk | NDK 用于构建 JNI 库的 makefile 片段 |
如果您没有一个正在运行的 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 应用程序的主界面
![Sandroid 设备的屏幕截图,在顶部显示了 Reset、Convert Image、Find Edges 和 + 按钮。剩余屏幕显示了作者的头像](https://i-blog.csdnimg.cn/blog_migrate/8cbdf1ad989b57f2ecf98dc3ccf3e5e4.png)
屏幕顶部的按钮支持您更改图像。第一个按钮 Reset 将图像恢复为原始彩色图像。选择 Convert Image 按钮会将图像转换为灰度图像,如图 3 所示。
图 3. 灰度图像
![屏幕截图显示了与图 2 相同的应用程序,但是现在图像是灰度图像](https://i-blog.csdnimg.cn/blog_migrate/32465fba7a3b8f56d08b1beb7b2d119e.png)
Find Edges 按钮从原始彩色图像开始,将其转换为灰度图像,然后执行 Sobel Edge Detection 算法。图 4 显示了边缘检测算法的结果。
图 4. 检测边缘
![屏幕截图显示了黑白图像,所有查找边缘都为白色,好像图像是使用木炭绘制的](https://i-blog.csdnimg.cn/blog_migrate/463940488ed672cc66f1148eeaabc407.png)
边缘检测算法通常在机器视觉应用程序中使用,并且作为多步骤图像处理操作的第一个步骤。从这点来说,最后的两个按钮支持您通过更改每个像素的亮度来使图像变暗或变亮。图 5 显示了灰度图像的较亮版本。
图 5. 增加了亮度
![屏幕截图显示的图像增加了图 4 中图像的亮度,呈现为一种较亮的灰色而不是黑色](https://i-blog.csdnimg.cn/blog_migrate/440eff5931a29e79209e7251311b19a3.png)
图 6 显示的图像边缘较暗。
图 6. 降低了亮度
![屏幕截图显示的图像降低了图 5 中图像的亮度。图像大部分是深灰色的,只用少量较亮的灰色线条勾勒了边缘,大多数细节都丢失了](https://i-blog.csdnimg.cn/blog_migrate/52f4303df87f3f65fb36b9a52d6b3cc1.png)
现在您可以开始处理图像了。
创建应用程序
在本部分中,我们将利用 Eclipse ADT 插件中提供的工具创建应用程序。即使不熟悉针对 Android 创建应用程序,您也能够轻松地从本部分中学到东西。参考资料 部分包含对创建 Android 应用程序很有用文章和教程。
ADT 新项目向导
在 Eclipse IDE 内部创建应用程序非常简单,这是因为有 ADT 新项目向导,如图 7 所示。
图 7. 创建新 Android 项目
![屏幕截图显示了 New Android Project 界面,其中 Build Target 设置为 Android 2.2](https://i-blog.csdnimg.cn/blog_migrate/46254ea07573c171f9cd46f08c20dc22.png)
填写新项目向导时,提供以下信息:
- 有效的项目名称。
- 目标版本。注意,对于此项目,您必须使用 Android V2.2 或 Android V2.3 作为目标 SDK 平台级别。
- 有效的应用程序名称。
- 软件包名称。
- 活动名称。
完成向导界面填写后,选择 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); } }
可将此分为一些有名的注释:
- 有一些成员变量:
-
tag
— 这在所有日志记录语句中使用,以在调试期间筛选LogCat
。 -
bitmapOrig
— 此Bitmap
保存原始彩色图像。 -
bitmapGray
— 此Bitmap
保存图像的灰度副本,并且仅在findEdges
例程中暂时使用。 -
bitmapWip
— 此Bitmap
保存修改亮度值时的灰度图像。 -
ivDisplay
— 此ImageView
是对 main.xml 布局文件中定义的ImageView
的引用。
-
- “NDK Stuff”部分包括 4 行内容:
- 使用
System.loadLibrary
加载了包含本机代码的库。注意,此代码包含在名为 "static" 的代码块中。这将在应用程序启动时加载库。 -
convertToGray
的原型声明 — 此函数有两个参数。第一个是颜色Bitmap
,第二个是使用第一个参数的灰度版本填充的Bitmap
。 -
changeBrightness
的原型声明 — 此函数有两个参数。第一个是表示up
或down
的整数。第二个是逐个像素进行修改的Bitmap
。 -
findEdges
的原型声明。它有两个参数。第一个是灰度Bitmap
,第二个是接收图像“仅显示边缘”版本的Bitmap
。
- 使用
-
onCreate
方法扩大了R.layout.main
标识的布局,获得了对 ImageView (ivDisplay
) 小部件的引用,然后加载了资源中的彩色图像。-
BitmapFactory
方法有一个options
参数,支持您加载 ARGB 格式的图像。“A”表示 alpha 通道,“RGB”表示红色、绿色、蓝色。许多开源图像处理库需要 24 位彩色图像,红色、绿色和蓝色各 8 位,并且每个像素由 RGB 三元组成。每个值的范围为 0 至 255。Android 平台上的图像保存为 32 位整数(alpha、红色、绿色和蓝色)。 - 加载了图像之后,会在
ImageView
中显示。
-
- 此类中的方法均衡对应于
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:
- 将 ibmphotophun.c 源文件编译到共享库中。
- 命名共享库。默认情况下,共享库命名约定为 lib<modulename>.so。因此,在这里将生成文件命名为 libibmphotophun.so。
- 指定所需的 "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
。下面是代码的简单介绍:
-
AndroidBitmapInfo
结构,在 bitmap.h 中定义,有助于了解Bitmap
对象。 -
AndroidBitmap_getInfo
函数,在 jnigraphics 库中,获取有关具体Bitmap
对象的信息。 - 下一步是确保传入到
convertToGray
函数的位图是想要的格式。 -
AndroidBitmap_lockPixels
函数锁定图像数据,这样您就可以直接在数据上执行操作。 -
AndroidBitmap_unlockPixels
函数解锁之前锁定的像素数据。这些函数应该被称为“锁定/解锁对”。 - 在锁定和解锁函数之前,您会看到像素操作。
指针 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 字节。记住此信息,我们看一下将彩色图像转换为灰度图像的图像处理算法:
- 当“锁定”图像数据时,对于输入彩色图像,名为
pixelscolor
的指针引用图像数据的基准地址;对于输出灰度图像,名为pixelsgray
的指针引用图像数据的基准地址。 - 两个
for-next
循环支持您迭代整个图像。- 首先,迭代图像的高,每次一“行”。使用
infocolor.height
值获得行的计数。 - 每迭代一行,指针就设置对应于行的图像数据第一“列”的存储位置。
- 当您迭代某一行的列时,就将彩色数据的每个像素转换为表示灰度值的单个值。
- 当转换了完成行时,您需要将指针指向下一行。这通过跳过跨距值来完成。
- 首先,迭代图像的高,每次一“行”。使用
对于所有像素导向的图像处理操作,您需要按照上述格式进行。例如,考虑清单 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
函数非常相似,但具有以下不同之处:
- 此函数仅需要一个灰度位图。已修改了传入的图像。
- 此函数每次对每个像素增加或减少 5。此常量可以更改。我使用 5 是因为每次进行时会明显更改图像,无需总是按加号或减号按钮。
- 像素值被限制为 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
例程与之前的两个函数有很多相同之处:
- 与
convertToGray
函数一样,此函数有两个位图参数,但是在本例中,两个位图参数都是灰度图像。 - 询问了位图以保证它们为所需的格式。
- 会相应地锁定和解锁位图像素。
- 该算法迭代源图像的行和列。
与之前的两个函数不同,此函数将每个像素与其周围的像素进行比较,而不是简单地对像素值本身进行数学操作。此函数中实现的算法是 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. 修改构建设置
![屏幕截图显示了在 Eclipse 中编辑 IBM Photo Phun 属性的 Builders 页面](https://i-blog.csdnimg.cn/blog_migrate/169c28cf102b936316fca962b934f9f0.png)
每个生成器都有 4 个配置选项卡。为您的生成器命名,比如将其命名为 Build NDK Library,然后填充选项卡。第一个选项卡("Main")指定可执行工具的位置和工作目录。浏览到您 jni 文件夹中的 ndk-build 文件和工作目录,如图 9 所示。
图 9. 设置 NDK 的 Builder 属性
![屏幕截图显示了 Main 选项卡,详细说明了 Eclipse 中 NDK 条目的编辑](https://i-blog.csdnimg.cn/blog_migrate/55714c8b6184df68c6731b1807bb2a06.png)
您只想使用 ndk-build 而不是 Eclipse 工作空间的其他内容操作此项目,因此设置 Refresh 选项卡,如图 10 所示。
图 10. 设置 Refresh 选项卡
![屏幕截图显示了 Refresh 选项卡,详细说明了 Eclipse 中 NDK 条目的编辑](https://i-blog.csdnimg.cn/blog_migrate/ccf5c1da22e8e7faae677e36f36e9daf.png)
只有在修改 Android.mk 文件或 ibmphotophun.c 文件时,才会想要重新构建库。为此,在 Build Options 选项卡中 Specify Resources 按钮的下方选择 jni 文件夹。此外,通过核对适当的时间指定何时想要构建工具,如图 11 所示。
图 11. 设置 build options
![屏幕截图显示了 Build Options 选项卡,详细说明了 Eclipse 中 NDK 条目的编辑](https://i-blog.csdnimg.cn/blog_migrate/ed7eb42ea799713d4be0207d0c8cc60b.png)
单击 OK 确认设置后,确保此 NDK 构建工具设置为列表中的第一个条目,方法是选择 Up 按钮,直到其位于 Builders 列表的顶部,如图 7 所示。
要测试是否正确设置了您的 Builder,打开 Eclipse 中的 ibmphotophun.c 源文件,方法是右键单击源文件,然后使用 Text Editor 打开它。进行简单的更改,然后保存文件。您应在控制台窗口中看到 NDK 工具链输出,如图 12 所示。如果您的 C 代码有错误,则它们显示为红色。
图 12. NDK 输出在 Eclipse IDE 的控制台中显示
![屏幕截图显示了在执行 gdb-setup 时 Eclipse 输出控制台的一个示例](https://i-blog.csdnimg.cn/blog_migrate/c5c4908fa4fb17d6bb0b14170db4caf5.png)
将 NDK 结合到您的构建流程中,您可以侧重于编写代码而不用了解构建环境太多。需要对应用逻辑进行更改?没问题,修改 Java 代码并保存文件。需要调整图像处理算法?不用担心,只需修改 C 例程并保存文件即可。Eclipse 和 ADT 插件会完成其他操作。
结束语
本教程展示了一个示例,解析如何使用 Android NDK 将由 C 编程语言编写的功能纳入到应用程序当中。本文所使用的这些功能示范了如何利用开源/公共领域图像处理算法。以相似的方式,可借助 Android NDK 使用任何与 Android 平台兼容的有效 C 代码。除了在 Eclipse 中使用 NDK 的机制的内容之外,您还学习了有关图像处理的一些基本概念。
下载
描述 | 名字 | 大小 |
---|---|---|
示例源代码 | os-androidndk-IBMPhotoPhun.source.zip | 995KB |