33. 画像処理
この章では、画像処理プログラミングのための機能と、その結果をシーンに表示する方法を学びます。
Texture
クラスで読み込んだ画像データは GPU のメモリ上に配置されるため、C++ プログラムで簡単にアクセスすることができません。一方 Image
クラスで読み込んだ画像データはメインメモリ上に配置されるため、通常の Array
や Grid
のように、C++ プログラムで簡単にアクセスしたり中身を変更したりできます。ただし、Image
にはシーンに画像を表示する機能は無く、表示する場合は Texture
もしくは、本章で登場する DynamicTexture
クラスを使う必要があります。
Image | DyanmicTexture | Texture | |
---|---|---|---|
データの格納場所 | メインメモリ | GPU メモリ | GPU メモリ |
内容の更新 | ✔ | ✔.fill() を使う |
|
描画 | ✔ | ✔ | |
CPU からのアクセス | ✔ | ||
GPU (シェーダ) からのアクセス | ✔ | ✔ |
33.1 Image クラスの基本
Siv3D で画像データを扱うときは Image
クラスを使うのが便利です。Image
クラスでは、画像データを二次元配列クラス Gird
のようなインタフェースで扱えます。
Image
クラスは実質的には次のような構造です。
// 説明のため簡略化
class Image
{
Array<Color> m_data;
uint32 m_width;
uint32 m_height;
};
Image
型の変数 image
に対する、インデックスによる要素へのアクセス image[y][x] = value;
は、内部では m_data[y * m_width + x] = value;
になります。また、Point
型の値 pos
による image[pos] = value;
は、m_data[pos.y * m_width + pos.x] = value;
になります。
なお、Color
型は r, g, b, a の各色を uint8
型で保持する、4 バイトの構造体です。
// 説明のため簡略化
struct Color
{
uint8 r;
uint8 g;
uint8 b;
uint8 a;
};
画像ファイルから Image
を作成するには、Image
のコンストラクタ引数に、読み込みたい画像ファイルのパスを渡します。このファイルパスは、実行ファイルがあるフォルダ(開発中は App
フォルダ)を基準とする相対パスか、絶対パスを使用します。
また、Texture
のコンストラクタに Image
を渡すことで、Image
が保持する画像データからテクスチャを作成することができます。
次のサンプルでは、マウスカーソルで選択した、画像の任意の位置のピクセル色を取得し表示します。
# include <Siv3D.hpp>
void Main()
{
// 画像ファイルから Image を作成
const Image image{ U"example/windmill.png" };
// Image から Texture を作成
const Texture texture{ image };
while (System::Update())
{
const Point pos = Cursor::Pos();
if (InRange(pos.x, 0, image.width() - 1)
&& InRange(pos.y, 0, image.height() - 1))
{
// マウスカーソルの位置にあるピクセルの色を取得
const Color pixelColor = image[pos];
Rect{ 500, 20, 40 }.draw(pixelColor);
PutText(U"{}"_fmt(pixelColor), Arg::topLeft(560, 20));
}
texture.draw();
}
}
33.2 プログラムで画像を作成する
Image
のコンストラクタに幅、高さ、色を渡して、画像を作成することができます。
# include <Siv3D.hpp>
void Main()
{
const Image image{ 400, 300, Color{ 63, 127, 255 } };
const Texture texture{ image };
while (System::Update())
{
texture.draw();
}
}
なお、Texture
のコンストラクタに Image
を渡したときには、画像データが Texture
にコピーされるので、Texture
を作成したあとで Image
を破棄しても問題ありません。上記のプログラムでは、メインループ中に使われない image
がメモリを消費したままなので、次のようにするとメモリ消費量を減らせます。
# include <Siv3D.hpp>
Image MakeImage()
{
return Image{ 400, 300, Color{ 63, 127, 255 } };
}
void Main()
{
const Texture texture{ MakeImage() };
while (System::Update())
{
texture.draw();
}
}
33.3 ピクセルを編集する
Image
が持つ画像データの幅は .width()
, 高さは .height()
, 幅と高さは .size()
で取得できます。次のようなループで、Image
内のすべてのピクセルにアクセスできます。
# include <Siv3D.hpp>
Image MakeImage()
{
Image image{ 400, 300 };
for (int32 y = 0; y < image.height(); ++y)
{
for (int32 x = 0; x < image.width(); ++x)
{
image[y][x] = ColorF{ (x / 399.0), (y / 299.0), 1.0 };
}
}
return image;
}
void Main()
{
const Texture texture{ MakeImage() };
while (System::Update())
{
texture.draw();
}
}
次のように step(Size)
を使って、ループを 1 つにまとめることもできます。
# include <Siv3D.hpp>
Image MakeImage()
{
Image image{ 400, 300 };
for (auto p : step(image.size()))
{
image[p] = ColorF{ (p.x / 399.0), (p.y / 299.0), 1.0 };
}
return image;
}
void Main()
{
const Texture texture{ MakeImage() };
while (System::Update())
{
texture.draw();
}
}
33.4 range-based for
range-based for を使って、すべてのピクセルにアクセスすることもできます。
# include <Siv3D.hpp>
Image MakeImage()
{
Image image{ U"example/windmill.png" };
for (auto& pixel : image)
{
// R 成分と B 成分を入れ替える
std::swap(pixel.r, pixel.b);
}
return image;
}
void Main()
{
const Texture texture{ MakeImage() };
while (System::Update())
{
texture.draw();
}
}
33.5 画像を保存する
Image
の画像データを画像ファイルとして保存するには .save(path)
を使います。画像の保存形式は、path
の拡張子から自動的に適切なものが選択されます。
# include <Siv3D.hpp>
void Main()
{
Image image{ U"example/windmill.png" };
for (auto& pixel : image)
{
std::swap(pixel.r, pixel.b);
}
// 画像を保存
image.save(U"tutorial1.png");
while (System::Update())
{
}
}
33.6 ダイアログでファイル名を指定して画像を保存する
Image
の画像データを、ダイアログでファイル名を指定して画像ファイルとして保存するには .saveWithDialog()
を使います。.save()
同様、画像の保存形式は、path
の拡張子から自動的に適切なものが選択されます。
# include <Siv3D.hpp>
void Main()
{
Image image{ U"example/windmill.png" };
for (auto& pixel : image)
{
std::swap(pixel.r, pixel.b);
}
// ダイアログでファイル名を指定して画像を保存
image.saveWithDialog();
while (System::Update())
{
}
}
33.7 画像処理
Image
には様々な画像処理関数が用意されています。どの画像処理も、自身を変更するメンバ関数と、画像処理後の結果を返すメンバ関数の 2 種類があります。
処理 | 結果の画像の例 | 自身を変更するメンバ関数 / 結果を返すメンバ関数 |
---|---|---|
色の反転 | ![]() |
negate / negated
|
グレイスケール化 | ![]() |
grayscale / grayscaled
|
セピアカラー | ![]() |
sepia / sepiaed
|
ポスタライズ | ![]() |
posterize / posterized
|
明度レベル変更 | ![]() |
brighten / brightened
|
左右反転 | ![]() |
mirror / mirrored
|
上下反転 | ![]() |
flip / flipped
|
90° 回転 | ![]() |
rotate90 / rotated90
|
180° 回転 | ![]() |
rotate180 / rotated180
|
270° 回転 | ![]() |
rotate270 / rotated270
|
ガンマ補正 | ![]() |
gammaCorrect / gammaCorrected
|
二値化 | ![]() |
threshold / thresholded
|
大津の手法による二値化 | ![]() |
threshold_Otsu / thresholded_Otsu
|
適応的二値化 | ![]() |
adaptiveThreshold / adaptiveThresholded
|
モザイク | ![]() |
mosaic / mosaiced
|
拡散 | ![]() |
spread / spreaded
|
ブラー | ![]() |
blur / blurred
|
メディアンブラー | ![]() |
medianBlur / medianBlurred
|
ガウスぼかし | ![]() |
gaussianBlur / gaussianBlurred
|
バイラテラルフィルタ | ![]() |
bilateralFilter / bilateralFiltered
|
膨張 | ![]() |
dilate / dilated
|
収縮 | ![]() |
erode / eroded
|
拡大縮小 | ![]() |
scale / scaled
|
周囲に枠を加える | ![]() |
border / bordered
|
任意角度の回転 | ![]() |
なし / rotated
|
正方形での切り抜き | ![]() |
なし / squareClipped
|
# include <Siv3D.hpp>
void Main()
{
const Image image{ U"example/windmill.png" };
// ガウスぼかしした画像からテクスチャを作成
const Texture texture{ image.grayscaled() };
while (System::Update())
{
texture.draw();
}
}
33.8 画像に図形を書き込む
Circle
や Line
, Rect
などの図形型を、メンバ関数 .paint()
および .overwrite()
を使って Image
に書き込むことができます。.paint()
はアルファ値に応じて色をブレンドします。.overwrite()
は引数で指定した色をそのまま書き込むため、処理が高速です。また、次のサンプル星形のように、画像外にはみ出した部分は書き込まれません。
# include <Siv3D.hpp>
void Main()
{
Image image{ 600, 600, Palette::White };
Circle{ 100, 100, 100 }.overwrite(image, Palette::Orange);
Rect{ 150, 150, 300, 200 }.paint(image, ColorF{ 0.0, 1.0, 0.5, 0.5 });
Line{ 100, 400, 400, 200 }.overwrite(image, 10, Palette::Seagreen);
// Shape2D には .paint() / .overwrite() が無いので、.asPolygon() で Polygon 型にする
Shape2D::Star(200, Vec2{ 500, 200 }).asPolygon().overwrite(image, Palette::Yellow);
// 透明の穴をあける
Rect{ 400, 400, 50 }.overwrite(image, ColorF{ 1.0, 0.0 });
image.save(U"tutorial2.png");
const Texture texture{ image };
while (System::Update())
{
texture.draw();
}
}
33.9 画像に別の画像を書き込む
Image
を別の Image
に書き込むことができます。書き込みの対象を自分自身にすることはできません。書き込みに使うメンバ関数は次の通りです。
メンバ関数 | アルファブレンド | 書き込み先のアルファ値の更新 |
---|---|---|
.paint() .paintAt()
|
✔ | |
.stamp() .stampAt()
|
✔ | 大きいほう |
.overwrite() .overwriteAt()
|
✔ |
Image
は Texture
のように絵文字やアイコンもロードできます。
# include <Siv3D.hpp>
void Main()
{
Image image{ 600, 600, Palette::White };
const Image windmillImage{ U"example/windmill.png" };
const Image catImage{ U"🐈"_emoji };
windmillImage.overwrite(image, 40, 40);
// 透過ピクセルに対する paint / stamp / overwrite の違い
Rect{ 100, 400, 400, 40 }.overwrite(image, Color{ 255, 0 });
catImage.paintAt(image, 150, 400);
catImage.stampAt(image, 300, 400);
catImage.overwriteAt(image, 450, 400);
const Texture texture{ image };
while (System::Update())
{
texture.draw();
}
}
33.10 内容を更新できるテクスチャ DynamicTexture
ペイントアプリのように、Image
をプログラムの実行中に頻繁に変更し、その結果をシーンに描きたい場合、Image
の内容を変更するたびに古い Texture
を破棄して新しい Texture
を作成するのは非効率です。そのような用途では、DynamicTexture
を 1 度だけ作成し、そのを更新するのが効率的です。
DynamicTexture
は Texture
のメンバ関数に加え、.fill(image)
メンバ関数を持ちます。.fill()
は、DynamicTexture
が空の場合は image
で新しいテクスチャを作成し、既にデータを持っている場合はその内容を image
で置き換えます。このとき新旧の画像データの縦横サイズは一致している必要があります。DynamicTexture
の .fill()
は、既に保持しているデータを上書きするだけなので、新しく Texture
を作成するよりも効率的です。
なお、Image
への書き込みは .draw()
と異なり、CPU 上で行われるため、大量の書き込みは実行時性能を低下させます。テクスチャに対して頻繁に複雑な書き込みを行いたい場合は、次の章で扱う RenderTexture
を使うのが基本です。
33.10.1 絵文字を書き込む
# include <Siv3D.hpp>
void Main()
{
Image image{ 600, 600, Palette::White };
const Image emoji{ U"😃"_emoji };
DynamicTexture dtexture{ image };
while (System::Update())
{
if (MouseL.down())
{
emoji.paintAt(image, Cursor::Pos());
// DynamicTexture の中身を Image で更新
dtexture.fill(image);
}
dtexture.draw();
}
}
33.10.2 線を書き込む
次のようなプログラムでペイントアプリが作れます。
Image
の .fill(color)
は、その色で画像を塗りつぶすメンバ関数です。
# include <Siv3D.hpp>
void Main()
{
// キャンバスのサイズ
constexpr Size canvasSize{ 600, 600 };
// ペンの太さ
constexpr int32 thickness = 8;
// ペンの色
constexpr Color penColor = Palette::Orange;
// 書き込み用の画像データを用意
Image image{ canvasSize, Palette::White };
// 表示用のテクスチャ(内容を更新するので DynamicTexture)
DynamicTexture texture{ image };
while (System::Update())
{
if (MouseL.pressed())
{
// 書き込む線の始点は直前のフレームのマウスカーソル座標
// (初回はタッチ操作時の座標のジャンプを防ぐため、現在のマウスカーソル座標にする)
const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos();
// image に線を書き込む
Line{ from, to }.overwrite(image, thickness, penColor);
// 書き込み終わった image でテクスチャを更新
texture.fill(image);
}
// 描いたものを消去するボタンが押されたら
if (SimpleGUI::Button(U"Clear", Vec2{ 640, 40 }, 120))
{
// 画像を白で塗りつぶす
image.fill(Palette::White);
// 塗りつぶし終わった image でテクスチャを更新
texture.fill(image);
}
// テクスチャを表示
texture.draw();
}
}
33.11 画像にテキストを書き込む
画像にテキストを書き込むには、Font
から各文字の画像を BitmapGlyph
型で取得し、その画像をチュートリアル 14.19 の自由描画の要領で書き込みます、
# include <Siv3D.hpp>
void Main()
{
Image image{ 600, 600, Palette::White };
const Font font{ 60, Typeface::Bold };
{
const String text = U"Hello, Siv3D!\nこんにちは。";
constexpr Vec2 basePos{ 20, 20 };
Vec2 penPos{ basePos };
for (const auto& ch : text)
{
// 改行文字なら
if (ch == U'\n')
{
// ペンの X 座標をリセット
penPos.x = basePos.x;
// ペンの Y 座標をフォントの高さ分進める
penPos.y += font.height();
continue;
}
const BitmapGlyph bitmapGlyph = font.renderBitmap(ch);
// 文字のテクスチャをペンの位置に文字ごとのオフセットを加算して描画
// .asPoint() は Vec2 を Point に変換する関数
bitmapGlyph.image.paint(image, (penPos + bitmapGlyph.getOffset()).asPoint(), Palette::Seagreen);
// ペンの X 座標を文字の幅の分進める
penPos.x += bitmapGlyph.xAdvance;
}
}
const Texture texture{ image };
while (System::Update())
{
texture.draw();
}
}