Как прочитать файл изображения на C++ в Android с помощью NDK

Разработка программного обеспечения для Android может осуществляться с помощью Java SDK или Родной комплект разработки, также известный как NDK предоставлен Android Open Source Project (AOSP). NDK часто используется для написания высокопроизводительного кода, такого как алгоритмы обработки изображений.

Многие приложения требуют чтения файлов с диска. Для чтения файлов изображений обычный подход заключается в чтении файлов с помощью API-интерфейсов Java, доступных в Android SDK, или с использованием абстракций более высокого уровня, таких как API-интерфейсы MediaStore. В этой статье я не буду рассматривать чтение различных форматов файлов на уровне Java.

Иногда может потребоваться обработка файлов изображений на собственном уровне (C++). В таких случаях обычным подходом является

  • Загрузите изображение как Битовая карта.
  • Направьте его на собственный уровень с помощью JNI.
  • Делайте операции чтения/записи в нативном слое.

Однако при определенных обстоятельствах вы можете захотеть прочитать изображение непосредственно в собственном слое. Если у вас есть такие обстоятельства — эта статья для вас!

К вашему сведению, когда я говорю «собственный слой» или «собственный код», я имею в виду код C++. я могу использовать
эти термины взаимозаменяемы в статье.

Кроме того, хотя статья в первую очередь посвящена чтению файлов изображений на C++, эти концепции можно легко экстраполировать на чтение любого формата файла в собственном слое в Android.


Прежде чем приступить к шагам и примерам кода, необходимо разобраться с еще одним слоном в комнате.

Зачем с самого начала читать изображение в нативном слое?

Я расскажу об этом после «как часть».

Мне сказали, что не всем интересна часть «почему», о которой я обычно говорю.

Моей женой (-_-)!

Пожалуйста, дайте мне знать, если это действительно так.

Как читать изображение в родном слое

Если вы читаете эту статью, я ожидаю, что вы знакомы с такими понятиями, как основы разработки под Android, NDK, Java Native Interface (JNI) и так далее.

Я надеюсь, что вы также знакомы с концепциями хранилища с ограниченной областью действия в Android.

В основном для улучшения защиты приложений и пользовательских данных на внешнем хранилище Android ужесточил доступ приложений к файлам на Android. TL;ДР; без запроса чрезмерных разрешений вы больше не можете получить доступ к файлам напрямую. Это хорошо для пользователей! Хорошо, что вы все еще можете попросить пользователя предоставить разрешения для определенных файлов, например, с помощью средства выбора файлов.

Поэтому мы не используем File больше. Это более масштабируемо для работы с Ури в Android.

Начнем с чтения файла изображения Ури.

Получить Uri изображения для чтения

Вы можете получить Ури файла с использованием Медиамагазин API или с помощью пользовательского интерфейса типа средства выбора файлов.

Простое средство выбора изображений может быть реализовано в Activity как это

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) {
    
  }
}

В оставшейся части этой статьи я буду предполагать, что у вас есть Ури в руке.

Следующий шаг — получить дескриптор файла из этого Ури.

Получить дескриптор файла от Uri

В Unix и Unix-подобных ОС файловый дескриптор (FD) — это уникальный идентификатор файла или другого ресурса ввода-вывода, такого как канал или сетевой сокет. Обычно они имеют неотрицательные целые значения. Отрицательные значения используются в качестве значений ошибок.

В Android мы можем использовать Ури получить соответствующий АссетФайлДескриптор и используйте его, чтобы открыть файл на уровне Java и получить его FD ценить. Как только мы получим родной FD value мы можем передать это крошечное целочисленное значение на собственный уровень через JNI для непосредственного чтения файла.

Важный При таком подходе, когда ресурс открывается на уровне Java и используется на уровне Native, убедитесь, что

  • Уровень Java продолжает владеть файлом, т. е. собственный уровень не должен закрывать файловый поток.
  • Уровень Java продолжает держать файл открытым до тех пор, пока собственный уровень не прочитает файл или больше не потребует, чтобы файл был открыт.

Нарушение этих правил может привести к неожиданным условиям гонки.

Вот как вы получите родной FD на уровне Java

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) {
    
}

Маршалл значение FD на собственный уровень через JNI

Что касается остального содержания, я ожидаю, что читатели знакомы с

  • Настройка JNI с Android.
  • Основы JNI в Android.

JNI расшифровывается как Java Native Interface. Ссылка на образец hello-jni из Android.

Поэтому для чтения файла на собственном уровне нам нужна базовая библиотека Java и соответствующий файл JNI. Вот пример библиотеки Java


public final class NativeImageLoader {

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

    
    public static native String readFile(int fd);
}

И допустим, у нас есть соответствующий файл JNI с именем image-loader-jni.cc который запекается в libimage-loader-jni.so двоичный файл, созданный путем построения целей сборки JNI.



#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");
}

Чтение файла в собственном слое

И вернуть некоторую информацию о файле

Есть несколько способов справиться с этим. Я перечислю два из них

Чтение изображения с помощью декодера изображений в NDK

живот ГДР Декодер изображений API, который можно использовать для чтения изображений в различных форматах, таких как JPEG, PNG, GIF, WebP и т. д.

Плюсы

  • Это часть NDK, поэтому вы можете
    • Избавьтесь от хлопот, связанных с добавлением в ваш проект еще одной сторонней нативной зависимости.
    • Получите неявное уменьшение размера APK, не добавляя сторонние библиотеки.
    • Поскольку это часть платформы, вы получаете критические обновления бесплатно (без обновления на вашей стороне).
  • Поддержка нескольких форматов изображений и возможность непрозрачного декодирования произвольных файлов.

Минусы

  • Это было добавлено в уровне API 30. Таким образом, вы можете настроить таргетинг только на устройства выше этой версии!
  • Подобно растровому изображению, декодирует изображения в один из Bitmap форматы (Примеры). По умолчанию изображение декодируется в ARGB_8888 формат (4 байта на пиксель).
  • Это непрозрачная библиотека, вы не можете использовать свой декодер для файлов определенного формата.

Вот как вы можете использовать его для чтения изображения и возврата некоторой информации обратно на уровень Java.

Я мог бы добавить пример, делающий все в самом коде JNI. Но это не каменный век, и мы не такие люди.

Мы любим некоторую структуру в нашем коде. Итак, давайте напишем новую библиотеку под названием «Изображение».


#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);
}

Далее давайте реализуем логику для декодирования изображения из fd. Это должно быть
реализовано в image.cc под 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;
}

А теперь используйте эту библиотеку в JNI и прочитайте изображение из 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());
}

Еще несколько указателей:

Хотя на практике я нашел AImageDecoder_setTargetSize быть медленнее, чем я ожидаю от операции понижения частоты дискретизации. Если вас беспокоит производительность этого API, и у вас есть другой подход, попробуйте загрузить изображение с полным разрешением и уменьшить разрешение. Image раздельно.


С решением до сих пор вы можете получить рабочую версию декодирования изображения на собственном уровне.

Причины читать дальше:

  • Вы хотите декодировать изображение в нативном слое, но у вас много клиентов, использующих Android <= API 30.
  • Вы хотите прочитать что-то кроме изображения.
  • У вас есть собственный и лучший декодер.
  • Ты любопытный читатель, пожиратель знаний!
  • У нас все еще есть ожидающий мистер Слон в комнате, чтобы выступить.

Чтение изображения с помощью пользовательских декодеров

Для прочитать любой файл с помощью fd ценить а потом
вы можете использовать свой собственный декодер для декодирования изображения.

Для целей этого примера я предполагаю, что у вас есть собственный декодер.
и это реализовано ниже ImageFactory реализация. Предположим,
интерфейс.

class ImageFactory {
public:

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

Чтение файла способом Unix!

Для этого подхода, прежде чем даже углубляться в детали нативного слоя, нам нужны два
дополнительная информация из уровня java.

  1. start offset из fd (Скорее всего 0 если вы не хотите читать файл с самого начала). Вы можете получить это, используя Описание AssetFileDescriptor#getStartOffset() API.
  2. length файла. Вы можете получить это, используя Описание AssetFileDescriptor#getLength() API.

После того, как вы получите эту информацию на уровне Java, передайте ее на собственный уровень через JNI. За
В следующем примере я предполагаю, что вы хотите декодировать файл изображения, и ваш декодер может с этим справиться.




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);
}

Если ваш декодер поддерживает буферизованные данные, вы можете прочитать файл изображения в буферах
также, с подходом выше.

Почему вы должны читать файл в собственном слое (или не должны)

Слон в комнате — Изображение создано автором с использованием Использование изображения в нативном слое

Если вас беспокоит высокая задержка чтения или декодирования изображения на уровне Java. Обратите внимание, что Android Java SDK также поставляется с ImageDecoder также имеет Java API который, вероятно, поддерживается той же собственной реализацией. Вы можете использовать эти API для чтения изображений как Drawable или Битовая карта.

Для любого вида постобработки, которую вы, возможно, захотите выполнить на собственном слое, вы можете легко упорядочить Bitmap ссылка на собственный слой. NDK имеет хорошую поддержку для Битовая карта и это позволяет вам обрабатывать их между Java и Native с небольшими накладными расходами.

Я планирую написать об этом подробнее в отдельной статье.

Вы можете не получить выгоду от задержки, используя ImageDecoder на собственном уровне по сравнению со слоем Java. Вы можете получить преимущества задержек, если у вас есть реализация декодера, которая может обрабатывать декодирование быстрее, чем то, что делает библиотека NDK.

Одна из веских причин для чтения файла на собственном уровне может состоять в том, чтобы избавиться от неприятных Bitmaps на уровне Java, когда вам это не нужно.

Например, если вы хотите просто прочитать изображение, выполнить некоторую постобработку, закодировать его в формате jpeg и сохранить на диск, вы можете не Bitmap ссылка на Java.

Я часто обнаруживал, что у меня большие Bitmaps может привести к видимым проблемам с производительностью, вероятно, потому, что мы должны полагаться на GC для восстановления памяти, удерживаемой Bitmap когда на них больше не ссылаются. Сборщик мусора не всегда может работать предсказуемым образом. Однако, Растр#переработать() API также может помочь вам в этом.

Предотвращение сортировки файловых данных через границу JNI

Рекомендуется использовать нативный подход, если

Вам нужно прочитать альтернативный формат файла, и у вас есть собственная реализация декодера для него. Таким образом, вы можете избежать первого чтения на уровне Java, поскольку String а затем перенаправить его на собственный уровень через JNI.

Я не уверен на 100%, как именно работает маршалинг данных через границу Java Native, но это Совет № 1 по JNI от веб-сайта разработчиков Android чтобы избежать сортировки больших данных.

«Может» быть более эффективным, чтобы пройти fd вместо этого на собственный слой.

Использовать только библиотеки C++

Это похоже на пункт выше. Если у вас есть сторонние библиотеки для декодирования вашего изображения или пользовательского формата файла, которые не входят в вариант Java, или вариант чистого Java менее эффективен, было бы неплохо использовать общий подход.

Вам нравится C++ больше, чем Java

Без комментариев, я вас слышу! Делай, что считаешь нужным — ибо этот мир твой холст!

использованная литература

Приложение

Еще несколько ресурсов на случай, если вы застряли на каком-либо из этих шагов.

фатальная ошибка: файл imagedecoder.h не найден

Так вы тоже наткнулись на это! Если вы потратили на это много часов, дайте мне знать в комментариях, как и я! Давайте разделим горе 😃

Может быть несколько причин, почему вы столкнулись с этим.

1. Вам не понравилась правильная целевая библиотека

Если вы используете CMake основанный подход, добавить jnigraphics к target_link_libraries.

В приведенном выше примере это будет выглядеть так

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

    ${log-lib}
    jnigraphics)

2. Вы включили его неправильно

Вот этого я не понял.

Путь к библиотеке android/imagedecoder.h и нет imagedecoder.h. Так что включайте правильно

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

Если это тоже не помогает, убедитесь, что вы настроили минимальную версию SDK >= 30.

Лицензия на изображение

Изображение, созданное с использованием стабильной диффузии, можно бесплатно использовать в соответствии с CreativeML Open RAIL-M от обнимаю лицо.co.

Хотите прочитать больше такого же содержания?

Если вы нашли эту статью полезной, не стесняйтесь делиться отзывами — это отличный стимул видеть счастливых читателей. Если вы нашли какую-то неточную информацию, пожалуйста, сообщите об этом — я буду очень рад обновить и дать вам кредиты!

Мне нравится писать статьи на тему, мало освещаемую в Интернете. Они вращаются вокруг написания быстрых алгоритмов, обработки изображений, а также общей разработки программного обеспечения.

Многие из них я публикую на Medium.

Если вы уже на среде — присоединяйтесь к более чем 4100 другим участникам и Подпишитесь на мои статьи, чтобы получать обновления по мере их публикации.

Если вы не на Medium — на Medium есть миллионы замечательных статей от более чем 100 000 авторов. Присоединяйтесь к Medium по этой ссылке чтобы получить все преимущества Medium за 5$ в месяц. Medium заплатит мне монету за поддержку моего письма без каких-либо дополнительных затрат для вас!

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *