HoudiniでRAW現像 Part.1 プラグイン開発編


# はじめに

Houdiniも20.5になりCOPが一新されテクスチャ作成や画像処理が盛り上がる中、
前からやってみたかったHoudiniでのデジタルカメラで撮影したRAW現像に挑戦してみました。
本来は現像まで行きたかったんですがプラグインで力尽きたのでパート分けしました。

# 概要
デジタルカメラで撮影したRAWファイルは独自形式であることがほとんどでありHoudiniで直接読み込むことができないためHDKを用いてプラグインを開発します。

# 開発環境
Windows 10
Houdini 20.5.370
Visual Studio 2022 Community
CMake 3.28
LibRaw

# 環境構築

今回は`$HSITE`を以下のようなディレクトリ構造にしました。
```
├─dev
│  ├─build
│  ├─externals
│  ├─scripts
│  └─sources
└─houdini20.5
    ├─bin
    └─dso
        └─fb
```
devディレクトリは開発に使います。
また外部共有ライブラリを使うための`$HSITE/houdini__HVER__`にbinディレクトリを作成しました。

以下のようなHoudiniを起動するためバッチファイルを作成しdev直下に置きます。

```bat:$HSITE/dev/houdini.bat
rem "環境変数HSITEを追加"

pushd %~dp0..\
set HSITE=%CD%
popd

echo HSITE=%HSITE%

rem "$HSITE/houdini__HVER__/binにパスを通す"
rem "LinuxであればLD_LIBRARY_PATHに追加"

pushd %HSITE%\houdini20.5\bin
set PATH=%PATH%;%CD%
popd

rem "Houdini Apprenticeを起動"
start "Houdini" "C:\Program Files\Side Effects Software\Houdini 20.5.370\bin\houdini.exe" -apprentice
```

さらにCmake等を走らせるためにcmdを起動するバッチファイルを作成し先ほどと同様にdev直下に置きます。

```bat:$HSITE/dev/hcmd.bat
rem "環境変数HFSを追加"

pushd "C:\Program Files\Side Effects Software\Houdini 20.5.370"
set HFS=%CD%
popd

echo HFS=%HFS%

rem "環境変数HSITEを追加"

pushd %~dp0..\
set HSITE=%CD%
popd

echo HSITE=%HSITE%

start "cmd"
```

バッチファイルはほとんど書いたことがないため適当です。ご了承ください。

# 開発

## LibRaw

. **公式ドキュメント**

https://www.libraw.org/docs/API-CXX.html

Rawファイル読み取りのためのライブラリとしてLibRawを使います。
`$HSITE/dev/externals`にGit Cloneします。

```bat
cd externals
git clone https://github.com/LibRaw/LibRaw.git
git clone https://github.com/LibRaw/LibRaw-cmake.git
```

## IMG_Format/IMG_File

. **公式ドキュメント**

https://www.sidefx.com/docs/hdk/_h_d_k__image__extend.html

. **公式サンプル**

`$HFS/toolkit/samples/IMG/IMG_Sample.*`

画像読み込みプラグインを作成するためには拡張子等の情報を定義するクラスである`IMG_Format`と画像ファイルの読み取りを担当する`IMG_File`を継承する必要があります。
またビルドしたプラグインは`$HSITE/dso/fb`に配置する必要があります。

最終的に以下のようなコードになりました。

```cpp:$HSITE/dev/sources/IMG_CameraRaw/IMG_CameraRaw.hpp
#pragma once

#include <limits>

#include <IMG/IMG_File.h>
#include <IMG/IMG_Format.h>

#include <libraw/libraw.h>

// フォーマット定義クラス
class IMG_CameraRawFormat final : public IMG_Format {
public:
    IMG_CameraRawFormat() = default;
    ~IMG_CameraRawFormat() override = default;

    auto createFile() const -> IMG_File* override;

    // デスクリプション系
    auto getFormatName() const -> const char* override {
        return "CameraRaw";
    }
    auto getFormatLabel() const -> const char* override {
        return "Camera Raw Formats.";
    }
    auto getFormatDescription() const -> const char* override {
        return "Camera Raw Formats.";
    }

    // 拡張子のチェック
    auto checkExtension(const char* filename) const -> int override;

    // unsigned short、1チャンネルのデータのみ許可
    auto getSupportedTypes() const -> IMG_DataType override {
        return IMG_USHORT;
    }
    auto getSupportedColorModels() const -> IMG_ColorModel override {
        return IMG_1CHAN;
    }

    // 最大解像度 環境依存
    auto getMaxResolution(unsigned& x, unsigned& y) const -> void override {
        x = std::numeric_limits<unsigned>::max();
        y = std::numeric_limits<unsigned>::max();
    }

    // スキャンラインでランダム読み込み可能
    auto isReadRandomAccess() const -> int override { return 1; }
    auto isWriteRandomAccess() const -> int override { return 0; }

    // 読み込みのみ許可
    auto isReadable() const -> bool override { return true; }
    auto isWritable() const -> bool override { return false; }

    // streamには非対応
    auto allowStreams() const -> int { return 0; }
};

// 読み取りクラス
class IMG_CameraRaw final : public IMG_File {
public:
    IMG_CameraRaw();
    ~IMG_CameraRaw() override;

    // 読み込み関連
    auto open() -> int override;
    auto openFile(const char* filename) -> int override;
    auto readScanline(int y, void* buf) -> int override;

    // 書き込み関連
    auto create(const IMG_Stat& stat) -> int override;
    auto writeScanline(int scan, const void* buf) -> int override;

    auto closeFile() -> int override;

private:
    LibRaw raw_processor;

    int ret;

    unsigned int raw_width, raw_height, top_margin, left_margin;
};
```

```cpp:$HSITE/dev/sources/IMG_CameraRaw/IMG_CameraRaw.cpp
#include <array>
#include <cstring>
#include <filesystem>
#include <string_view>

// 必須
#include <UT/UT_DSOVersion.h>

#include <UT/UT_JSONValue.h>
#include <UT/UT_JSONValueArray.h>
#include <UT/UT_JSONValueMap.h>

#include "IMG_CameraRaw.hpp"

IMG_CameraRaw::IMG_CameraRaw() : raw_processor{}, ret{LIBRAW_SUCCESS}, 
                                 raw_width{0}, raw_height{0}, 
                                 top_margin{0}, left_margin{0} {}

IMG_CameraRaw::~IMG_CameraRaw() { close(); }

auto IMG_CameraRaw::open() -> int { return 0; }

auto IMG_CameraRaw::openFile(const char* filename) -> int {
    // メタデータを開く
    ret = raw_processor.open_file(filename);
    if (ret != LIBRAW_SUCCESS) {
        return 0;
    }

    // raw_width/height 記録解像度
    raw_width = static_cast<unsigned int>(raw_processor.imgdata.sizes.raw_width);
    raw_height = static_cast<unsigned int>(raw_processor.imgdata.sizes.raw_height);
    // 上左のマージン　オプティカルブラックピクセルにあたると思われる
    top_margin = static_cast<unsigned int>(raw_processor.imgdata.sizes.top_margin);
    left_margin = static_cast<unsigned int>(raw_processor.imgdata.sizes.left_margin);

    // 解像度を設定 総解像度からマージンを引き有効解像度を計算
    myStat.setResolution(raw_width - (left_margin * 2), raw_height - (top_margin * 2));

    // Raw配列をメモリに格納
    // todo: クソ重いのでopenFileで律儀に開くのやめたほうがよさそう
    ret = raw_processor.unpack();
    if (ret != LIBRAW_SUCCESS) {
        return 0;
    }

    // メタデータの構築
    UT_JSONValueMap metadata{};

    // 記録解像度
    auto* raw_size = metadata.addArrayChild("raw_size");
    raw_size->emplace(static_cast<int64_t>(raw_width));
    raw_size->emplace(static_cast<int64_t>(raw_height));

    // 上左のマージン
    auto* raw_margin = metadata.addArrayChild("raw_margin");
    raw_margin->emplace(static_cast<int64_t>(top_margin));
    raw_margin->emplace(static_cast<int64_t>(top_margin));
    raw_margin->emplace(static_cast<int64_t>(left_margin));
    raw_margin->emplace(static_cast<int64_t>(left_margin));

    // 黒レベル
    metadata.emplace("raw_black", static_cast<int64_t>(raw_processor.imgdata.color.black));

    // 各チャンネルごとの黒レベル
    auto* cblack = metadata.addArrayChild("raw_cblack");
    cblack->emplace(static_cast<int64_t>(raw_processor.imgdata.color.cblack[0]));
    cblack->emplace(static_cast<int64_t>(raw_processor.imgdata.color.cblack[1]));
    cblack->emplace(static_cast<int64_t>(raw_processor.imgdata.color.cblack[2]));
    cblack->emplace(static_cast<int64_t>(raw_processor.imgdata.color.cblack[3]));

    // 最大レベル
    metadata.emplace("raw_white", static_cast<int64_t>(raw_processor.imgdata.color.maximum));

    // 各チャンネルごとの最大レベル
    auto* cwhite = metadata.addArrayChild("raw_cwhite");
    cwhite->emplace(static_cast<int64_t>(raw_processor.imgdata.color.linear_max[0]));
    cwhite->emplace(static_cast<int64_t>(raw_processor.imgdata.color.linear_max[1]));
    cwhite->emplace(static_cast<int64_t>(raw_processor.imgdata.color.linear_max[2]));
    cwhite->emplace(static_cast<int64_t>(raw_processor.imgdata.color.linear_max[3]));

    // メタデータの設定
    for (const auto& item : metadata) {
        myStat.setMetadata(item.first, *item.second);
    }

    metadata.clear();

    // プレーンを追加
    auto* plane = myStat.addDefaultPlane();
    plane->setColorModel(IMG_1CHAN);
    plane->setDataType(IMG_USHORT);

    return 1;
}

auto IMG_CameraRaw::readScanline(int y, void* buf) -> int {
    // 解像度外はエラー
    if (y >= myStat.getYres()) {
        return 0;
    }

    // メモリに格納されたRaw配列の先頭アドレス
    // マージン分を考慮しラインの先頭アドレスまでシフト
    auto* raw_ptr = raw_processor.imgdata.rawdata.raw_image;
    raw_ptr += raw_width * (top_margin + y);
    raw_ptr += left_margin;

    // バッファにコピー
    memcpy(buf, reinterpret_cast<void*>(raw_ptr), myStat.bytesPerScanline());

    return 1;
}

// 書き込み無効なので何もしない
auto IMG_CameraRaw::create(const IMG_Stat& stat) -> int {
    return 0;
}

// 書き込み無効なので何もしない
auto IMG_CameraRaw::writeScanline(int scan, const void* buf) -> int {
    return 0;
}

auto IMG_CameraRaw::closeFile() -> int
{
    // ファイルから読み込んだデータを全て解放
    raw_processor.recycle();

    return 1;
}

auto IMG_CameraRawFormat::createFile() const -> IMG_File* {
    return (new IMG_CameraRaw{});
}

auto IMG_CameraRawFormat::checkExtension(const char* filename) const -> int {
    // 拡張子のリスト とりあえずSONY、CANON、NIKON
    static std::array<std::string_view, 3> extensions{".ARW", ".CR3", ".NEF"};

    // 渡されたファイルパスの拡張子
    std::filesystem::path file_path(filename);
    auto file_ext = file_path.extension().string();

    // それぞれ比較
    for (const auto& ext : extensions) {
        if (ext == file_ext) {
            return 1;
        }
    }

    return 0;
}

// 新規画像フォーマット登録のための関数
auto newIMGFormat(void*) -> void {
    new IMG_CameraRawFormat{};
}
```

## FBformats

プラグインをHoudiniに読み込ませるため、`$HSITE/houdini__HVER__`直下に*FBformats*という名前のテキストファイルを作成する必要があります。
公式サンプルを参考に*FBformats*を作成するPythonスクリプトを作成しました。
やっていることは`$HSITE/houdini__HVER__/dso/fb`にあるプラグインを列挙しているだけです。

```python:$HSITE/dev/scripts/create_fbformats.py
import os
import sys
import glob
import platform

if (len(sys.argv) < 2):
    print('ERROR! 引数の数が足りません。')
    sys.exit(1)

# $HSITE/houdini__HVER__
hsite_dir = sys.argv[1]

if (not os.path.isdir(hsite_dir)):
    print('ERROR! HSITEディレクトリが見つかりません。')
    sys.exit(1)

# $HSITE/houdini__HVER__/dso/fb
plugin_dir = os.path.join(hsite_dir, 'dso', 'fb')

if (not os.path.isdir(plugin_dir)):
    print('ERROR! dso_fbディレクトリが見つかりません。')
    sys.exit(1)

# プラグインの拡張子　プラットフォームに応じて変更
plugin_ext = ''

os_name = platform.system()
if (os_name == 'Windows'):
    plugin_ext = 'dll'
elif (os_name == 'Darwin'):
    plugin_ext = 'dylib'
elif (os_name == 'Linux'):
    plugin_ext = 'so'
else:
    print('ERROR! 対応していないOSで実行されました。')
    sys.exit(1)

# FBformtsに書き込む文字列
fbformats = '#include "$HFS/houdini/FBformats"\n\n'

for plugin_file in glob.glob(f'{plugin_dir}/*.{plugin_ext}'):
    fbformats += f'{os.path.basename(plugin_file)}\n'

with open(f'{hsite_dir}/FBformats', 'wt') as f:
    f.write(fbformats)
```

## CMake

以下はビルドに使ったCMakeファイルです。
*FBformats*を作成するPythonスクリプトの実行をCMakeでポストビルドイベントに設定し、LibRawの出力物もポストビルドイベントで`$HSITE/houdini__HVER__/bin`にコピーしています。

```cmake:$HSITE/dev/CMakeLists.txt
# 3.24以上のバージョンでないと動作しない
cmake_minimum_required(VERSION 3.24)

# C++バージョンの設定
set(CMAKE_CXX_STANDARD 17)

# Houdini用のCmakeファイルへのパス
list(APPEND CMAKE_PREFIX_PATH "$ENV{HFS}/toolkit/cmake")

# ユニコードの設定
add_definitions(-DUNICODE -D_UNICODE)

project(FBPlugins)

# Libraw用CMakeのキャッシュ変数を設定
set(LIBRAW_PATH "${CMAKE_SOURCE_DIR}/externals/LibRaw" CACHE STRING "")

# Librawへのディレクトリを登録
add_subdirectory(externals/LibRaw-cmake)

# Houdiniパッケージを追加
# パッケージ内部の変数を使用したいためグローバルスコープに昇格
find_package(Houdini REQUIRED GLOBAL)

# HSITEのHoudiniバージョンへのパスを作成
string(APPEND houdini_ver_dir "houdini" ${Houdini_VERSION_MAJOR} "." ${Houdini_VERSION_MINOR})
set(hsite_dir "$ENV{HSITE}/${houdini_ver_dir}")

message("HSITEのバージョンディレクトリ=${hsite_dir}")
message("Houdiniに同梱されているPythonパス=${_python_binary}")

# ビルド後に実行するポスト処理
function(post_process target_name)
    # FBFormatsを上書きするためのpythonスクリプトを実行する関数
    add_custom_command(
        TARGET ${target_name} POST_BUILD
        COMMAND ${_python_binary} "${CMAKE_SOURCE_DIR}/scripts/create_fbformats.py" "${hsite_dir}"
    )

    # Librawの共有ライブラリを$HSITE/binへコピー
    add_custom_command(
        TARGET ${target_name} POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory $<TARGET_FILE_DIR:libraw::libraw_r> "${hsite_dir}/bin"
    )
endfunction(post_process)

# ソースへのディレクトリを登録
add_subdirectory(sources/IMG_CameraRaw)
```

```cmake:$HSITE/dev/sources/IMG_CameraRaw/CMakeLists.txt
set(plugin_name IMG_CameraRaw)

# ソースファイルを収集
file(GLOB sources "*.h" "*.hpp" "*.H" "*.c" "*.cpp" "*.C")

add_library(${plugin_name} SHARED
    ${sources}
)

# プラグインに必要なライブラリをリンク
target_link_libraries(${plugin_name} Houdini libraw::libraw_r)

target_include_directories(${plugin_name} PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}
)

# プラグインの出力先を設定
houdini_configure_target(${plugin_name} INSTDIR "${hsite_dir}/dso/fb")

# ポスト処理
post_process(${plugin_name})
```

## ビルド

```bat
mkdir build
cd build
cmake ../
```

ためしにレガシーCOPで手持ちのa7R5で撮影したARWを読み込んでみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/424a6a56-d232-ecf7-6d3b-ca01886909ca.png)

拡大するとベイヤー配列が確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/9cad81a8-8738-73b3-0740-67fbed7e09ab.png)

infoで設定したメタデータも確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/9942947d-a36e-c8dd-932f-a68e700ba404.png)

Copernicusでも読み込むことができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/a18f7085-546b-4f32-a97e-0f8bd3d43d80.png)

しかしCopernicusの場合、整数値の画像を扱えない可能性がありそうなのでプラグイン側で黒レベル減算など追加処理をしてやる必要がありそうです。

# おわりに
今回のプラグイン開発でUT_JSON関連クラスの扱いで一番詰まりました。
サンプルではほとんど読み取りしかしていなかったので...
プラグイン自体はとても簡単な構造なのでぜひみなさんも開発してみてください。

# 参考
[【rawpy】PythonでRAW現像 -その3 :基本現像処理編-](https://orcamagazine.com/post-622/)
[Github raypy](https://github.com/letmaik/rawpy/tree/main)
[LibRawでRAWファイルをのぞいてみる](https://ameblo.jp/delphinus1024/entry-12014570683.html)
[dds read write](https://ikatnek.blogspot.com/2020/09/dds-read-write.html)