Atenção para os prazos da Maratona Behind the Code! Saiba mais

Reutilizar o código C existente com o Android NDK

Antes de iniciar

Uma das primeiras motivações para explorar o NDK é a oportunidade de aproveitar projetos de software livre, muitos dos quais escritos em C. Após a conclusão deste tutorial, teremos aprendido a criar uma biblioteca de Java Native Interface (JNI), escrita em C e compilada com o Kit de Desenvolvimento Nativo (NDK), e a incorporado em um aplicativo Android escrito na linguagem Java. O aplicativo demonstra como executar operações básicas de processamento de imagem em dados de imagem brutos. Aprenderemos também a expandir o ambiente de desenvolvimento Eclipse para integrar um projeto do NDK em um arquivo de projeto do SDK do Android. Com essa base, estaremos mais bem equipados para transferir o código de software livre existente para a plataforma Android.

Sobre este tutorial

Este tutorial apresenta o NDK do Android no ambiente Eclipse. O NDK é usado para acrescentar funcionalidade a um aplicativo Android que use a linguagem de programação C. O tutorial começa com uma olhada de alto nível no NDK e seus cenários de uso comuns. A partir daí, é apresentado o tópico de processamento de imagem, seguido por uma introdução e demonstração do aplicativo deste tutorial: o IBM Photo Phun. Esse aplicativo é uma mistura de código Java baseado no SDK e código C compilado no NDK. O tutorial continua apresentando a Java Native Interface (JNI), uma tecnologia interessante ao trabalhar com o NDK. Uma olhada nos arquivos de origem do projeto concluído fornece um roteiro para o aplicativo desenvolvido aqui. Depois, vamos criar esse aplicativo passo a passo. Há explicações sobre a classe Java e os arquivos de origem em C. Para concluir, o ambiente de desenvolvimento Eclipse é customizado para integrar a cadeia de ferramentas do NDK diretamente ao processo de desenvolvimento Eclipse fácil de usar.

Pré-requisitos

Para acompanhar este tutorial, é preciso saber desenvolver aplicativos em Android com o SDK do Android e ter familiaridade básica com a linguagem de programação C. Além disso, é preciso ter os seguintes:

  • Eclipse e Android Developer Tools (ADT) — Editor de código primário, Compilador Java e plug-in Android Development Tools
  • Kit de Desenvolvimento de Software (SDK) do Android
  • Kit de Desenvolvimento Nativo (NDK) do Android
  • Imagem PNG — Imagem usada para testar operações de processamento de imagem

Criei as amostras de código deste tutorial em um MacBook Pro com Eclipse V3.4.2 e Android SDK V8, com suporte ao release 2.2 do Android (Froyo). O release do NDK usado neste tutorial foi o r4b. O código precisa da versão r4b ou posterior porque os recursos de manipulação de imagem do NDK do Android não estão disponíveis em releases anteriores do NDK.

Consulte Recursos para obter links para essas ferramentas.

NDK do Android

Vamos começar dando uma olhada no NDK do Android e como ele pode ser usado para aprimorar a plataforma Android. Embora o SDK do Android forneça um ambiente de programação cheio de funcionalidades, o NDK do Android amplia os horizontes e pode agilizar a entrega da funcionalidade desejada, introduzindo código de origem existente, parte do qual talvez seja proprietária e parte código de software livre.

NDK

O NDK é um release de software disponível como download gratuito no Web site do Android. O NDK inclui todos os componentes necessários para incorporar funcionalidades escritas em C a um aplicativo Android. O release inicial do NDK oferecia apenas funcionalidades muito primitivas, com restrições significativas. Em cada release sucessivo, o NDK aumentou seus recursos. No r5 do aplicativo NDK, os autores podem escrever uma parte significativa de um aplicativo diretamente em C, incluindo a interface com o usuário e o recurso de manipulação de eventos. Os recursos demonstrados aqui, que permitem a funcionalidade de manipulação de imagem, foram apresentados com a versão r4b do NDK.

Dois usos comuns do NDK são para aumentar o desempenho do aplicativo e aproveitar código C existente transferindo-o para Android. Vejamos primeiro a melhoria no desempenho. Escrever código em C não garante um aumento significativo no desempenho. De fato, um código nativo mal escrito pode, na verdade, reduzir o desempenho do aplicativo em comparação com um aplicativo Java bem escrito. As melhorias de desempenho do aplicativo estão disponíveis quando funções bem preparadas e escritas em C são aproveitadas para executar operações baseadas na memória ou computacionalmente intensivas, como as demonstradas neste tutorial. Em especial, algoritmos que aproveitam a aritmética de ponteiro são os mais indicados para uso no NDK. O segundo caso de uso comum do NDK é para transferir um conjunto existente de código C escrito para outra plataforma, como Linux®. Este tutorial demonstra o NDK de um modo que destaca os casos de desempenho e reutilização.

O NDK contém um compilador e cria scripts, permitindo concentrar-se nos arquivos de origem em C e deixar a mágica do desenvolvimento para a instalação do NDK. O processo de desenvolvimento do NDK é facilmente incorporado ao ambiente de desenvolvimento do Eclipse, que é demonstrado na seção sobre Customização de Eclipse.

Antes de passar para o próprio aplicativo, vamos pegar um pequeno desvio para tratar de alguns pontos fundamentais do processamento de imagens digitais.

Pontos fundamentais do processamento de imagens digitais

Um aspecto positivo da tecnologia moderna de computadores é o surgimento e onipresença da fotografia digital. Mas fotografia digital não é só captar seu filho fazendo algo bonitinho. As imagens digitais se encontram em toda parte, de simples imagens em celulares até álbuns de casamento de alta qualidade ou imagens do espaço distantes, além de inúmeras outras aplicações. As imagens digitais são fácies de capturar, trocar e até alterar. Modificar uma imagem digital é que nos interessa aqui e representa a funcionalidade principal do aplicativo de amostra deste tutorial.

A manipulação de imagem digital ocorre de inúmeras maneiras, incluindo, mas não limitado a, as seguintes operações:

  • Corte— Extração de uma parte da imagem
  • Escala— Mudança no tamanho da imagem
  • Rotação— Mudança na orientação da imagem
  • Conversão— Converter de um formato em outro
  • Amostragem— Alterar a densidade da imagem
  • Mistura/Morphing— Alterar a aparência da imagem
  • Filtragem— Extrair elementos da imagem, como cores ou frequências
  • Detecção de borda— Usada para aplicativos de visão da máquina, a fim de identificar objetos dentro da imagem
  • Compactação— Redução do tamanho de armazenamento da imagem
  • Aprimoramento da imagem por operações com pixels: Equalização de histograma Contraste Brilho

Algumas dessas operações são executadas pixel a pixel, enquanto outras envolvem matemática de matrizes para funcionar em pequenas seções da imagem por vez. Independentemente das operações dos os algoritmos de processamento de imagem envolvem o trabalho com dados de imagem brutos. Este tutorial demonstra o uso de operações de pixel e de matriz na linguagem de programação C, executada em um dispositivo Android.

Arquitetura do aplicativo

Esta seção explora a arquitetura do aplicativo de amostra do tutorial, começando com uma olhada de alto nível no projeto concluído, passando depois por cada uma das principais etapas do seu desenvolvimento. Pode-se acompanhar passo a passo para redesenvolver o próprio aplicativo ou fazer o download do projeto concluído na seção Recursos .

Projeto concluído

Este tutorial demonstra o desenvolvimento de um aplicativo simples de processamento de imagem, o IBM Photo Phun. A Figura 1 mostra uma captura de tela do IDE do Eclipse com o projeto expandido, permitindo ver os arquivos de origem e de saída.

Figura 1. Visualização do projeto Eclipse
Visualização do projeto Eclipse

A UI do aplicativo é desenvolvida com técnicas tradicionais de desenvolvimento Android, usando um arquivo de layout único (main.xml) e uma única Atividade, implementada em IBMPhotoPhun.java. Um único arquivo de origem em C, localizado em uma pasta chamada jni, sob a pasta principal do projeto, contém as rotinas de processamento de imagem. A cadeia de ferramentas NDK compila o arquivo de origem em C em um arquivo de biblioteca compartilhada chamado libibmphotophun.so. O(s) arquivo(s) de biblioteca compilado(s) é(são) armazenado(s) na pasta libs. É criado um arquivo de biblioteca para cada plataforma de hardware de destino ou arquitetura de processador. A Tabela 1 enumera os arquivos de origem do aplicativo.

Tabela 1. Arquivos de origem de aplicativo necessários
Arquivo Comentário
IBMPhotoPhun.java Estende a classe de Atividade Android para UI e a lógica de aplicativo
Ibmphotophun.c Implementa rotinas de processamento de imagem
main.xml Página inicial da UI de aplicativo
AndroidManifest.xml Descritor de implementação de aplicativo Android
sampleimage.png Imagem usada para fins de demonstração (fique à vontade para substituir por sua própria imagem)
Android.mk Fragmento de makefile usado pelo NDK para desenvolver a biblioteca da JNI

Se não estiver disponível um ambiente de desenvolvimento Android funcional, este é um ótimo momento para instalar as ferramentas Android. Para obter mais informações sobre como configurar um ambiente de desenvolvimento Android, consulte os links das ferramentas necessárias em Recursos , além de alguns artigos introdutórios e tutoriais sobre o desenvolvimento de aplicativos para Android. Estar familiarizado com Android é útil para entender este tutorial.

Agora que tivemos uma visão geral da arquitetura e do aplicativo, pode-se ver como é executar em um dispositivo Android.

Demonstrando o aplicativo

Às vezes é útil começar tendo o fim em mente. Então, antes de mergulharmos no processo passo a passo de criação deste aplicativo, vamos dar uma olhada rápida dele em ação. As seguintes capturas de tela foram capturadas de um Nexus One que executa Android 2.2 (Froyo). As imagens foram capturadas usando a ferramenta Dalvik Debug Monitor Service (DDMS), que é instalada como parte do plug-in de Eclipse Android Developer Tools.

A Figura 2 mostra a tela inicial do aplicativo com a imagem de amostra carregada. Com uma rápida olhada na imagem é possível entender como acabei sendo programador e não no estúdio de um programa de TV, graças ao meu rosto que fica muito bem no rádio. Fique à vontade para substituir por sua própria imagem ao criar seu próprio aplicativo.

Figura 2. Tela inicial do aplicativo IBM Photo Phun
Tela inicial do aplicativo IBM Photo Phun

Os botões no alto da tela permitem mudar a imagem. O primeiro botão, Reset, restaura a imagem com sua cor original. Selecionar o botão Convert Image converte a imagem em escala de tons, como mostrado na Figure 3.

Figura 3. Imagem em escala de tons
Imagem em escala de tons

O botão Find Edges começa com a imagem em cor original, converte-a em escala de tons e depois executa o algoritmo Sobel de Detecção de Borda. A Figura 4 mostra o resultado do algoritmo de detecção de borda.

Figura 4. Detectando a borda
Detectando a borda

Os algoritmos de detecção de borda costumam ser usados em aplicativos de visão de máquina, como etapa preliminar em uma operação de várias etapas para processamento de imagem. A partir deste ponto, os dois botões finais permitem tornar a imagem mais escura ou clara mudando o brilho de cada pixel. A Figura 5 mostra uma versão mais brilhante da imagem em escala de tons.

Figura 5. Mais brilho
Mais brilho

A Figura 6 mostra a imagem de bordas um pouco mais escura.

Figura 6. Menos brilho
Menos brilho

Agora é a sua vez.

Criando o aplicativo

Nesta seção, criaremos o aplicativo aproveitando as ferramentas fornecidas no plug-in Eclipse ADT. Mesmo quem não está familiarizado com a criação de aplicativos para Android deve ser capaz de acompanhar sem problemas e aprender o que é ensinado nesta seção. A seção Recursos contém artigos e tutoriais úteis com noções básicas de criação de aplicativos Android.

Novo assistente de projeto ADT

É muito simples criar o aplicativo no IDE do Eclipse graças ao novo assistente de projeto ADT, mostrado na Figura 7.

Figura 7. Criando um projeto em Android
Criando um projeto em Android

Ao preencher o novo assistente de projeto, forneça as seguintes informações:

  1. Nome de projeto válido.
  2. Destino de construção. Note que, para este projeto, deve-se usar Android V2.2 ou Android V2.3 como nível de plataforma SDK de destino.
  3. Nome de aplicativo válido.
  4. Nome de pacote.
  5. Nome de atividade.

Depois de preencher a tela do assistente, selecione Finish. Clicar no botão Next sugere a criação do projeto de “Teste” para acompanhar este projeto, uma etapa útil, mas que fica para outro dia.

Depois que o projeto está preenchido no Eclipse, estamos pronto para implementar os arquivos de origem necessários para esse aplicativo. Começaremos com os elementos da UI do aplicativo.

Implementando a interface com o usuário

A UI desse aplicativo é bem simples. Ela contém uma única Activity com alguns widgets Button e um widget ImageView para exibir a imagem escolhida. Como muitos aplicativos em Android, a UI é definida no arquivo main.xml, mostrado na Listagem 1.

Listagem 1. Arquivo de layout de 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>

Note o uso dos elementos LinearLayout . O elemento externo controla o fluxo vertical da UI e o LinearLayout interno é configurado para gerenciamento horizontal de seus filhos. O elemento de layout horizontal contém todos os widgets Button no alto da tela. O ImageView é definido para centralizar a imagem contida e tem um atributo ID , o que permite manipular seu conteúdo durante o tempo de execução.

Cada um dos widgets Button tem um atributo onClick . O valor desse atributo deve corresponder ao método público de anulação dentro da classe Activity que o contém, que precisa de um único argumento View . Essa abordagem é um modo rápido e fácil de configurar os manipuladores de cliques sem o trabalho de definir manipuladores anônimos ou obter acesso ao elemento durante o tempo de execução. Consulte Recursos para obter mais informações sobre essa abordagem de manipulação de toques em Button .

Depois que a UI foi definida no arquivo de layout, o código Activity deve ser escrito para funcionar com a UI. Isso é implementado no arquivo IBMPhotoPhun.java, onde a classe Activity é estendida. Pode-se ver o código na Listagem 2.

Listagem 2. Importações e declaração de classe do 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);
    }   
}

Vamos dividir isso em alguns comentários notáveis:

  1. Há algumas variáveis de membro: tag— Usada em todas as instruções de log para ajudar a filtrar LogCat durante a depuração. bitmapOrig— Esse Bitmap tem a imagem na cor original. bitmapGray— Esse Bitmap tem uma cópia em escala de tons da imagem e é usado apenas temporariamente durante a rotina findEdges . bitmapWip— Esse Bitmap tem a imagem em escala de tons que foi modificada quando os valores de brilho foram modificados. ivDisplay— Esse ImageView é uma referência ao ImageView definido no arquivo de layout main.xml.
  2. A seção “Coisas do NDK” inclui quatro linhas: A biblioteca que contém nosso código nativo é carregada com uma chamada para System.loadLibrary. Note que esse código está contido em um bloco marcado como “estático”. Isso faz a biblioteca ser carregada quando o aplicativo é iniciado. Declaração de protótipo para convertToGray— Essa função assume dois parâmetros. O primeiro é o Bitmap de cor e o segundo é um Bitmap preenchido com uma versão em escala de tons do primeiro. Declaração de protótipo para changeBrightness— Essa função assume dois parâmetros. O primeiro é um número inteiro que representa up ou down. O segundo é um Bitmap modificado pixel a pixel. Declaração de protótipo para findEdges. Assume dois parâmetros. O primeiro é um Bitmap em escala de tons e o segundo é um Bitmap que recebe a versão da imagem apenas com as bordas.
  3. O método onCreate infla o layout identificado por R.layout.main, obtém uma referência ao widgetbImageView (ivDisplay) e carrega a imagem em cores a partir dos recursos. O método BitmapFactory aceita um parâmetro options que permite carregar a imagem no formato ARGB. “A” significa canal alfa e “RGB” significa “red, green, blue”, ou seja, “vermelho, verde, azul”, respectivamente. Muitas bibliotecas de processamento de imagem de software livre esperam uma imagem colorida de 24 bits, oito bits para cada uma das cores (vermelho, verde e azul), com cada pixel composto de do RGB triplo. Cada valor vai de 0 a 255. As imagens na plataforma Android são armazenadas como números inteiros de 32 bit, como alfa, vermelho, verde e azul. Depois que a imagem é carregada, é exibida no ImageView.
  4. O equilíbrio de métodos nessa classe corresponde aos “manipuladores de clique” dos widgets Button : onResetImage carrega a imagem original em cores no ImageView. onConvertToGray cria o Bitmap de destino como imagem de 8 bits e chama a função nativa convertToGray . A imagem resultante (bitmapWip) é exibida no ImageView. onFindEdges cria dois objetos Bitmap intermediários, converte a imagem em cores em uma imagem em escala de tons e chama a função nativa findEdges . A imagem resultante (bitmapWip) é exibida no ImageView. onDimmer e onBrighter cada um chama o método changeBrightness para modificar a imagem. A imagem resultante (bitmapWip) é exibida no ImageView.

Isso basicamente encerra o código da UI. Agora é hora de implementar as rotinas de processo de imagem, mas primeiro precisamos criar a própria biblioteca.

Criando os arquivos NDK

Agora que a UI e a lógica do aplicativo Android estão prontas, é preciso implementar as funções de processamento de imagem. Para isso, é preciso criar uma biblioteca nativa Java com o NDK. Nesse caso, usaremos código C de domínio público para implementar as funções de processamento de imagem e empacotá-las em uma biblioteca utilizável pelo aplicativo Android.

Criando a biblioteca nativa

O NDK cria bibliotecas compartilhadas e depende de um sistema de makefile. Para criar a biblioteca nativa para este projeto, é preciso executar as seguintes etapas:

  1. Crie uma nova pasta chamada jni abaixo do seu arquivo de projeto.
  2. Na pasta jni, crie um arquivo chamado Android.mk, que contém as instruções de makefile para criar adequadamente e dar um nome à sua biblioteca.
  3. Na pasta jni, crie o arquivo de origem, referenciado no arquivo Android.mk. O nome do arquivo de origem C para esse tutorial é ibmphotophun.c.

A Listagem 3 mostra o conteúdo do arquivo Android.mk.

Listagem 3. Arquivo 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)

Entre outras coisas, esse makefile (fragmento) instrui o NDK a:

  1. Compilar o arquivo de origem ibmphotophun.c em uma biblioteca compartilhada.
  2. Dar um nome há biblioteca compartilhada. Por padrão, a convenção de nomenclatura da biblioteca compartilhada é lib.so. De modo que o arquivo resultante aqui é chamado de libibmphotophun.so.
  3. Especifique as bibliotecas de “entrada” necessárias. A biblioteca compartilhada depende de arquivos de biblioteca integrados para log (liblog.so) e gráficos jni (libjnigraphics.so). A biblioteca de log permite acrescentar entradas ao LogCat, algo útil na fase de desenvolvimento do seu projeto. A biblioteca de gráficos fornece rotinas para trabalhar com bitmaps Android e seus dados de imagem.

O arquivo de origem ibmphotophun.c contém algumas instruções de inclusão em C e a definição do tipo argb, que corresponde ao tipo de dado Color no SDK do Android. A Listagem 4 mostra o ibmphotophun.c sem as rotinas de imagem, que são apresentadas a seguir.

Listagem 4. Macros e inclusões ibmphotophun.c
/∗
 ∗ ibmphotophun.c
 ∗ 
 ∗ Author: Frank Ableson
 ∗ Contact Info: fableson@msiservices.com
 ∗/

#include <jni.h>
#include <android/log.h>
#include <android/bitmap.h>

#define  LOGTAG    "libibmphotophun"
#define  LOGI(...)  android_log_print(ANDROID_LOG_INFO,LOG_TAG,VAARGS)
#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;

As macros LOGI e LOGE fazem chamadas para o recurso Logging e são equivalentes em funcionalidade a Log.i() e Log.e() respectivamente no SDK do Android. O tipo de dado argb definido com as palavras-chave de estrutura typedef permite que o código C acesse os quatro elementos de dados de um único pixel armazenado em um número inteiro de 32 bits. As três instruções de inclusão fornecem as declarações necessárias ao compilador C para colagem jni, log e manuseio de bitmap, respectivamente.

Agora é hora de implementar algumas rotinas de processamento de imagem, mas antes de examinarmos o código em si, é preciso entender as convenções de nomenclatura das funções JNI.

Quando o código Java chama uma função nativa, ele mapeia o nome dela em uma função expandida, ou decorada, que é exportada pela biblioteca compartilhada JNI. Esta é a convenção: Java_fully_qualified_classname_functionname.

Por exemplo, a função convertToGray é implementada em código C como Java_com_msi_ibm_ndk_IBMPhotoPhun_convertToGray.

Os dois primeiros argumentos das funções JNI incluem um ponteiro para o ambiente JNI e para a instância de objeto de classe de chamada. Para obter mais informações sobre JNI, consulte a seção Recursos .

Criar a biblioteca é bem simples. Abra uma janela de terminal (ou DOS) e mude o diretório para a pasta jni onde esses arquivos foram armazenados. Confirme se o NDK está no seu caminho e execute o script de criação do NDK. Esse script contém toda a “cola” necessária para criar a biblioteca. A biblioteca resultante é colocada na pasta libs no mesmo nível da pasta jni (/libs/, por exemplo).

Quando o aplicativo Android é empacotado pelo plug-in ADT para Eclipse, os arquivos de biblioteca são incluídos e “conectados” automaticamente. Um arquivo de biblioteca é gerado para cada plataforma de hardware suportada. A biblioteca correta é carregada no tempo de execução.

Vejamos como os algoritmos de processamento de imagem são implementados.

Implementando os algoritmos de processamento de imagem

As rotinas de processamento de imagem usadas por esse aplicativo foram adaptadas de uma variedade de rotinas em domínio público e acadêmicas, juntamente com a experiência que adquiri no meu próprio hobby com processamento de imagem. Duas das funções usam operações de pixel e o terceiro utiliza uma abordagem de matriz mínima. Vejamos primeiro a função convertToGray da Listagem 5.

Listagem 5. Função 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);

    
}

Essa função assume dois argumentos do código Java de chamada: um Bitmap colorido no formato ARGB e um Bitmap em escala de tons e 8 bits que recebe uma versão em escala de tons da imagem colorida. Estes são os detalhes do código:

  1. A estrutura AndroidBitmapInfo definida em bitmap.h, é útil para aprender sobre o objeto Bitmap .
  2. A função AndroidBitmap_getInfo , encontrada na biblioteca jnigraphics, obtém informações sobre um objeto Bitmap específico.
  3. A próxima etapa é assegurar que os bitmaps passados para a função convertToGray estejam no formato esperado.
  4. A função AndroidBitmap_lockPixels bloqueia os dados da imagem para permitir executar as operações diretamente neles.
  5. A função AndroidBitmap_unlockPixels desbloqueia os dados de pixel bloqueados anteriormente. Essas funções devem ser chamadas como “par de bloqueio/desbloqueio”.
  6. Entre as funções de bloqueio e desbloqueio pode-se ver as operações de pixel.

Ponteiros divertidos

Os aplicativos de processamento de imagem em C em geral envolvem o uso de ponteiros. Eles são variáveis que “apontam” para um endereço de memória. O tipo de dados de uma variável especifica o tipo e o tamanho da memória com a qual se trabalha. Por exemplo, um char representa um valor assinado de 8 bits, de modo que um ponteiro char (char *) permite referenciar um valor de 8 bits e executar operações por meio desse ponteiro. Os dados de imagem são representados como uint8_t, o que significa um valor não assinado de 8 bits, no qual cada byte tem um valor entre 0 e 255. Uma coleção de três valores não assinados de 8 bits representa um pixel dos dados de imagem para uma imagem de 24 bits.

Trabalhar com uma imagem envolve trabalhar em linhas individuais de dados e mover-se pelas colunas. A estrutura de Bitmap contém um membro conhecido como avanço. Ele representa a largura, em bytes, de uma linha de dados de imagem. Por exemplo, uma imagem colorida de 24 bits mais canal alfa tem 32 bits, ou 4 bytes, por pixel. Assim, uma imagem com largura de 320 pixels tem avanço de 3204 ou 1.280 bytes. Uma imagem de 8 bits em escala de tons tem 8 bits, ou 1 byte, por pixel. Um bitmap em escala de tons com largura de 320 pixels tem um avanço de 3201 ou simplesmente 320 bytes. Com essas informações em mente, vejamos o algoritmo de processamento de imagem para conversão de uma imagem colorida em imagem em escala de tons:

  1. Quando os dados da imagem são “bloqueados”, o endereço base dos dados da imagem são referenciados por um ponteiro chamado pixelscolor para a imagem colorida de entrada e pixelsgray para a imagem em escala de tons de saída.
  2. Dois loops for-next permitem a iteração em toda a imagem.
    1. Primeiro, a iteração é feita na altura da imagem, uma passage por “linha”. Use o valor infocolor.height para obter a contagem de linhas.
    2. Em cada passagem pelas linhas, um ponteiro é configurado no local de memória correspondente à primeira “coluna” dos dados da imagem para a linha.
    3. Ao fazer a iteração pelas colunas para determinada linha, convertemos cada pixel de dados de cor em um único valor que representa o valor de escala de tom.
    4. Quando a linha completa está convertida é preciso avançar os ponteiros para próxima linha. Isso é feito saltando para frente na memória pelo valor de avanço.

Em todas as operações de processamento de imagem orientadas por pixel, seguiremos o formato acima. Por exemplo, considere a função changeBrightness mostrada na Listagem 6.

Listagem 6. Função 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);

    
}

Essa função opera de maneira muito similar à função convertToGray , com as seguintes diferenças:

  1. Essa função exige apenas um único bitmap em escala de tons. A imagem passada é modificada no local.
  2. A função acrescenta ou subtrai um valor de 5 de cada pixel em cada passagem. Essa constante pode ser alterada. Usei 5 porque faz a imagem mudar de forma bem evidente em cada passagem sem ter de pressionar demais os botões de mais e menos.
  3. Os valores de pixel estão restritos ao intervalo de 0 a 255. Cuidado ao executar essas operações com variáveis não assinadas diretamente como são para “agrupamento”. Meu esforço inicial na função changeBrightness resultou em acrescentar 5 a um valor como 252 e terminar com 2. O efeito foi divertido, mas não era o que eu queria. Por isso estou usando o número inteiro chamado v e lançando os dados de pixel ao número inteiro assinado e depois comparando esse valor com 0 e 255.

Há mais um algoritmo de processamento de imagem a examinar: a função findEdges , que funciona de forma um pouco diferente das outras duas funções anteriores, orientadas por pixels. A Listagem 7 mostra a função findEdges .

Listagem 7. A função findEdges detecta as bordas da imagem
/∗
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);

    
}

A rotina findEdges tem muito em comum com as duas funções anteriores:

  1. Como a função convertToGray , esta função assume dois parâmetros de bitmap, mas, neste caso, ambos são em escala de tons.
  2. Os bitmaps são interrogados para garantir que estejam no formato esperado.
  3. Os pixels de bitmap são bloqueados e desbloqueados apropriadamente.
  4. O algoritmo itera nas linhas e colunas da imagem de origem.

Ao contrário das duas funções anteriores, esta compara cada pixel com os pixels ao redor dela em vez de simplesmente executar uma operação matemática no próprio valor de pixel. O algoritmo implementado nessa função é uma variante do algoritmo Sobel de Detecção de Borda. Nesta implementação, estou comparando cada pixel com seus vizinhos com uma borda de um pixel em cada direção. Variantes desse e de outros algoritmos podem usar “bordas” maiores para obter resultados diferentes. Comparar cada pixel com seus vizinhos acentua o contraste entre os pixels e, ao fazer isso, destaca as “bordas”.

Não vou me aprofundar na matemática envolvida nesse algoritmo por duas razões. Primeiro, está fora do escopo deste tutorial tratar da matemática em si. E segundo, a finalidade exata deste tutorial — (re)utilizar código de origem em C existente — é demonstrada usando um algoritmo de processamento de imagem existente. Pode-se obter os resultados desejados sem reinventar a roda ou ter de transferir esse código para a tecnologia Java. C é o ambiente ideal para trabalhar com dados de imagem graças à aritmética de ponteiro.

Para obter mais informações sobre algoritmos de processamento de imagem, consulte Recursos.

Customização de Eclipse

Um dos melhores aspectos de trabalhar com o IDE do Eclipse é que raramente é preciso compilar. Sempre que salvamos um arquivo em um IDE do Eclipse, nosso projeto é compilado automaticamente. Isso é ótimo para arquivos do SDK do Android (ou seja, Java) e para os arquivos XML de Android, mas e para a biblioteca criada com NDK? Vejamos.

Estendendo o ambiente Eclipse

Como já mencionado, criar uma biblioteca nativa é tão simples como executar o comando ndk-build . Contudo, ao trabalhar com um projeto para qualquer outra coisa que não seja um exercício trivial, é inconveniente passar para uma janela de terminal ou de comando e executar o comando ndk-build , voltar ao ambiente Eclipse e forçar uma atualização “tocando” em um dos arquivos do projeto, o que força a recompilação e reempacotamento do aplicativo inteiro. A solução é estender o ambiente Eclipse customizando as configurações de desenvolvimento do seu projeto Android.

Para modificar as configurações de desenvolvimento, primeiro visualize as propriedades do projeto Android e selecione Builders na lista. Acrescente um novo Construtor e mova-o para o alto da lista, como mostrado na Figura 8.

Figura 8. Modificando as configurações de desenvolvimento
Modificando as configurações de desenvolvimento

Cada construtor tem quatro guias de configuração. Dê um nome ao seu construtor, como Build NDK Library, depois preencha as guias. A primeira guia (“Main”) especifica a localização da ferramenta executável e o diretório de trabalho. Indique a localização do arquivo ndk-build e o diretório de trabalho da sua pasta jni, como mostrado na Figura 9.

Figura 9. Configurando as propriedades do Construtor para o NDK
Configurando as propriedades do Construtor para o NDK

Queremos apenas que o ndk-build opere nesse projeto e não em outros dentro da área de trabalho do Eclipse, então defina isso na guia Refresh, como mostrado na Figura 10.

Figura 10. Configurando a guia Refresh
Configurando a guia Refresh

A única vez em que queremos que a biblioteca seja recriada é quando o arquivo Android.mk ou ibmphotophun.c é modificado. Para configurar isso, escolha a pasta jni sob o botão Specify Resources na guia Build Options. Especifique também quando deseja que a ferramenta de desenvolvimento seja executada marcando o tempo adequado, como mostrado na Figura 10.

Figura 11. Configurando as opções de desenvolvimento
Configurando as opções de desenvolvimento

Após clicar em OK para confirmar suas configurações, certifique-se de que essa ferramenta de desenvolvimento do NDK esteja configurada como primeira entrada na lista selecionando o botão Up até que ela esteja no alto da lista de Construtores, como mostrado na Figura 7.

Para testar se o seu Construtor foi configurado adequadamente, abra o arquivo de origem ibmphotophun.c no Eclipse dando um clique com o botão direito do mouse sobre ele e escolhendo Abrir no editor de texto. Faça uma mudança simples e salve o arquivo. É preciso ter a saída de cadeia de ferramentas do NDK na janela de console, como mostrado na Figura 11. Se seu código C contiver erros eles serão mostrados em vermelho.

Figura 12. A saída do NDK aparece no console do IDE do Eclipse
A saída do NDK aparece no console do IDE do Eclipse

Com o NDK costurado ao seu processo de desenvolvimento, é possível se concentrar em escrever código sem se preocupar muito com o ambiente de desenvolvimento. Precisa fazer uma mudança na lógica do aplicativo? Sem problema, modifique o código Java e salve o arquivo. Precisa ajustar o algoritmo de processamento de imagem? Sem problema, basta modificar a rotina em C e salvar o arquivo. O Eclipse e o plug-in ADT cuidam do resto.

Resumo

Este tutorial apresentou um exemplo de uso do NDK do Android para incorporar funcionalidade à linguagem de programação C. As funções usadas aqui representam uma amostragem dos algoritmos de processamento de imagem de software livre/domínio público. De modo similar, qualquer código C válido e compatível com a plataforma Android pode ser utilizado com a ajuda do NDK do Android. Além da mecânica do uso do NDK com Eclipse, aprendemos também alguns conceitos fundamentais sobre o processamento de imagem.