Sử dụng lại mã C với Bộ công cụ của nhà phát triển nguyên gốc của Android (Native Developer's Kit - NDK)

Học cách sử dụng bộ công cụ NDK

Bộ công cụ của nhà phát triển phần mềm (SDK-Software Developer Kit) Android được phần lớn các nhà phát triển ứng dụng Android sử dụng đòi hỏi sử dụng ngôn ngữ lập trình Java™. Tuy nhiên, có một khối lớn mã ngôn ngữ C có sẵn trực tuyến. Bộ công cụ của nhà phát triển nguyên gốc (NDK) Android cho phép một nhà phát triển Android tái sử dụng mã nguồn C hiện có trong một ứng dụng Android. Trong hướng dẫn này, bạn sẽ tạo ra một ứng dụng xử lý ảnh bằng ngôn ngữ lập trình Java, sử dụng mã C để thực hiện các phép toán xử lý ảnh cơ bản.

Frank Ableson, Thiết kế phần mềm

Sau khi sự nghiệp trong đội bóng rổ của trường cao đẳng kết thúc mà không có một hợp đồng dài hạn nhiều năm chơi cho đội LA Lakers, Frank Ableson đã chuyển chí hướng của mình sang thiết kế phần mềm máy tính. Ông thích giải quyết các vấn đề phức tạp, nhất là trong các lĩnh vực truyền thông và lập giao diện phần cứng. Khi không làm việc, ông dành thời gian với người vợ Nikki và con cái. Bạn có thể gặp Frank tại địa chỉ frank@cfgsolutions.com.



05 01 2012

Trước khi bạn bắt đầu

Một trong những động lực để khám phá NDK ngay từ đầu là cơ hội để sử dụng các dự án mã nguồn mở, nhiều dự án trong số đó được viết bằng C. Sau khi hoàn thành hướng dẫn này, bạn sẽ học được cách tạo một thư viện giao diện nguyên gốc Java (JNI), được viết bằng C và được biên dịch bằng NDK, và kết hợp thư viện đó vào một ứng dụng Android được viết bằng ngôn ngữ Java. Ứng dụng này giải thích cách thực hiện các phép xử lý ảnh cơ bản đối với dữ liệu ảnh thô. Bạn cũng sẽ tìm hiểu cách mở rộng môi trường xây dựng của Eclipse để tích hợp một dự án NDK vào một tệp dự án SDK của Android. Từ nền cơ sở này, bạn sẽ được trang bị tốt hơn để chuyển mã nguồn mở hiện có vào nền tảng Android.

Về hướng dẫn này

Hướng dẫn này giới thiệu NDK của Android trong môi trường Eclipse. NDK được sử dụng để thêm chức năng cho một ứng dụng Android bằng cách sử dụng ngôn ngữ lập trình C. Hướng dẫn này bắt đầu bằng việc xem xét NDK ở mức cao và các kịch bản sử dụng phổ biến của nó. Từ đó, chủ đề về xử lý ảnh được giới thiệu, tiếp theo là một giới thiệu và trình diễn ứng dụng trong hướng dẫn này: IBM Photo Phun. Ứng dụng này là một sự kết hợp của mã Java dựa trên-SDK và mã C do NDK biên dịch. Hướng dẫn này chuyển sang giới thiệu giao diện nguyên gốc Java (JNI), là công nghệ thú vị khi làm việc với NDK. Việc xem trước các tệp nguồn của dự án đã hoàn thành cung cấp một lộ trình cho ứng dụng đã xây dựng ở đây. Sau đó, theo cách làm từng bước một, bạn sẽ xây dựng ứng dụng này. Các tệp lớp Java và mã nguồn C được giải thích. Để kết thúc, môi trường xây dựng của Eclipse sẽ được tùy chỉnh để tích hợp chuỗi công cụ NDK trực tiếp vào quy trình xây dựng của Eclipse rất dễ sử dụng.

Điều kiện cần trước

Để làm theo hướng dẫn này, bạn cần quen thuộc với việc xây dựng các ứng dụng Android bằng SDK của Android và có một sự hiểu biết cơ bản về ngôn ngữ lập trình C. Ngoài ra, bạn sẽ cần những thứ sau đây:

  • Eclipse và Android Developer Tools (ADT-Các công cụ của nhà phát triển Android) — Trình soạn thảo mã chính, trình biên dịch Java, và Trình cắm thêm của Các công cụ phát triển Android.
  • Bộ công cụ của Android Software Developer (SDK-Bộ công cụ của nhà phát triển phần mềm Android).
  • Bộ công cụ của Android Native Developer (NDK-Bộ công cụ của nhà phát triển nguyên gốc Android).
  • Ảnh PNG — Ảnh được sử dụng để thử nghiệm các hoạt động xử lý ảnh.

Tôi tạo ra các mẫu mã cho hướng dẫn này trên một MacBook Pro với Eclipse V3.4.2 và Android SDK V8, hỗ trợ bản phát hành Android có nhãn 2.2 (Froyo). Bản phát hành NDK được sử dụng trong hướng dẫn này là r4b. Mã này đòi hỏi phiên bản r4b hoặc mới hơn vì các khả năng xử lý ảnh của NDK của Android không có sẵn trong các phiên bản trước của NDK.

Xem phần Tài nguyên để biết các liên kết đến những công cụ này.


NDK của Android

Hãy bắt đầu bằng xem xét NDK của Android và cách có thể sử dụng nó để tăng cường nền tảng Android. Trong khi SDK Android cung cấp một môi trường lập trình rất phong phú, thì NDK của Android mở rộng tầm bao quát và có thể đẩy nhanh tốc độ phân phối các chức năng mong muốn bằng cách mang tới thêm các mã nguồn hiện có, một số trong đó có thể là quyền sở hữu riêng và một số trong đó có thể là mã nguồn mở.

NDK

NDK là một bản phát hành phần mềm có sẵn dưới dạng bản tải về miễn phí từ trang web của Android. NDK bao gồm tất cả các thành phần cần thiết để kết hợp các chức năng được viết bằng C vào một ứng dụng Android. Bản phát hành ban đầu của NDK đã chỉ cung cấp chức năng nguyên thủy nhất với các hạn chế đáng kể. Với mỗi bản phát hành kế tiếp, NDK đã mở rộng các khả năng của mình. Như với r5 của ứng dụng NDK, các tác giả có thể viết một phần đáng kể của một ứng dụng trực tiếp bằng C, bao gồm giao diện người dùng và khả năng xử lý sự kiện. Các tính năng cho phép chức năng xử lý ảnh đã mô tả ở đây đã được đưa vào phiên bản r4b của NDK.

Hai cách sử dụng NDK phổ biến là tăng hiệu năng của ứng dụng và tận dụng mã C hiện có bằng cách chuyển nó sang Android. Đầu tiên ta hãy xem xét về cải thiện hiệu năng. Viết mã bằng C không đảm bảo làm tăng đáng kể về hiệu năng. Thực vậy, mã nguyên gốc C nếu viết kém thực sự có thể làm chậm một ứng dụng khi so sánh với một ứng dụng Java được viết tốt. Có sẵn khả năng cải tiến hiệu năng của ứng dụng khi sử dụng các hàm được viết khéo léo, cẩn thận bằng C để thực hiện các hoạt động dựa trên bộ nhớ hoặc thực hiện nhiều tính toán như các hoạt động trình diễn trong hướng dẫn này. Nói riêng, các thuật toán có sử dụng số học con trỏ đặc biệt sẵn sàng để sử dụng với NDK. Trường hợp sử dụng NDK phổ biến thứ nhì là chuyển một phần thân hiện có của mã C đã viết cho một nền tảng khác, ví dụ như Linux®. Hướng dẫn này trình diễn NDK theo cách làm nổi bật hiệu năng và các trường hợp tái sử dụng.

NDK có một trình biên dịch và các kịch bản xây dựng, cho phép bạn tập trung vào các tệp nguồn C và để lại phép thuật xây dựng cho bản cài đặt NDK. Quá trình xây dựng bằng NDK có thể dễ dàng được tích hợp vào môi trường phát triển Eclipse, được trình bày trong phần Tùy chỉnh Eclipse.

Trước khi nhảy vào bản thân ứng dụng, chúng ta hãy rẽ sang ngang một chút để thảo luận một vài điều cơ bản về xử lý ảnh kỹ thuật số.

Cơ bản về xử lý ảnh kỹ thuật số

Một khía cạnh thú vị của công nghệ máy tính hiện đại là sự xuất hiện và có mặt khắp nơi của chụp ảnh kỹ thuật số. Có nhiều thứ về chụp ảnh kỹ thuật số hơn là chỉ đơn giản chụp ảnh con cái bạn đang làm một cái gì đó dễ thương. Các ảnh kỹ thuật số có ở khắp mọi nơi từ những bức ảnh trên điện thoại di động đơn sơ đến các tập ảnh cưới cao cấp, các ảnh vũ trụ, và nhiều ứng dụng khác. Các ảnh kỹ thuật số dễ dàng chụp, trao đổi, và thậm chí sửa đổi. Việc sửa đổi một ảnh kỹ thuật số là mối quan tâm của chúng ta ở đây và nó là chức năng cốt lõi của ứng dụng mẫu của hướng dẫn này.

Việc xử lý ảnh kỹ thuật số xảy ra theo rất nhiều cách, bao gồm nhưng không giới hạn chỉ là các hoạt động sau đây:

  • Xén ảnh— Trích xuất một phần của một ảnh
  • Co dãn ảnh— Thay đổi kích thước của một ảnh
  • Xoay ảnh— Thay đổi hướng của một ảnh
  • Chuyển đổi ảnh— Chuyển đổi từ một định dạng này sang một định dạng khác
  • Lấy mẫu ảnh— Thay đổi mật độ của một ảnh
  • Trộn/Kỹ xảo biến hình ảnh— Thay đổi vẻ ngoài của một ảnh
  • Lọc ảnh— Trích xuất các chi tiết của một ảnh, như các màu hoặc tần số
  • Phát hiện đường viền của ảnh— Thường sử dụng cho các ứng dụng thị giác máy tính để nhận biết các đối tượng trong ảnh
  • Nén ảnh— Giảm kích thước lưu trữ của một ảnh
  • Làm tăng chất lượng của một ảnh thông qua các phép toán điểm ảnh (pixel):
    • Cân bằng hoành đồ
    • Tương phản
    • Độ sáng

Một số các phép toán này được thực hiện trên cơ sở từng điểm ảnh, trong khi các phép toán khác liên quan đến phép toán ma trận để làm việc trên các phần nhỏ của ảnh tại một thời điểm. Bất kể các phép toán này, tất cả các thuật toán xử lý ảnh đều liên quan đến làm việc với dữ liệu ảnh thô. Hướng dẫn này trình diễn việc sử dụng các phép toán điểm ảnh và ma trận theo ngôn ngữ lập trình C, chạy trên một thiết bị Android.


Kiến trúc của ứng dụng

Phần này khám phá kiến trúc của ứng dụng mẫu của hướng dẫn này, bắt đầu bằng xem xét thoáng qua ở mức cao về dự án đã hoàn thành, sau đó tiến hành qua từng bước chính trong việc xây dựng nó. Bạn có thể làm theo từng bước một để tự mình xây dựng lại ứng dụng hoặc bạn có thể tải về dự án đã hoàn thành từ phần Tài nguyên.

Dự án đã hoàn thành

Hướng dẫn này cho thấy việc xây dựng một ứng dụng xử lý ảnh đơn giản, IBM Photo Phun. Hình 1 cho thấy một ảnh chụp màn hình từ IDE Eclipse với dự án đã mở rộng để xem các tệp nguồn và các tệp đầu ra.

Hình 1. Khung nhìn dự án của Eclipse
Ảnh chụp màn hình hiển thị một khung nhìn mở rộng trong Eclipse của tất cả các phần tử của dự án IBM Photo Phun

Giao diện người dùng của ứng dụng được xây dựng bằng các kỹ thuật phát triển Android truyền thống, bằng cách sử dụng một tệp bố cục duy nhất (main.xml) và một Activity (Hoạt động) duy nhất, được thực hiện trong tệp IBMPhotoPhun.java. Tệp nguồn C duy nhất, nằm trong một thư mục tên là jni, bên dưới thư mục chính của dự án, chứa các thường trình xử lý ảnh. Chuỗi công cụ NDK biên dịch tệp nguồn C thành một tệp thư viện chia sẻ tên là libibmphotophun.so. (Các) tệp thư viện đã biên dịch được lưu trữ trong thư mục libs. Một tệp thư viện được tạo ra cho mỗi nền tảng phần cứng đích hoặc kiến trúc bộ xử lý đích. Bảng 1 liệt kê các tệp nguồn của ứng dụng..

Bảng 1. Các tệp nguồn ứng dụng cần thiết
TệpChú thích
IBMPhotoPhun.javaMở rộng lớp Activity của Android cho giao diện người dùng và logic ứng dụng.
Ibmphotophun.cThực hiện các thường trình xử lý ảnh.
main.xmlTrang chủ của giao diện người dùng ứng dụng.
AndroidManifest.xmlTệp mô tả triển khai cho ứng dụng Android
sampleimage.pngẢnh được sử dụng cho mục đích trình diễn (xin cứ tự nhiên thay thế một ảnh của riêng bạn).
Android.mkẢnh được sử dụng cho mục đích trình diễn (xin cứ tự nhiên thay thế một ảnh của riêng bạn).

Nếu bạn không có một môi trường phát triển Android đang hoạt động, bây giờ là đúng lúc nhất để cài đặt các công cụ Android. Để biết thêm thông tin về cách thiết lập một môi trường phát triển Android, xem phần Tài nguyên để biết các liên kết đến các công cụ cần thiết, cộng với một số bài giới thiệu và các hướng dẫn về phát triển các ứng dụng cho Android. Làm quen với Android là có ích để hiểu hướng dẫn này.

Bây giờ bạn có một cái nhìn tổng quan về kiến trúc và ứng dụng, bạn có thể thấy nó trông ra sao khi chạy trên một thiết bị Android.

Trình diễn ứng dụng

Đôi khi sẽ rất hữu ích nếu khởi đầu từ đích muốn đạt đến trong tâm trí, vì vậy trước khi bạn đi sâu vào quá trình tạo ứng dụng này theo từng bước một, hãy xem qua nó hoạt động ra sao. Các ảnh chụp màn hình sau đây đã được chụp từ một máy Nexus One chạy Android 2.2 (Froyo). Các ảnh này đã được chụp bằng cách sử dụng công cụ DDMS (Dalvik Debug Monitor Service - Dịch vụ giám sát gỡ rối Dalvik), cài đặt như là một phần của Trình cắm thêm Eclipse của Các công cụ của nhà phát triển Android).

Hình 2 cho thấy màn hình chủ của ứng dụng với ảnh mẫu đã tải về. Hãy xem qua ảnh và bạn sẽ hiểu tại sao tôi đã làm một lập trình viên chứ không phải người dẫn một chương trình truyền hình nào đó, nhờ khuôn mặt "rất hợp với đài phát thanh" của tôi. Xin cứ tự nhiên thay thế ảnh riêng của mình khi bạn tự xây dựng ứng dụng này.

Hình 2. Màn hình chủ của ứng dụng IBM Photo Phun
Ảnh chụp màn hình của thiết bị android hiển thị các nút nhấn để thiết lập lại, Chuyển đổi ảnh, tìm các đường viền, và + ở trên đỉnh. phần còn lại của màn hình hiển thị ảnh chụp thẳng của tác giả

Các nút trên đầu màn hình cho phép bạn thay đổi ảnh. Nút đầu tiên, (đặt lại), khôi phục ảnh trở về ảnh màu ban đầu này. Chọn nút Convert Image (Chuyển đổi ảnh) chuyển đổi ảnh đó thành ảnh đen trắng theo thang độ xám, như trong Hình 3.

Hình 3. Ảnh đen trắng theo thang độ xám
Ảnh chụp màn hình hiển thị cùng ứng dụng như hình 2, nhưng bây giờ ảnh là âm bản xám

Nút Find Edges (Tìm các đường viền) bắt đầu với ảnh màu ban đầu, chuyển đổi nó thành ảnh đen trắng theo thang độ xám, sau đó thực hiện một thuật toán phát hiện đường viền Sobel. Hình 4 cho thấy kết quả của thuật toán phát hiện đường viền.

Hình 4. Phát hiện các đường viền
Ảnh chụp màn hình hiển thị ảnh đen trắng với tất cả các đường viền đã thấy được viền bằng màu trắng, cung cấp một dáng vẻ âm bản được vẽ bằng chì đen

Các thuật toán phát hiện đường viền thường được sử dụng trong các ứng dụng thị giác máy tính như là một bước sơ bộ trong một hoạt động xử lý ảnh nhiều bước. Từ thời điểm này, hai nút cuối cùng cho phép bạn làm cho ảnh sẫm hơn hoặc nhạt hơn hơn bằng cách thay đổi độ sáng của mỗi điểm ảnh. Hình 5 cho thấy một phiên bản nhạt hơn của ảnh đen trắng theo thang độ xám.

Hình 5. Tăng độ sáng
Ảnh chụp màn hình hiển thị ảnh từ Hình 4 với độ sáng được tăng thêm, biểu thị màu xám nhạt, chứ không phải màu đen

Hình 6 cho thấy ảnh với các đường viền sẫm hơn một chút.

Hình 6. Giảm độ sáng
Ảnh chụp màn hình hiển thị ảnh từ Hình 5 với độ sáng giảm đi. Ảnh màu xám tối nhạt có một vài nét xám nhạt vẫn còn vạch của một vài đường viền và hầu hết các chi tiết bị mất

Bây giờ đến lượt của bạn.


Tạo ứng dụng

Trong phần này, chúng ta sẽ tạo ứng dụng bằng cách sử dụng các công cụ được cung cấp trong trình cắm thêm Eclipse của ADT. Thậm chí nếu bạn chưa quen với việc tạo các ứng dụng cho Android, bạn sẽ có thể làm theo khá dễ dàng và học hỏi từ phần này. Phần Tài nguyên chứa các bài viết có ích và các hướng dẫn về những điều cơ bản của việc tạo các ứng dụng Android.

Trình hướng dẫn dự án mới của ADT

Việc tạo ứng dụng trong IDE Eclipse là rất đơn giản, nhờ trình hướng dẫn tạo dự án mới của ADT, như trong Hình 7.

Hình 7. Tạo một dự án Android mới
Ảnh chụp màn hình hiển thị màn hình Android Project mới với việc thiết lập Build Target tới Android 2.2

Khi điền vào trình hướng dẫn dự án mới, hãy cung cấp thông tin sau:

  1. Tên dự án hợp lệ.
  2. Đích xây dựng. Lưu ý với dự án này, bạn phải sử dụng Android V2.2 hoặc Android V2.3 làm mức nền tảng SDK đích.
  3. Tên ứng dụng hợp lệ.
  4. Tên gói.
  5. Tên Activity (Hoạt động).

Một khi bạn đã điền thông số vào trình hướng dẫn, chọn Finish (Kết thúc). Việc nhấn nút Next nhắc bạn tạo ra một dự án "Test" đi kèm với dự án này, đó là một bước có ích, nhưng đây là một câu chuyện khác.

Một khi dự án đã ở trong Eclipse, bạn đã sẵn sàng để thực hiện các tệp nguồn cần thiết cho ứng dụng này. Bạn sẽ bắt đầu với các phần tử của UI (giao diện người dùng) của ứng dụng.

Thực hiện giao diện người dùng

Giao diện người dùng cho ứng dụng này khá đơn giản. Nó chứa chỉ một Activity với một ít các tiện ích (widget) Button (nút nhấn) và một tiện ích ImageView (Xem ảnh) để hiển thị ảnh đã chọn. Giống như nhiều ứng dụng Android, Giao diện người dùng được định nghĩa trong tệp main.xml, hiển thị trong Liệt kê 1.

Liệt kê 1. Tệp bố cục 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>

Lưu ý việc sử dụng hai phần tử LinearLayout. Phần tử bên ngoài kiểm soát dòng chảy thẳng đứng của giao diện người dùng và phần tử LinearLayout bên trong được thiết lập để quản lý các con của nó theo chiều ngang. Phần tử bố cục ngang nắm giữ tất cả các tiện ích Button trên đầu màn hình. Tiện ích ImageView được đặt ở trung tâm ảnh đã có và có một thuộc tính id, cho phép bạn xử lý các nội dung của nó trong thời gian chạy.

Mỗi tiện ích Button có một thuộc tính onClick. Giá trị của thuộc tính này phải tương ứng với một phương thức công cộng không có giá trị trả về (public void) trong lớp Activity chứa chúng, và chỉ nhận một đối số View. Đây là một cách tiếp cận nhanh chóng và dễ dàng để thiết lập các trình xử lý nhấn chuột mà không gặp rắc rối về định nghĩa các trình xử lý ẩn danh hay truy cập vào phần tử trong thời gian chạy. Xem phần Tài nguyên để biết thêm thông tin về phương thức này để xử lý các lần nhấn Button.

Sau khi đã định nghĩa giao diện người dùng trong tệp bố cục, cần phải viết mã Activity để làm việc với giao diện người dùng. Điều này được thực hiện trong tệp IBMPhotoPhun.java, ở đây lớp Activity được mở rộng. Bạn có thể xem mã trong Liệt kê 2.

Liệt kê 2. Nhập khẩu và khai báo lớp của 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);
    }   
}

Hãy phân tích việc này thành một số nhận xét đáng chú ý:

  1. Có một số ít các biến thành viên:
    • tag— Biến này được sử dụng trong tất cả các câu lệnh ghi nhật ký để giúp lọc LogCat trong quá trình gỡ lỗi.
    • bitmapOrig— Biến Bitmap này lưu giữ ảnh màu ban đầu.
    • bitmapGray— Biến Bitmap này là một bản sao của ảnh đen trắng theo thang độ xám và chỉ được sử dụng tạm thời trong thường trình findEdges.
    • bitmapWip— Biến Bitmap là ảnh đen trắng theo thang độ xám đã sửa đổi khi sửa đổi các giá trị độ sáng.
    • ivDisplay— Biến ImageView này là một tham chiếu đến ImageView được định nghĩa trong tệp bố cục main.xml.
  2. Phần "NDK Stuff" bao gồm bốn dòng:
    • Thư viện có chứa mã nguyên gốc của chúng ta được nạp bằng một cuộc gọi đến System.loadLibrary. Lưu ý rằng đoạn mã này được chứa trong một khối đánh dấu là "static" (tĩnh). Điều này làm cho thư viện được nạp khi khởi động ứng dụng.
    • Khai báo nguyên mẫu của hàm convertToGray— Hàm này có hai tham số. Tham số đầu tiên là một ảnh màu Bitmap và tham số thứ hai là một ảnh Bitmap phiên bản đen trắng theo thang độ xám của ảnh đầu tiên.
    • Khai báo nguyên mẫu của hàm changeBrightness— Hàm này có hai tham số. Tham số đầu tiên là một số nguyên thể hiện up hoặc down (tăng lên hoặc giảm xuống). Tham số thứ hai là một ảnh Bitmap được sửa đổi trên cơ sở theo từng điểm ảnh.
    • Khai báo nguyên mẫu của hàm findEdges. Hàm có hai tham số. Tham số đầu tiên là một ảnh Bitmap đen trắng theo thang độ xám và tham số thứ hai là một ảnh Bitmap là phiên bản "chỉ các đường viền" của ảnh này.
  3. Phương thức onCreate khuyếch trương bố cục do R.layout.main định nghĩa, nhận được một tham chiếu đến tiện ích (ivDisplay), sau đó tải ảnh màu từ nguồn tài nguyên.
    • Phương thức BitmapFactory có một tham số options (các tùy chọn) cho phép bạn tải ảnh theo định dạng ARGB. Chữ "A" là viết tắt cho kênh alpha và "RGB" là viết tắt tương ứng của màu đỏ, xanh lá cây, xanh dương. Nhiều thư viện xử lý ảnh mã nguồn mở nhận đầu vào là một ảnh màu 24-bit, tám bit cho từng màu đỏ, xanh lá cây và xanh dương, mỗi điểm ảnh là một bộ ba màu RGB. Mỗi giá trị có dải từ 0 đến 255. Các ảnh trên nền tảng Android được lưu trữ như một số nguyên 32-bit với alpha, đỏ, xanh lá cây, xanh dương.
    • Sau khi ảnh được nạp, nó được hiển thị trong ImageView.
  4. Sự cân bằng của các phương thức trong lớp này tương ứng với "các trình xử lý nhấn chuột" cho các tiện ích Button:
    • onResetImage tải ảnh màu ban đầu vào ImageView.
    • onConvertToGray tạo Bitmap đích là một ảnh 8-bit và gọi hàm nguyên gốc convertToGray. Ảnh kết quả (bitmapWip) được hiển thị trong ImageView.
    • onFindEdges tạo ra hai đối tượng Bitmap trung gian, chuyển đổi ảnh màu thành một ảnh đen trắng theo thang độ xám và gọi hàm nguyên gốc findEdges. Ảnh kết quả (bitmapWip) được hiển thị trong ImageView.
    • onDimmeronBrighter gọi phương thức changeBrightness để thay đổi ảnh. Ảnh kết quả (bitmapWip) được hiển thị trong ImageView.

Đó là tóm tắt về mã giao diện người dùng. Bây giờ là lúc để bạn thực hiện các thường trình xử lý ảnh, nhưng trước tiên, chúng ta cần tạo ra thư viện đã.


Tạo các tệp NDK

Bây giờ giao diện người dùng của ứng dụng Android và logic ứng dụng đã sẵn sàng, bạn cần thực hiện các hàm xử lý ảnh. Để làm điều này, bạn cần tạo một thư viện Java nguyên gốc với NDK. Trong trường hợp này, bạn sẽ sử dụng một số đoạn mã C miền công cộng để thực hiện các hàm xử lý ảnh và đóng gói chúng vào một thư viện có thể sử dụng được với ứng dụng Android.

Xây dựng thư viện nguyên gốc

NDK tạo ra các thư viện chia sẻ và dựa trên một hệ thống tệp thực hiện (makefile). Để xây dựng thư viện nguyên gốc cho các dự án này, bạn cần phải thực hiện các bước sau:

  1. Tạo một thư mục mới có tên là jni bên dưới tệp dự án của bạn.
  2. Trong thư mục jni, tạo một tệp có tên là Android.mk, trong đó có chứa các chỉ thị của tệp thực hiện để xây dựng và đặt tên đúng cho thư viện của bạn.
  3. Trong thư mục jni, tạo tệp nguồn, được tham chiếu trong tệp Android.mk. Tên của tệp nguồn C cho hướng dẫn này là ibmphotophun.c.

Liệt kê 3 chứa các nội dung của tệp Android.mk.

Liệt kê 3. Tệp 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)

Trong số những thứ khác, tệp thực hiện này (đoạn mã) chỉ thị cho NDK để:

  1. Biên dịch tệp nguồn ibmphotophun.c vào một thư viện chia sẻ.
  2. Đặt tên thư viện chia sẻ. Theo mặc định, quy ước đặt tên thư viện chia sẻ là lib<tên mô đun>.so. Vì vậy, ở đây đặt tên tệp kết quả là libibmphotophun.so.
  3. Quy định các thư viện "đầu vào" cần thiết. Thư viện chia sẻ dựa vào hai tệp thư viện dựng sẵn dành cho ghi nhật ký (liblog.so) và dành cho đồ họa jni (libjnigraphics.so). Thư viện ghi nhật ký cho phép bạn thêm các mục vào LogCat, rất có ích trong giai đoạn phát triển dự án của bạn. Thư viện đồ họa cung cấp các thường trình để làm việc với các bitmap của Android và dữ liệu ảnh của chúng.

Tệp nguồn ibmphotophun.c chứa một vài đoạn mã C bao gồm các câu lệnh và các định nghĩa của kiểu argb, tương ứng với kiểu dữ liệu màu sắc (Color) trong SDK của Android. Liệt kê 4 cho thấy tệp ibmphotophun.c không có thường trình ảnh, vì được trình bày ở dưới.

Liệt kê 4. Các include (lệnh bao gồm) và các macro của tệp 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;

Các macro LOGILOGE thực hiện cuộc gọi đến tiện ích Logging và tương đương với chức năng tương ứng của Log.i()Log.e() trong SDK của Android. Kiểu dữ liệu argb được định nghĩa với các từ khóa cấu trúc (struct) typedef cho phép mã C truy cập vào bốn phần tử dữ liệu của một điểm ảnh riêng lẻ được lưu trữ dưới dạng một số nguyên 32-bit. Ba câu lệnh include cung cấp các khai báo cần thiết cho trình biên dịch C dành cho xử lý keo dán (glue) jni, ghi nhật ký và xử lý bitmap, lần lượt tương ứng.

Bây giờ là lúc để bạn thực hiện một số thường trình xử lý ảnh, nhưng trước khi chúng ta xem xét chính những mã đó, bạn cần hiểu quy ước đặt tên các hàm JNI.

Khi mã Java gọi một hàm nguyên gốc, nó ánh xạ tên hàm đến một hàm đã mở rộng hoặc đã trang trí, được xuất khẩu bởi thư viện JNI chia sẻ. Dưới đây là quy ước: Java_fully_qualified_classname_functionname.

Ví dụ, thực hiện hàm convertToGray trong mã C như Java_com_msi_ibm_ndk_IBMPhotoPhun_convertToGray.

Hai đối số đầu tiên cho hàm JNI bao gồm một con trỏ đến môi trường JNI và đến cá thể đối tượng lớp đang gọi. Để biết thêm thông tin về JNI, xin vui lòng xem phần Tài nguyên.

Việc xây dựng thư viện này khá đơn giản. Mở một cửa sổ đầu cuối (hay DOS) và chuyển đến thư mục jni, ở đây bạn đã lưu các tệp này. Hãy chắc chắn rằng NDK có trong đường dẫn của bạn và chạy thi hành kịch bản lệnh ndk-build. Kịch bản lệnh này chứa tất cả keo dán cần thiết để xây dựng thư viện. Thư viện kết quả được đặt trong thư mục libs ở cùng thứ bậc như thư mục jni (ví dụ <project folder>/libs/).

Khi đóng gói ứng dụng Android bằng trình cắm thêm ADT cho Eclipse, các tệp thư viện được bao gồm và "được nối" tự động cho bạn. Một tệp thư viện được tạo ra cho mỗi nền tảng phần cứng được hỗ trợ. Thư viện phù hợp được nạp trong thời gian chạy.

Hãy xem cách thực hiện các thuật toán xử lý ảnh.

Thực hiện các thuật toán xử lý ảnh

Thực hiện các thuật toán xử lý ảnh Các thường trình xử lý ảnh được ứng dụng này sử dụng đã được phỏng theo một loạt các thường trình miền công cộng và của giới học thuật, cùng với kinh nghiệm riêng của tôi với tư cách là một người say mê xử lý ảnh. Hai trong số các hàm sử dụng các phép toán điểm ảnh và hàm thứ ba sử dụng một cách tiếp cận ma trận tối thiểu. Trước tiên chúng ta hãy xem xét hàm convertToGray trong Liệt kê 5.

Liệt kê 5. Hàm 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);

    
}

Hàm này nhận hai đối số từ mã Java đang gọi nó: một Bitmap màu theo định dạng ARGB và một Bitmap đen trắng theo thang độ xám 8-bit, là một phiên bản đen trắng theo thang độ xám của ảnh màu. Dưới đây là giải thích kỹ về đoạn mã này:

  1. Cấu trúc AndroidBitmapInfo được định nghĩa trong bitmap.h, có ích cho việc tìm hiểu về một đối tượng Bitmap.
  2. Hàm AndroidBitmap_getInfo có trong thư viện jnigraphics, nhận thông tin về một đối tượng Bitmap cụ thể.
  3. Bước tiếp theo là đảm bảo rằng bitmap được chuyển vào hàm convertToGray có định dạng như mong đợi.
  4. Hàm AndroidBitmap_lockPixels khóa chặt dữ liệu ảnh để cho bạn có thể thực hiện các phép toán trực tiếp trên dữ liệu.
  5. Hàm AndroidBitmap_unlockPixels mở khóa dữ liệu điểm ảnh đã bị khóa trước đó. Các hàm này nên được gọi là một "cặp khóa/mở khóa".
  6. Bạn thấy các phép toán điểm ảnh được kẹp giữa các hàm khóa và mở khóa.

Trò vui về con trỏ

Các ứng dụng xử lý ảnh viết bằng ngôn ngữ C thường dính đến việc sử dụng các con trỏ. Các con trỏ là các biến "trỏ" đến một địa chỉ bộ nhớ. Kiểu dữ liệu của một biến xác định kiểu và kích thước của vùng bộ nhớ mà bạn đang làm việc. Ví dụ, kiểu char biểu diễn một giá trị 8-bit có dấu, do đó, một con trỏ char (char *) cho phép bạn tham khảo một giá trị 8-bit và thực hiện các phép toán thông qua con trỏ đó. Dữ liệu ảnh được biểu diễn dưới dạng uint8_t, có nghĩa là một giá trị 8-bit không dấu, ở đây mỗi byte nhận giá trị từ 0 đến 255. Một bộ ba giá trị 8-bit không dấu biểu diễn một điểm ảnh của dữ liệu ảnh cho một ảnh 24-bit.

Duyệt đi qua một ảnh liên quan đến làm việc trên từng hàng dữ liệu riêng lẻ và di chuyển qua các cột. Cấu trúc Bitmap chứa một thành viên được gọi là bước dài (stride). Bước dài biểu diễn chiều rộng, theo byte, của một hàng dữ liệu ảnh. Ví dụ, một ảnh 24-bit màu cộng kênh alpha có 32 bit, hoặc 4 byte, cho mỗi điểm ảnh. Vì vậy, một ảnh có chiều rộng 320 điểm ảnh có một bước dài là 320*4 hoặc 1.280 byte. Ảnh đen trắng theo thang độ xám 8-bit có 8 bit, tức là 1 byte, cho mỗi điểm ảnh. Một ảnh bitmap đen trắng theo thang độ xám có chiều rộng là 320 pixel có một bước dài là 320*1 hoặc chỉ đơn giản là 320 byte. Với thông tin này trong tâm trí, chúng ta hãy xem xét thuật toán xử lý ảnh để chuyển đổi một ảnh màu thành một ảnh đen trắng theo thang độ xám:

  1. Khi dữ liệu ảnh "bị khóa", địa chỉ cơ sở của dữ liệu ảnh được tham chiếu bằng một con trỏ có tên pixelscolor cho ảnh màu đầu vào và pixelsgray cho ảnh đen trắng theo thang độ xám đầu ra.
  2. Hai vòng lặp for-next cho phép bạn lặp duyệt qua toàn bộ ảnh.
    1. Trước tiên, bạn lặp qua chiều cao của ảnh, một lần chuyển qua mỗi "hàng." Sử dụng giá trị infocolor.height để lấy số đếm các hàng.
    2. Ở mỗi lần chuyển qua các hàng một con trỏ được đặt đến vị trí bộ nhớ tương ứng với "cột" đầu tiên của dữ liệu ảnh cho hàng đó.
    3. Khi bạn lặp qua các cột với một hàng cụ thể, bạn chuyển đổi từng điểm ảnh của dữ liệu màu thành một giá trị duy nhất biểu diễn giá trị mức xám.
    4. Khi toàn bộ hàng được chuyển đổi, bạn cần chuyển các con trỏ xuống dòng kế tiếp. Thực hiện việc này bằng cách nhảy về phía trước trong bộ nhớ một đoạn bằng giá trị bước dài.

Đối với tất cả các phép toán xử lý ảnh theo từng điểm ảnh, bạn làm theo khuôn dạng nói trên. Ví dụ, hãy xem xét hàm changeBrightness được hiển thị trong Liệt kê 6.

Liệt kê 6. Hàm 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);

    
}

Hàm này hoạt động theo cách rất giống với hàm convertToGray với các điểm khác biệt sau:

  1. Hàm này đòi hỏi chỉ một ảnh bitmap đen trắng theo thang độ xám. Ảnh chuyển đến cho nó được sửa đổi tại chỗ.
  2. Hàm cộng hoặc trừ đi một giá trị là 5 đối với mỗi điểm ảnh trên mỗi lần đi qua. Có thể thay đổi hằng số này. Tôi đã sử dụng 5 bởi vì nó làm thay đổi ảnh đáng kể với mỗi lần chuyển qua mà không cần phải nhấn các nút cộng hoặc trừ quá nhiều lần.
  3. Các giá trị điểm ảnh bị hạn chế giữa 0 và 255. Hãy cẩn thận khi thực hiện trực tiếp các phép toán này với các biến không dấu vì rất dễ dẫn đến "xoay vòng tròn". Ban đầu tôi thao tác với hàm changeBrightness đã dẫn đến việc thêm 5 vào giá trị ví dụ là 252 và bị xoay vòng thành 2. Kết quả khi xem rất vui nhộn, nhưng đó không phải những gì tôi đã muốn. Đó là lý do tại sao tôi đang sử dụng số nguyên có tên là v và ép kiểu dữ liệu điểm ảnh thành số nguyên có dấu rồi so sánh giá trị này với 0 và 255.

Vẫn còn một thuật toán xử lý ảnh nữa cần xem xét: hàm findEdges, hoạt động hơi khác một chút so với hai hàm hướng điểm ảnh ở trên. Liệt kê 7 cho thấy hàm findEdges.

Liệt kê 7. Hàm findEdges hàm phát hiện các đường nét trong một ảnh
/*
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);

    
}

Thường trình findEdges có nhiều điểm chung với hai hàm trước:

  1. Cũng giống như hàm convertToGray, hàm này có hai tham số kiểu bitmap, nhưng trong trường hợp này, cả hai đều là ảnh đen trắng theo thang độ xám.
  2. Ảnh bitmap được kiểm tra để đảm bảo rằng chúng có định dạng như dự kiến.
  3. Các điểm ảnh bitmap bị khóa và được mở khóa một cách thích hợp.
  4. Thuật toán lặp duyệt qua các hàng và các cột của ảnh nguồn.

Không giống như hai hàm trước, hàm này so sánh từng điểm ảnh với các điểm ảnh xung quanh nó, chứ không chỉ đơn giản thực hiện một phép toán toán học với chính giá trị điểm ảnh đó. Thuật toán được thực hiện trong hàm này là một biến thể của thuật toán phát hiện đường viền Sobel. Trong cách triển khai này, tôi so sánh từng điểm ảnh với các điểm ảnh xung quanh nó trong đường viền một điểm ảnh theo mỗi hướng. Các biến thể của thuật toán này và các thuật toán khác có thể sử dụng "đường viền" lớn hơn để thu được các kết quả khác nhau. Việc so sánh từng điểm ảnh với các điểm ảnh xung quanh của nó đặt trọng tâm vào sự tương phản giữa các điểm ảnh và như vậy làm nổi bật "đường viền".

Tôi sẽ không đi vào nội dung toán học liên quan đến thuật toán này vì hai lý do. Đầu tiên, việc quan tâm đến nội dung toán học nằm ngoài phạm vi của hướng dẫn này. Và thứ hai, mục đích chính xác của hướng dẫn này — (tái) sử dụng mã nguồn C hiện có — được thể hiện bằng cách sử dụng một thuật toán xử lý ảnh hiện có. Bạn có thể để thu được các kết quả mong muốn mà không cần phát minh lại cái bánh xe hay chuyển mã này sang công nghệ Java. C là một môi trường lý tưởng để làm việc với dữ liệu ảnh, nhờ vào số học con trỏ.

Để biết thêm thông tin về các thuật toán xử lý ảnh, xin vui lòng xem Tài nguyên.


Tùy chỉnh Eclipse

Một trong những khía cạnh thú vị khi làm việc với IDE của Eclipse là bạn hiếm khi phải biên dịch. Bất cứ khi nào bạn lưu một tệp trong IDE Eclipse, dự án của bạn được xây dựng tự động. Điều này thật tuyệt vời cho các tệp của SDK Android (nghĩa là các tệp Java) và các tệp XML Android, thế còn với thư viện xây dựng bởi NDK thì sao? Hãy tìm hiểu tiếp.

Mở rộng môi trường Eclipse

Như đã đề cập, việc xây dựng thư viện nguyên gốc là đơn giản, chỉ việc chạy lệnh ndk-build. Tuy nhiên, khi làm việc với dự án cho bất cứ cái gì khác, không phải là một bài tập tầm thường, khá rắc rối để thoát ra đến một cửa sổ đầu cuối hoặc cửa sổ lệnh và thực hiện lệnh ndk-build, rồi quay trở lại môi trường Eclipse, và buộc làm mới bằng cách "chạm vào" một trong các tệp dự án, buộc biên dịch lại và đóng gói lại ứng dụng hoàn chỉnh. Giải pháp cho việc này là mở rộng môi trường Eclipse bằng cách tùy chỉnh các thiết lập xây dựng cho dự án Android của bạn.

Để thay đổi các thiết lập xây dựng, đầu tiên xem các thuộc tính của dự án Android và chọn Builders (Các trình xây dựng) trong danh sách. Thêm một Builder mới và di chuyển nó lên đầu danh sách, như thể hiện trong Hình 8.

Hình 8. Sửa đổi các thiết lập xây dựng
Ảnh chụp màn hình hiển thị chỉnh sửa trang các Builder của các đặc tính của dự án IBM Photo Phun trong Eclipse

Mỗi trình xây dựng có bốn thẻ cấu hình. Hãy đặt một tên cho trình xây dựng của bạn, ví dụ như Build NDK Library (xây dựng thư viện NDK), sau điền vào các thẻ. Thẻ đầu tiên ("Main") chỉ rõ nơi chứa công cụ chạy thi hành và thư mục làm việc. Hãy trỏ vị trí của tệp ndk-build và thư mục làm việc vào thư mục jni của bạn, như thể hiện trong Hình 9.

Hình 9. Thiết lập các thuộc tính Builder cho các NDK
Ảnh chụp màn hình hiển thị thẻ Main trong việc chỉnh sửa chi tiết với mục nhập NDK trong Eclipse

Bạn chỉ muốn ndk-build hoạt động trên dự án này và không hoạt động cho những dự án khác trong vùng làm việc Eclipse của bạn, do đó, thiết lập điều này trên thẻ Refresh (Làm mới), như thể hiện trong Hình 10.

Hình 10. Thiết lập thẻ Refresh
Ảnh chụp màn hình hiển thị thẻ Refresh trong việc chỉnh sửa chi tiết với mục nhập NDK trong Eclipse

Thời điểm duy nhất bạn muốn xây dựng lại thư viện là khi hoặc tệp Android.mk hoặc tệp ibmphotophun.c được sửa đổi. Để thiết lập điều này, chọn thư mục jni dưới nút Specify Resources (Quy định tài nguyên) trên thẻ Build Options (Các tùy chọn xây dựng). Ngoài ra, chỉ rõ khi bạn muốn chạy công cụ xây dựng bằng cách chọn và đánh dấu thời gian thích hợp, như trong Hình 10.

Hình 11. Thiết lập các tùy chọn xây dựng
Ảnh chụp màn hình hiển thị thẻ Build Options trong việc chỉnh sửa chi tiết với mục nhập NDK trong Eclipse

Sau khi nhấn OK để xác nhận các thiết lập của bạn, hãy chắc chắn rằng công cụ xây dựng NDK này được đặt ở mục đầu tiên trong danh sách bằng cách chọn nút Up cho đến khi công cụ này ở đầu danh sách các Builder, như thể hiện trong Hình 7.

Để kiểm tra xem Builder của bạn được thiết lập đúng chưa, mở tệp nguồn ibmphotophun.c trong Eclipse bằng cách nhấn chuột phải vào tệp nguồn và chọn mở nó với Text Editor (Trình soạn thảo văn bản). Thực hiện một thay đổi đơn giản và ghi lưu tệp. Bạn sẽ thấy đầu ra chuỗi công cụ NDK trong cửa sổ của giao diện điều khiển, như trong Hình 11. Nếu mã C của bạn có lỗi, chúng được thể hiện bằng màu đỏ.

Hình 12. Kết quả đầu ra NDK trên giao diện điều khiển của IDE Eclipse
Ảnh chụp màn hình hiển thị một ví dụ về đầu ra của bàn điều khiển từ Eclipse khi thực hiện gdb-setup

Với NDK được gắn chặt vào quá trình xây dựng của bạn, bạn có thể tập trung vào viết mã và chẳng phải bận tâm nhiều về môi trường xây dựng. Cần thực hiện một thay đổi logic ứng dụng? Không có vấn đề gì, hãy sửa đổi mã Java và lưu tệp. Cần chỉnh sửa thuật toán xử lý ảnh? Đừng lo lắng, chỉ cần thay đổi thường trình C và lưu tệp. Eclipse và trình cắm thêm ADT sẽ giải quyết phần còn lại cho bạn.


Tóm tắt

Hướng dẫn này đã trình bày một ví dụ về sử dụng NDK của Android để kết hợp các chức năng bằng ngôn ngữ lập trình C. Các hàm được sử dụng ở đây biểu diễn một mẫu của thuật toán xử lý ảnh mã nguồn mở/miền công cộng. Theo cách tương tự, bất kỳ mã C hợp lệ nào tương thích với nền tảng Android có thể được sử dụng với sự trợ giúp của NDK của Android. Ngoài các cơ chế về sử dụng NDK trong Eclipse, bạn cũng đã học được một số khái niệm cơ bản xung quanh việc xử lý ảnh.


Tải về

Mô tảTênKích thước
Source codeos-androidndk-IBMPhotoPhun.source.zip995KB

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

  • Tải về bản SDK của Android, truy cập vào tài liệu tham khảo API và nhận được các tin tức mới nhất về Android từ trang web của các nhà phát triển Android chính thức. Phiên bản V1.5 và mới hơn sẽ thích hợp. Hướng dẫn này đã sử dụng Android SDK V8, hỗ trợ bản phát hành Android có nhãn 2.2 (Froyo).
  • Tải về bản NDK của Android . Bản phát hành NDK đã sử dụng trong hướng dẫn này là r4b.
  • Android là nguồn mở, có nghĩa là bạn có thể nhận được mã nguồn của nó từ Dự án mã nguồn mở Android.
  • Nhận được bản Eclipse IDE mới nhất. Trong hướng dẫn này đã sử dụng V3.4.2.
  • Đổi mới dự án phát triển nguồn mở tiếp theo của bạn với phần mềm dùng thử IBM, có sẵn để tải về hoặc trên đĩa DVD.
  • Tải về các phiên bản đánh giá sản phẩm của IBM hoặc khám phá các thử nghiệm trực tuyến trong SOA Sandbox của IBM và nhận các công cụ phát triển ứng dụng thực hành của bạn và các sản phẩm phần mềm trung gian từ DB2®, Lotus®, Rational®, Tivoli® và WebSphere®.

Thảo luận

  • Tham gia vào các blog developerWorks và dành tâm trí cho cộng đồng developerWorks.
  • Dành tâm trí cho cộng đồng developerWorks. Kết nối với những người dùng developerWorks khác trong khi khám phá các blog theo hướng người phát triển, các diễn đàn, các nhóm và các wiki.

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Nguồn mở
ArticleID=784077
ArticleTitle=Sử dụng lại mã C với Bộ công cụ của nhà phát triển nguyên gốc của Android (Native Developer's Kit - NDK)
publish-date=01052012