はじめに
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直下に置きます。
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直下に置きます。
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
. 公式ドキュメント
Rawファイル読み取りのためのライブラリとしてLibRawを使います。
$HSITE/dev/externals
にGit Cloneします。
cd externals
git clone https://github.com/LibRaw/LibRaw.git
git clone https://github.com/LibRaw/LibRaw-cmake.git
IMG_Format/IMG_File
. 公式ドキュメント
. 公式サンプル
$HFS/toolkit/samples/IMG/IMG_Sample.*
画像読み込みプラグインを作成するためには拡張子等の情報を定義するクラスであるIMG_Format
と画像ファイルの読み取りを担当するIMG_File
を継承する必要があります。
またビルドしたプラグインは$HSITE/dso/fb
に配置する必要があります。
最終的に以下のようなコードになりました。
#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;
};
#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
にあるプラグインを列挙しているだけです。
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
にコピーしています。
# 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)
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})
ビルド
mkdir build
cd build
cmake ../
ためしにレガシーCOPで手持ちのa7R5で撮影したARWを読み込んでみました。
拡大するとベイヤー配列が確認できます。
infoで設定したメタデータも確認できます。
Copernicusでも読み込むことができました。
しかしCopernicusの場合、整数値の画像を扱えない可能性がありそうなのでプラグイン側で黒レベル減算など追加処理をしてやる必要がありそうです。
おわりに
今回のプラグイン開発でUT_JSON関連クラスの扱いで一番詰まりました。
サンプルではほとんど読み取りしかしていなかったので...
プラグイン自体はとても簡単な構造なのでぜひみなさんも開発してみてください。
参考
【rawpy】PythonでRAW現像 -その3 :基本現像処理編-
Github raypy
LibRawでRAWファイルをのぞいてみる
dds read write