So lesen Sie eine Bilddatei in C++ in Android mit NDK

Die Softwareentwicklung in Android kann mit dem Java SDK oder durchgeführt werden Natives Entwicklungskit, auch bekannt als NDK zur Verfügung gestellt vom Android Open Source Project (AOSP). NDK wird häufig zum Schreiben von Hochleistungscode wie Bildverarbeitungsalgorithmen verwendet.

Viele Apps haben Anforderungen zum Lesen von Dateien von der Festplatte. Zum Lesen von Bilddateien besteht der übliche Ansatz darin, Dateien mit Java-APIs zu lesen, die im Android SDK verfügbar sind, oder Abstraktionen auf höherer Ebene wie MediaStore-APIs zu verwenden. Ich werde in diesem Artikel nicht auf das Lesen verschiedener Dateiformate in der Java-Schicht eingehen.

Manchmal müssen die Bilddateien möglicherweise in nativer Ebene (C++) verarbeitet werden. In solchen Fällen ist die übliche Vorgehensweise

  • Laden Sie das Bild als Bitmap.
  • Marshallen Sie es mit JNI auf der nativen Schicht.
  • Führen Sie Lese-/Schreibvorgänge in der nativen Schicht durch.

Unter bestimmten Umständen möchten Sie das Bild jedoch möglicherweise direkt in der nativen Ebene lesen. Wenn Sie solche Umstände haben – dieser Artikel ist für Sie!

FYI, wenn ich “native Schicht” oder “nativen Code” sage, bedeutet das in C++-Code. darf ich verwenden
diese Begriffe im Artikel austauschbar.

Auch wenn es in diesem Artikel hauptsächlich um das Lesen von Bilddateien in C++ geht, können die Konzepte leicht extrapoliert werden Lesen eines beliebigen Dateiformats in nativer Ebene in Android.


Bevor Sie mit den Schritten und Codebeispielen beginnen, gibt es noch einen weiteren Elefanten im Raum, der angesprochen werden muss.

Warum das Bild zunächst in der nativen Ebene lesen?

Ich werde es nach dem “Wie-Teil” abdecken.

Mir wurde gesagt, dass nicht jeder an dem Warum-Teil interessiert ist, über den ich normalerweise rede.

Von meiner Frau (-_-)!

Bitte lassen Sie mich wissen, ob dies tatsächlich der Fall ist.

So lesen Sie das Bild in der nativen Ebene

Wenn Sie diesen Artikel lesen, gehe ich davon aus, dass Sie mit Konzepten wie den Grundlagen der Android-Entwicklung, NDK, Java Native Interface (JNI) und so weiter vertraut sind.

Ich hoffe, Sie sind auch mit bereichsbezogenen Speicherkonzepten in Android vertraut.

Grundsätzlich hat Android zum besseren Schutz von App- und Benutzerdaten auf externen Speichern verschärft, wie Anwendungen auf Dateien auf Android zugreifen können. TL;DR; Ohne übermäßige Berechtigungen können Sie nicht mehr direkt auf Dateien zugreifen. Das ist gut für die Benutzer! Das Gute ist, dass Sie den Benutzer immer noch bitten können, Berechtigungen für bestimmte Dateien zu erteilen, z. B. mit einer Dateiauswahl.

Also verwenden wir nicht File mehr. Es ist skalierbarer zu handhaben Uri im Android.

Beginnen wir mit dem Lesen von Bilddateien Uri.

Holen Sie sich den Uri des Bildes zum Lesen

Du kannst bekommen Uri einer Datei mit Mediastore APIs oder durch Verwendung einer Art Dateiauswahl-Benutzeroberfläche.

Eine einfache Bildauswahl kann in implementiert werden Activity so was

public class MainActivity extends AppCompatActivity {

  private final ActivityResultLauncher<String[]> galleryActivityLauncher
      = registerForActivityResult(new ActivityResultContracts.OpenDocument(),
     this::onPickImage);

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

  
  public void pickImage(View unused) {
    galleryActivityLauncher.launch(new String[]{"image/*"});
  }

  private void onPickImage(Uri imageUri) {
    
  }
}

Für den Rest dieses Artikels gehe ich davon aus, dass Sie die haben Uri in der Hand.

Der nächste Schritt besteht darin, den Dateideskriptor daraus abzurufen Uri.

Holen Sie sich den Dateideskriptor von Uri

In Unix und Unix-ähnlichen Betriebssystemen ist ein Dateideskriptor (FD) ein eindeutiger Bezeichner für eine Datei oder eine andere E/A-Ressource wie eine Pipe oder ein Netzwerk-Socket. Sie haben typischerweise nicht negative ganzzahlige Werte. Als Fehlerwerte verwendete negative Werte.

In Android können wir die verwenden Uri entsprechend zu bekommen AssetFileDescriptor und verwenden Sie es, um die Datei in der Java-Schicht zu öffnen und abzurufen FD Wert. Sobald wir den Eingeborenen bekommen FD value können wir diesen winzigen Integer-Wert über JNI in die native Schicht packen, um die Datei direkt zu lesen.

Wichtig Stellen Sie bei dieser Art von Ansatz sicher, dass die Ressource von der Java-Schicht geöffnet und von der nativen Schicht verwendet wird

  • Die Java-Schicht bleibt weiterhin Eigentümer der Datei, dh die native Schicht sollte den Dateistrom nicht schließen.
  • Die Java-Schicht hält die Datei weiterhin geöffnet, bis die native Schicht die Datei gelesen hat oder nicht mehr erfordert, dass die Datei geöffnet ist.

Ein Verstoß gegen diese Regeln kann zu unerwarteten Rennbedingungen führen.

So erhalten Sie den Eingeborenen FD in der Java-Schicht

Context context = getApplicationContext();
ContentResolver contentResolver = context.getContentResolver();
try (AssetFileDescriptor assetFileDescriptor
    = contentResolver.openAssetFileDescriptor(imageUri, "r")) {
    ParcelFileDescriptor parcelFileDescriptor = assetFileDescriptor.getParcelFileDescriptor();
    int fd = parcelFileDescriptor.getFd();

    
    

    parcelFileDescriptor.close();
} catch (IOException ioException) {
    
}

Marshallen Sie den FD-Wert über JNI in die native Schicht

Für den Rest des Inhalts erwarte ich, dass die Leser damit vertraut sind

  • Einrichten von JNI mit Android.
  • Grundlagen von JNI in Android.

JNI steht für Java Native Interface – Verweis auf hello-jni-Beispiel von Android.

Zum Lesen einer Datei in der nativen Schicht benötigen wir also eine grundlegende Java-Bibliothek und die entsprechende JNI-Datei. Hier ist ein Beispiel für eine Java-Bibliothek


public final class NativeImageLoader {

    static {
        System.loadLibrary("image-loader-jni");
    }

    
    public static native String readFile(int fd);
}

Nehmen wir an, wir haben eine entsprechende JNI-Datei mit dem Namen image-loader-jni.cc das wird in die gebacken libimage-loader-jni.so Binärdatei, die durch Erstellen der JNI-Build-Ziele erstellt wurde.



#include <jni.h>



extern "C" JNIEXPORT jstring JNICALL
Java_dev_minhazav_samples_NativeImageLoader_readFile(
    JNIEnv* env, jclass, jint fd) {
    if (fd < 0) {
        return env->NewStringUTF("Invalid fd");
    }

    

    return env->NewStringUTF("Dummy string");
}

Lesen Sie die Datei in der nativen Ebene

Und geben Sie einige Informationen über die Datei zurück

Es gibt mehrere Möglichkeiten, dies zu handhaben. Ich werde zwei davon auflisten

Bild mit Bilddecoder in NDK lesen

DDR-Bauch ImageDecoder API, die zum Lesen von Bildern in verschiedenen Formaten wie JPEG, PNG, GIF, WebP usw. verwendet werden kann.

Vorteile

  • Es ist Teil von NDK, also können Sie es
    • Überspringen Sie den Aufwand, Ihrem Projekt eine weitere native Abhängigkeit von Drittanbietern hinzuzufügen.
    • Erhalten Sie eine implizite Reduzierung der APK-Größe, indem Sie keine Bibliotheken von Drittanbietern hinzufügen.
    • Da es Teil der Plattform ist, erhalten Sie wichtige Updates kostenlos (ohne Aktualisierung auf Ihrer Seite).
  • Unterstützung für mehrere Bildformate und die undurchsichtige Dekodierung beliebiger Dateien.

Nachteile

  • Dies wurde in API-Level 30 hinzugefügt. Sie können also nur Geräte über dieser Version ansprechen!
  • Ähnlich wie Bitmap, dekodiert Bilder in eines der Bitmap Formate (Beispiele). Standardmäßig wird das Bild in dekodiert ARGB_8888 Format (4 Byte pro Pixel).
  • Es ist eine undurchsichtige Bibliothek, Sie können Ihren Decoder für bestimmte Dateiformate nicht einspeisen.

So können Sie das Bild lesen und einige Informationen an die Java-Schicht zurückgeben.

Ich hätte das Beispiel hinzufügen können, das alles im JNI-Code selbst macht. Aber dies ist keine Steinzeit und wir sind nicht diese Art von Menschen.

Wir lieben etwas Struktur in unserem Code. Schreiben wir also eine neue Bibliothek namens „Image“.


#include <memory>

#include <assert.h>
#include <android/imagedecoder.h>





class Image {
public:
    friend class ImageFactory;

    
    Image(int width, int height, int channels, int stride) :
        width_(width),
        height_(height),
        channels_(channels),
        stride_(stride) {
        
        this->pixels_ = std::make_unique<uint8_t[]>(width * height * channels);
    }

    
    uint8_t operator()(int x, int y, int c) const {
        
        uint8_t* pixel = this->pixels_.get() + (y * stride_ + x * 4 + c);
        return *pixel;
    }

    int width() const { return this->width_; }
    int height() const { return this->height_; }
    int channels() const { return this->channels_; }
    int stride() const { return this->stride_; }

private:
    void* pixels() {
        return static_cast<void*>(this->pixels_.get());
    }

    std::unique_ptr<uint8_t[]> pixels_;
    const int width_;
    const int height_;
    const int channels_;
    const int stride_;
};



class ImageFactory {
public:

    
    
    
    
    
    
    
    
    static std::unique_ptr<Image> FromFd(int fd);
}

Als nächstes implementieren wir die Logik zum Decodieren des Bildes fd. Das soll sein
Implementiert in image.cc unter ImageFactory#FromFd(..).


#include "image.h"

#include <android/imagedecoder.h>

static std::unique_ptr<Image> ImageFactory::FromFd(int fd) {
    
    AImageDecoder* decoder;
    int result = AImageDecoder_createFromFd(fd, &decoder);
    if (result != ANDROID_IMAGE_DECODER_SUCCESS) {
        
        
        
        return nullptr;
    }

    
    auto decoder_cleanup = [&decoder] () {
        AImageDecoder_delete(decoder);
    };

    const AImageDecoderHeaderInfo* header_info = AImageDecoder_getHeaderInfo(decoder);
    int bitmap_format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(header_info);
    
    
    if (bitmap_format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
        decoder_cleanup();
        return nullptr;
    }
    constexpr int kChannels = 4;
    int width = AImageDecoderHeaderInfo_getWidth(header_info);
    int height = AImageDecoderHeaderInfo_getHeight(header_info);

    size_t stride = AImageDecoder_getMinimumStride(decoder);
    std::unique_ptr<Image> image_ptr = std::make_unique<Image>(
        width, height, kChannels, stride);

    size_t size = width * height * kChannels;
    int decode_result = AImageDecoder_decodeImage(
        decoder, image_ptr->pixels(), stride, size);
    if (decode_result != ANDROID_IMAGE_DECODER_SUCCESS) {
        decoder_cleanup();
        return nullptr;
    }

    decoder_cleanup();
    return image_ptr;
}

Und jetzt benutze diese Bibliothek im JNI und lies das Bild aus fd.



#include <string>
#include <jni.h>

#include "image.h"

extern "C" JNIEXPORT jstring JNICALL
Java_dev_minhazav_samples_NativeImageLoader_readFile(
    JNIEnv* env, jclass, jint fd) {
    if (fd < 0) {
        return env->NewStringUTF("Invalid fd");
    } 

    std::unique_ptr<Image> image = ImageFactory::FromFd(fd);
    if (image == nullptr) {
        return env->NewStringUTF("Failed to read or decode image.");
    }

    
    std::string message = "Image load success: Dimension = "
        + std::to_string(image->width()) + "x" + std::to_string(image->height())
        + " Stride = " + std::to_string(image->stride());
    return env->NewStringUTF(message.c_str());
}

Noch ein paar Hinweise:

Obwohl ich in der Praxis fand AImageDecoder_setTargetSize langsamer sein, als ich es von einer Downsampling-Operation erwarten würde. Wenn Sie die Leistung dieser API beunruhigt und Sie einen anderen Ansatz zur Hand haben, versuchen Sie, das Bild mit voller Auflösung zu laden und herunterzusampeln Image separat.


Mit der bisherigen Lösung können Sie eine funktionierende Version der Bilddecodierung in der nativen Ebene erhalten.

Gründe weiter zu lesen:

  • Sie möchten Bilder in der nativen Ebene dekodieren, aber Sie haben viele Kunden, die Android <= API 30 verwenden.
  • Sie möchten etwas anderes als ein Bild lesen.
  • Sie haben Ihren eigenen benutzerdefinierten und besseren Decoder.
  • Du bist ein neugieriger Leser, du Wissensschwein!
  • Wir haben immer noch den ausstehenden Mr. Elephant im Raum, den wir ansprechen können.

Lesen Sie das Bild mit benutzerdefinierten Decodern

Der folgende Ansatz kann dazu verwendet werden Lesen Sie eine beliebige Datei mit fd Wert und dann
Sie können Ihren eigenen Decoder verwenden, um das Bild zu decodieren.

Für dieses Beispiel gehe ich davon aus, dass Sie einen eigenen Decoder haben
und es ist darunter implementiert ImageFactory Implementierung. Nehmen wir an, ein
Schnittstelle.

class ImageFactory {
public:

    
    
    
    static std::unique_ptr<Image> FromString(const std::string& image_buffer);
}

Datei lesen auf Unix-Art!

Für diesen Ansatz benötigen wir zwei, bevor wir überhaupt auf die Details der nativen Schicht eingehen
zusätzliche Informationen aus der Java-Schicht.

  1. start offset des fd (Sehr wahrscheinlich 0 es sei denn, Sie möchten die Datei nicht von Anfang an lesen). Sie können dies mit erhalten AssetFileDescriptor#getStartOffset() API.
  2. length der Datei. Sie können dies mit erhalten AssetFileDescriptor#getLength() API.

Nachdem Sie diese Informationen in der Java-Schicht erhalten haben, ordnen Sie sie über JNI der nativen Schicht zu. Zum
Im folgenden Beispiel gehe ich davon aus, dass Sie eine Bilddatei decodieren möchten und Ihr Decoder damit umgehen kann.




std::unique_ptr<Image> image = nullptr;
{
    std::string image_buffer;
    image_buffer.resize(fd_length);
    int remaining_length = read(fd, &image_buffer[0], length);
    if (remaining_length != 0) {
        return env->NewStringUTF("Failed to read full image");
    }

    image = ImageFactory::FromString(image_buffer);
}

Wenn Ihr Decoder gepufferte Daten unterstützt, können Sie die Bilddatei in Puffern lesen
auch mit dem obigen Ansatz.

Warum Sie die Datei in der nativen Ebene lesen sollten (oder nicht)

Elefant im Raum – Vom Autor generiertes Bild mit Bild in nativer Ebene konsumieren

Wenn Ihr Anliegen eine hohe Latenz beim Lesen oder Decodieren von Bildern in der Java-Schicht ist. Beachten Sie, dass das Android Java SDK ebenfalls enthalten ist ImageDecoder hat auch eine Java-API was wahrscheinlich von derselben nativen Implementierung unterstützt wird. Sie können diese APIs verwenden, um Bilder als zu lesen Ziehbar oder Bitmap.

Für jede Art von Nachbearbeitung, die Sie möglicherweise in der nativen Schicht durchführen möchten, können Sie problemlos Marshallen Bitmap Verweis auf die native Schicht. NDK bietet gute Unterstützung für Bitmap und es ermöglicht Ihnen, sie mit wenig Overhead zwischen Java und Native zu verarbeiten.

Ich plane, mehr darüber in einem separaten Artikel zu schreiben.

Sie erhalten möglicherweise keinen Latenzvorteil, wenn Sie verwenden ImageDecoder in der nativen Schicht im Vergleich zur Java-Schicht. Sie können Latenzvorteile erzielen, wenn Sie über eine Decoderimplementierung verfügen, die die Decodierung schneller verarbeiten kann als die NDK-Bibliothek.

Ein triftiger Grund, die Datei trotzdem in der nativen Ebene zu lesen, könnte darin bestehen, das Halten von bösen Dateien loszuwerden Bitmaps in der Java-Schicht, wenn Sie es nicht brauchen.

Wenn Sie beispielsweise nur ein Bild lesen, etwas nachbearbeiten, als JPEG codieren und auf der Festplatte speichern möchten, können Sie vermeiden, a Bitmap Referenz in Java.

Ich habe oft festgestellt, dass das Halten groß ist Bitmaps kann zu sichtbaren Leistungsproblemen führen, wahrscheinlich weil wir uns auf GC verlassen müssen, um den von gehaltenen Speicher zurückzufordern Bitmap wenn sie nicht mehr referenziert werden. GC funktioniert möglicherweise nicht immer auf vorhersehbare Weise. Jedoch, Bitmap#recycle() Die API könnte Ihnen auch dabei helfen.

Vermeiden des Marshallens von Dateidaten über JNI-Grenzen hinweg

Es ist eine gute Idee, den nativen Ansatz zu verwenden, wenn

Sie müssen ein alternatives Dateiformat lesen und haben Ihre benutzerdefinierte Decoderimplementierung dafür. Auf diese Weise können Sie vermeiden, es zuerst in der Java-Schicht wie zu lesen String und später über JNI auf die native Schicht gemarshallt werden.

Ich bin mir nicht 100% sicher, wie genau das Datenmarshalling über die Grenzen von Java Native hinweg funktioniert, aber es ist so Nr. 1 Tipp rund um JNI von der Android-Entwickler-Website um das Marshallen großer Datenmengen zu vermeiden.

Es “könnte” performanter sein, zu bestehen fd stattdessen auf die native Ebene.

Verwenden Sie nur C++-Bibliotheken

Dies ist ähnlich dem obigen Punkt. Wenn Sie Bibliotheken von Drittanbietern zum Decodieren Ihres Bildes oder benutzerdefinierten Dateiformats haben, die nicht in der Java-Variante verfügbar sind, oder die reine Java-Variante weniger leistungsfähig ist, wäre es eine gute Idee, den gemeinsamen Ansatz zu verwenden.

Sie mögen C++ mehr als Java

Keine Kommentare, ich höre dich! Tu, was du für richtig hältst – denn diese Welt ist deine Leinwand!

Verweise

Anhang

Einige weitere Ressourcen, falls Sie bei einem dieser Schritte stecken bleiben.

schwerwiegender Fehler: Datei „imagedecoder.h“ nicht gefunden

Sie sind also auch darauf gestoßen! Wenn Sie viele Stunden damit verbracht haben, lassen Sie es mich über Kommentare wissen, wie ich es auch getan habe! Lasst uns das Elend teilen 😃

Es kann ein paar Gründe geben, warum Sie damit konfrontiert sind.

1. Ihnen hat die richtige Zielbibliothek nicht gefallen

Wenn Sie verwenden CMake basierter Ansatz, hinzufügen jnigraphics zu target_link_libraries.

Im obigen Beispiel würde es so aussehen

target_link_libraries( # Specifies the target library.
    image-loader-jni

    ${log-lib}
    jnigraphics)

2. Du hast es nicht richtig aufgenommen

Das war mir nicht klar.

Der Bibliothekspfad ist android/imagedecoder.h und nicht imagedecoder.h. Also richtig einbinden

#include <jni.h>
#include <android/imagedecoder.h>

Wenn dies auch nicht hilft, stellen Sie sicher, dass Sie mindestens eine SDK-Version von >= 30 anstreben.

Bildlizenz

Das mit stabiler Diffusion erstellte Bild kann kostenlos unter verwendet werden CreativeML Open RAIL-M durch huggingface.co.

Möchten Sie mehr ähnliche Inhalte lesen?

Wenn Sie diesen Artikel nützlich fanden, können Sie uns gerne Feedback geben – es ist ein großer Anreiz, zufriedene Leser zu sehen. Wenn Sie einige ungenaue Informationen gefunden haben, melden Sie dies bitte ebenfalls – ich würde mich sehr freuen, Sie zu aktualisieren und Ihnen Credits zu geben!

Ich schreibe gerne Artikel zu Themen, die im Internet weniger behandelt werden. Sie drehen sich um das Schreiben schneller Algorithmen, Bildverarbeitung sowie allgemeines Software-Engineering.

Ich veröffentliche viele davon auf Medium.

Wenn Sie bereits auf Medium sind – Bitte schließen Sie sich über 4100 anderen Mitgliedern an und Abonnieren Sie meine Artikel, um Updates zu erhalten, wenn ich sie veröffentliche.

Wenn Sie nicht auf Medium sind – Medium hat Millionen erstaunlicher Artikel von über 100.000 Autoren. Melden Sie sich mit diesem Link bei Medium an um alle Vorteile von Medium für 5 $ pro Monat zu erhalten. Medium wird mir einen Betrag zahlen, um mein Schreiben zu unterstützen, ohne dass dir zusätzliche Kosten entstehen!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *