🔥

Vulkanを利用してAndroidカメラ映像を表示する

2022/05/10に公開

本稿は、Androidでのカメラ映像をVulkanを用いてSurfaceViewに表示することを扱ったエントリーになります。前提としてVulkan Tutorialを終えておりAndroidのCamera2の映像を表示したい方を対象としてサンプルコードを掲載しています。

システム要件

  • minSdk = 26 で Android8以降
  • VulkanのAPIは1.1以降
    • ドライバーの実装で実際に動作できるのは、Android10以降
  • Pixel3a + Android11で確認しています
  • Vulkan-Hppを利用

Surface(ANativeWindow)との接続

vk::Swapchainの作成方法です。ANativeWindowからvk::Surfaceを作成します。

ANativeWindow *newNativeWindow = nullptr;
// jobjectからANativeWindowの変換
newNativeWindow = ANativeWindow_fromSurface(env, surface);

// vk::Surfaceの作成
auto surface = instance->createAndroidSurfaceKHRUnique(
  vk::AndroidSurfaceCreateInfoKHR()
    .setFlags(vk::AndroidSurfaceCreateFlagsKHR())
    .setPNext(nullptr)
    .setWindow(newNativeWindow));

// vk::SurfaceからSwapChainを作成
auto swapchain = device->createSwapchainKHRUnique(
  vk::SwapchainCreateInfoKHR
    .setSurface(surface.get())
    .setMinImageCount(capabilities.minImageCount)
    .setImageFormat(format.format)
    (...省略...)

カメラとの接続

カメラ映像(Camera2)の取り込みは、Surface経由でAImageReaderを用いて取得します。sdkのバージョンは26。Android8以降で利用可能です。また、JavaのImageReaderで、UsageをセットできるようになるのがTiramisu以降のようです。C++を選択しましょう。

AImageReader *reader;
ANativeWindow *window;

AImageReader_newWithUsage(
    width,
  height,
  format,
  AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
  AHARDWAREBUFFER_USAGE_CPU_READ_RARELY,
  maxImages,
  &reader);

AImageReader_getWindow(reader, &window);

// この window をJNIのSurface経由で、カメラと接続する。
ANativeWindow_toSurface(env, window);

カメラ映像の取得

AImageReaderからAImageを取得し。AHardwareBufferを取得します。

AImage *image = nullptr;
AHardwareBuffer *buffer;
if (AImageReader_acquireLatestImage(reader, &image) == AMEDIA_OK) {
    AImage_getHardwareBuffer(image, &buffer) != AMEDIA_OK);
}

Samplerの作成

AHardwareBufferからvk::Samplerを作成します。YcbcrRGB変換をGPUに、やってもらえます。

vk::AndroidHardwareBufferFormatPropertiesANDROID format;
vk::AndroidHardwareBufferPropertiesANDROID properties;
properties.pNext = &format;

device->getAndroidHardwareBufferPropertiesANDROID(buffer, &properties);
auto externalFormat = vk::ExternalFormatANDROID().setExternalFormat(format.externalFormat);

auto conversion = device->createSamplerYcbcrConversionUnique(
  vk::SamplerYcbcrConversionCreateInfo()
    .setPNext(&externalFormat)
    .setFormat(vk::Format::eUndefined)
    .setYcbcrModel(format.suggestedYcbcrModel)
    .setYcbcrRange(format.suggestedYcbcrRange)
    .setComponents(format.samplerYcbcrConversionComponents)
    .setXChromaOffset(format.suggestedXChromaOffset)
    .setYChromaOffset(format.suggestedYChromaOffset)
    .setChromaFilter(vk::Filter::eNearest)
    .setForceExplicitReconstruction(false));

auto samplerCreate = vk::SamplerCreateInfo()
  .setMagFilter(vk::Filter::eNearest)
  .setMinFilter(vk::Filter::eNearest)
  .setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
  .setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
  .setAddressModeW(vk::SamplerAddressMode::eClampToEdge)
  .setMipLodBias(0.0f)
  .setMaxAnisotropy(1)
  .setCompareOp(vk::CompareOp::eNever)
  .setMinLod(0.0f)
  .setMaxLod(0.0f)
  .setBorderColor(vk::BorderColor::eFloatOpaqueWhite)
  .setUnnormalizedCoordinates(false);

auto ycbcrConversionInfo = vk::SamplerYcbcrConversionInfo()
  .setConversion(conversion.get());
           
samplerCreate.setPNext(&ycbcrConversionInfo);

auto sampler = device->createSamplerUnique(samplerCreate);

バッファーの利用

vk::Imageを作成。vk::Memoryを利用して、AHardwareBuffervk::Imageを紐づけます。vk::Imageができたらvk::ImageViewを作成してDescriptorにセットします。

vk::UniqueImage image = device->createImageUnique(
  vk::ImageCreateInfo()
    .setInitialLayout(vk::ImageLayout::eUndefined)
    .setPNext(&vk::ExternalMemoryImageCreateInfo()
    .setHandleTypes(vk::ExternalMemoryHandleTypeFlagBits::eAndroidHardwareBufferANDROID)
    .setPNext(&externalFormat)));

const auto hardwareBufferProperties = device->getAndroidHardwareBufferPropertiesANDROID(
            *buffer);
	    
vk::UniqueMemory memory = device->allocateMemoryUnique(
  vk::MemoryAllocateInfo()
  .setAllocationSize(hardwareBufferProperties.allocationSize)
  .setMemoryTypeIndex(FindMemoryType(
    hardwareBufferProperties.memoryTypeBits,
    vk::MemoryPropertyFlagBits::eHostVisible
  ))
  .setPNext(&vk::MemoryDedicatedAllocateInfo()
  .setImage(image.get())
  .setPNext(&vk::ImportAndroidHardwareBufferInfoANDROID().setBuffer(buffer))));

終わりに

以上です。AndroidでVulkanコードを書く際に基本となるコードを記載しました。実際に動くコードは、こちらに置いてあります。

Discussion