読者です 読者をやめる 読者になる 読者になる

Bitmap読み書き

下記記事にて、Windows標準ライブラリで画像を読み書きする方法を記述しました。
GDI+ --- 画像ファイル読み書き - 何でもプログラミング

Bitmapであれば更に何のライブラリも必要なく簡単に読み書きできます。

ちょっとした画像関係の動作確認レベルの時に便利です。

今回は24bitBitmapを読み書きしてみます。

ヘッダ構造体

Bitmapファイルのヘッダ部分をまとめた構造体です。

コンパイラが勝手にパディングを入れないよう、pshpack2、poppackで挟んでいます。(VisualC++専用)

#include <pshpack2.h>
struct BitmapHeader
{
    unsigned short bfType; 
    unsigned long bfSize;
    unsigned short bfReserved1;
    unsigned short bfReserved2;
    unsigned long bfOffBits;
    unsigned int biSize;
    int biWidth;
    int biHeight;
    unsigned short biPlanes;
    unsigned short biBitCount;
    unsigned int biCompression;
    unsigned int biSizeImage;
    int biXPixPerMeter;
    int biYPixPerMeter;
    unsigned int biClrUsed;
    unsigned int biClrImporant;
};
#include <poppack.h>


読み込み

Bitmapの画像データは、幅が4Byteの倍数になるよう格納されています。

今回はデータを詰めて取り出すよう実装しています。

void LoadBitmap24(std::string path, int* width, int* height, std::vector<unsigned char>* pixels)
{
    std::ifstream file(path, std::ios::binary);
    assert(file.is_open());

    BitmapHeader header;
    file.read((char*)&header, sizeof(header));
    int w = header.biWidth;
    int h = header.biHeight;
    int step = ToBitmapStep(3 * w);

    std::vector<unsigned char> originalPixels(step * h);
    file.read((char*)originalPixels.data(), originalPixels.size());

    pixels->resize(3 * w * h);
    for (int i = 0; i < h; ++i)
        memcpy(pixels->data() + i * 3 * w, originalPixels.data() + i * step, 3 * w);

    *width = w;
    *height = h;
}
int ToBitmapStep(int step)
{
    int paddings[] = { 0, 3, 2, 1 };
    return step + paddings[step % 4];
}


書き込み

ヘッダの内容は、ほとんど決められた値になっています。

画像データを、幅が4Byteの倍数になるよう調整して書き出しています。

void SaveBitmap24(std::string path, int width, int height, const unsigned char* pixels)
{
    std::ofstream file(path, std::ios::binary);
    assert(file.is_open());

    int step = ToBitmapStep(3 * width);

    BitmapHeader header;
    header.bfType = 0x4d42; // "BM"
    header.bfSize = step * height + 54;
    header.bfReserved1 = 0;
    header.bfReserved2 = 0;
    header.bfOffBits = 54;
    header.biSize = 40;
    header.biWidth = width;
    header.biHeight = height;
    header.biPlanes = 1;
    header.biBitCount = 24;
    header.biCompression = 0;
    header.biSizeImage = 0;
    header.biXPixPerMeter = 0;
    header.biYPixPerMeter = 0;
    header.biClrUsed = 0;
    header.biClrImporant = 0;
    file.write((char*)&header, sizeof(header));

    std::vector<unsigned char> bmpPixels(step * height);
    for (int i = 0; i < height; ++i)
        memcpy(bmpPixels.data() + i * step, pixels + i * 3 * width, 3 * width);
    file.write((char*)bmpPixels.data(), bmpPixels.size());
}


動作確認

Bitmapを読み込んで、赤青反転の後にBitmap保存をしてみます。

int main()
{
    int width, height;
    std::vector<byte> pixels;
    LoadBitmap24("parrots.bmp", &width, &height, &pixels);

    for (int i = 0; i < width * height; ++i)
        std::swap(pixels[3 * i], pixels[3 * i + 2]);

    SaveBitmap24("output.bmp", width, height, pixels.data());
    return 0;
}
f:id:any-programming:20170319005947j:plain f:id:any-programming:20170319010003j:plain






WindowsでOpenGLES --- imgui導入

OpenGL上でGUIを実装するライブラリの一つに、ImGuiがあります。
GitHub - ocornut/imgui: Bloat-free Immediate Mode Graphical User interface for C++ with minimal dependencies

数ファイル取り込むだけでGUIを実装できるため、簡単なソフトの作成には便利です。

今回はGLFW上でImGuiを動作させてみたいと思います。

GLFWに関しましては下記記事を参照してください。
WindowsでOpenGLES --- GLFW導入 - 何でもプログラミング

Immediate Mode GUI

このライブラリは "Immediate Mode GUI" という方式を採用しており、一般的な、ボタンオブジェクトを作ってテキストを変更して…といった "Retained Mode GUI" とは異なります。

Immediate Modeでは毎回GUIを描き直すため、既にあるGUIツリーを更新していくRetained Modeに比べて、わかりやすいコードになる傾向があります。

ただしライブラリ側でstatic変数に状態を持っていたりするため、Immediate Modeが完全優位というわけでもありません。

個人的には毎回GUIの作り直し(一部のみの更新をしない)が重要だと思いますので、Retained ModeであってもwebのReactのようなフレームワークがあれば変わらないと思います。

ファイル取り込み

GitHubからダウンロードし、下記ファイルをプロジェクトに追加します。

imgui_impl_glfw_gl3はexamples/opengl3_example/以下に入っています。

f:id:any-programming:20170425160757p:plain

imgui_impl_glfw_gl3.cppの修正

元のままではOpenGL3を利用するようになっていますので、OpenGLES3に対応するよう修正します。

// GLES3/gl3.hをincludeするように
-#include <GL/gl3w.h>
+#define GLFW_INCLUDE_ES3
+#define GL_GLEXT_PROTOTYPES

// GL_BLEND_SRCは存在しないのでGL_BLEND_SRC_ALPHAに
-GLint last_blend_src; glGetIntegerv(GL_BLEND_SRC, &last_blend_src);
-GLint last_blend_dst; glGetIntegerv(GL_BLEND_DST, &last_blend_dst);
+GLint last_blend_src; glGetIntegerv(GL_BLEND_SRC_ALPHA, &last_blend_src);
+GLint last_blend_dst; glGetIntegerv(GL_BLEND_DST_ALPHA, &last_blend_dst);

// Vertexシェーダのversion変更
-"#version 330\n"
+"#version 300 es\n"

// Fragmentシェーダのversion変更、precisionの設定
-"#version 330\n"
+"#version 300 es\n"
+"precision mediump float;\n"


アプリケーションコード

ImGui_ImplGlfwGL3_Initで初期化し、描画ごとにImGui_ImplGlfwGL3_NewFrameとImGui::Render()を呼び出します。

今回はImGui::ShowTestWindowを用いてサンプルを描画しています。

毎フレーム描画する必要がない場合はglfwPollEventsの代わりにglfwWaitEventsを利用してください。

フレームレートはglfwSwapInterval(1)でディスプレイと同期するようになるのですが、Windowsでは呼ばなくてもディスプレイと同期しているようです。

#define GLFW_INCLUDE_ES3
#define GL_GLEXT_PROTOTYPES
#include <GLFW\glfw3.h>

#include "imgui.h"
#include "imgui_impl_glfw_gl3.h"

#pragma comment(lib, "glfw3.lib")
#pragma comment(lib, "libGLESv2.lib")

int main()
{
    assert(glfwInit());

    glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
    glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);

    GLFWwindow* window = glfwCreateWindow(650, 350, "ImGui", NULL, NULL);    
    assert(window);
    glfwMakeContextCurrent(window);
       
    assert(ImGui_ImplGlfwGL3_Init(window, true));

    glfwSetWindowSizeCallback(window, [](GLFWwindow*, int w, int h) 
    {  
        glViewport(0, 0, w, h);
    });
        
    while (glfwWindowShouldClose(window) == GL_FALSE)
    {
        glfwPollEvents();

        glClearColor(0, 0, 0, 0);
        glClear(GL_COLOR_BUFFER_BIT);

        ImGui_ImplGlfwGL3_NewFrame();
        ImGui::ShowTestWindow();
        ImGui::Render();

        glfwSwapBuffers(window);
    }

    ImGui_ImplGlfwGL3_Shutdown();
    glfwTerminate();

    return 0;
}

f:id:any-programming:20170425182525p:plain





WindowsでOpenGLES --- 三角形描画

下記記事にてGLFWで、OpenGLESが初期化されたウィンドウを作成しました。
WindowsでOpenGLES --- GLFW導入 - 何でもプログラミング

今回はとりあえず三角形を描画してみたいと思います。

shared_ptrでリソースの管理

OpenGLではCreateとDeleteをきちんと管理する必要があります。(glCreateProgram() → glDeleteProgram(GLuint)など)

手動で管理するのは面倒なので、shared_ptrに解放を任せてみます。

shared_ptrにはカスタムデリータが設定できるので、そこで解放を行います。

OpenGLにはglCreateProgramのような単体リソース作成と、glGenTexturesのような複数リソース作成の2パターンがあるので、2つ定義してみました。

今回は暗黙キャストが利用したかったのでクラスを定義しましたが、不必要であればshared_ptrそのもので良いと思います。

class GLResource
{
    std::shared_ptr<GLuint> _resource;
public:
    GLResource() {}
    // glCreateProgram, glDeleteProgramなど用
    GLResource(GLuint resource, std::function<void(GLuint)> glDelete)
    {
        if (resource != 0)
            _resource.reset(new GLuint(resource), [=](GLuint* x) { glDelete(*x); delete x; });
    }
    // glGenTextures、glDeleteTextursなど用
    GLResource(std::function<void(GLsizei, GLuint*)> glGen, std::function<void(GLsizei, GLuint*)> glDelete)
    {
        GLuint resource = 0;
        glGen(1, &resource);
        if (resource != 0)
            _resource.reset(new GLuint(resource), [=](GLuint* x) { glDelete(1, x); delete x; });
    }
    operator bool()   { return (bool)_resource; }
    operator GLuint() { return *_resource; }
};


Program(Shader)作成

シェーダコードをコンパイルしてプログラムを作成する関数を定義します。

CompileShader、PrintProgramLogは後述いたします。

GLResource CreateProgram(std::string vertexCode, std::string fragmentCode)
{
    // Vertexシェーダコンパイル
    GLResource vertexShader(glCreateShader(GL_VERTEX_SHADER), glDeleteShader);
    assert(vertexShader);
    CompileShader(vertexShader, vertexCode.c_str());

    // Fragmentシェーダコンパイル
    GLResource fragmentShader(glCreateShader(GL_FRAGMENT_SHADER), glDeleteShader);
    assert(fragmentShader);
    CompileShader(fragmentShader, fragmentCode.c_str());

    GLResource program(glCreateProgram(), glDeleteProgram);
    assert(program);

    // リンクプログラム
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    glLinkProgram(program);
    PrintProgramLog(program);
    glDetachShader(program, fragmentShader);
    glDetachShader(program, vertexShader);

    // リンクエラー確認
    GLint status;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    assert(status != GL_FALSE);

    return program;
}

CompileShader

void CompileShader(GLuint shader, const GLchar* code)
{
    glShaderSource(shader, 1, &code, NULL);
    glCompileShader(shader);

    PrintShaderLog(shader);

    // コンパイルエラー確認
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    assert(status != GL_FALSE);
}
void PrintShaderLog(GLuint shader)
{
    GLint length;
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
    if (0 < length)
    {
        std::vector<GLchar> log(length);
        glGetShaderInfoLog(shader, log.size(), &length, log.data());
        printf("%s\n", log.data());
    }
}

PrintProgramLog

void PrintProgramLog(GLuint program)
{
    GLint length;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
    if (0 < length)
    {
        std::vector<GLchar> log(length);
        glGetProgramInfoLog(program, log.size(), &length, log.data());
        printf("%s\n", log.data());
    }
}


main関数

入力ポリゴンを赤色に塗りつぶすだけのGLSLで、三角形を描画しています。

ウィンドウのサイズ変更に追従するよう、コールバックを登録しています。

GLFWのコールバック登録は関数ポインタであり、ローカル変数キャプチャのためにdrawグローバル変数を用意しました。

std::function<void()> draw;

int main()
{
    assert(glfwInit());

    glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
    glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);

    GLFWwindow* window = glfwCreateWindow(200, 200, "Triangle", NULL, NULL);
    assert(window);
    glfwMakeContextCurrent(window);

    // Program準備
    std::string vertexCode =
        "#version 300 es\n"
        "layout (location = 0) in vec4 position;\n"
        "void main() { gl_Position = position; }";
    std::string fragmentCode =
        "#version 300 es\n"
        "precision mediump float;\n"
        "out vec4 fragColor;\n"
        "void main() { fragColor = vec4(1.0, 0.0, 0.0, 1.0); }";
    GLResource program = CreateProgram(vertexCode, fragmentCode);
    glUseProgram(program);

    // 頂点データ設定    
    glEnableVertexAttribArray(0);
    const GLfloat positions[] =
    {
         0.0,  1.0,
         1.0, -1.0,
        -1.0, -1.0
    };
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, positions);

    // Draw関数定義
    draw = [&]
    {
        glClearColor(0, 0, 0, 1);
        glClear(GL_COLOR_BUFFER_BIT);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(window);
    };

    // コールバック登録
    glfwSetWindowSizeCallback(window, [](GLFWwindow*, int w, int h) 
    {  
        glViewport(0, 0, w, h);
    });        
    glfwSetWindowRefreshCallback(window, [](GLFWwindow*)
    {
        draw();
    });

    // メインループ
    while (glfwWindowShouldClose(window) == GL_FALSE)
    {
        glfwWaitEvents();
    }

    glfwTerminate();

    return 0;
}

f:id:any-programming:20170425133858p:plain





WindowsでOpenGLES --- GLFW導入

下記の記事にてWIN32APIでOpenGLESを利用する方法を記述しました。
WindowsでOpenGLES --- ANGLE準備 - 何でもプログラミング

今回は、ウィンドウの作成やOpenGLESの初期化などをクロスプラットフォームに行ってくれるライブラリ、GLFWを導入してみたいと思います。

ダウンロード

下記ページより必要なdll(glfw3.dll)を取得します。(ソースをCMakeでビルドする方法でも問題ありません。)
GLFW - Download
f:id:any-programming:20170421172955p:plain

GLFW初期化

OpenGLES3を有効化するために、いくつか設定を行います。

あとはglfwCreateWindowを呼ぶだけで、必要な初期化を全て行ってくれます。

#define GLFW_INCLUDE_ES3
#define GL_GLEXT_PROTOTYPES
#include <GLFW\glfw3.h>

#pragma comment(lib, "glfw3.lib")
#pragma comment(lib, "libGLESv2.lib")

assert(glfwInit());

glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);

GLFWwindow* window = glfwCreateWindow(300, 300, "GL Window", NULL, NULL);
assert(window);
glfwMakeContextCurrent(window);


Draw関数

ウィンドウを灰色に塗りつぶすだけの関数です。

void Draw(GLFWwindow* window)
{
    glClearColor(0.5, 0.5, 0.5, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    glfwSwapBuffers(window);
}


Run

下記のようにイベントループを回します。

また再描画が必要な時にDrawを呼ぶよう、コールバックをアタッチしています。

実行すると灰色に塗りつぶされたウィンドウが表示されます。

glfwSetWindowRefreshCallback(window, Draw);

while (glfwWindowShouldClose(window) == GL_FALSE)
{
    glfwWaitEvents();
}

f:id:any-programming:20170420000211p:plain





WindowsでOpenGLES --- ANGLE準備

WindowsでOpenGLESを使いたい場合は、Google開発のANGLEが利用できます。
GitHub - google/angle: A conformant OpenGL ES implementation for Windows, Mac and Linux.

ANGLEを利用すれば、OpenGLESで記述したものを、Direct3DやDesktop OpenGLレンダリングしてくれます。(現在デフォルトでDirect3D11を利用。eglGetPlatformDisplayEXTで変更可能)

今回はANGLEをビルドして、動作確認をしてみたいと思います。

ビルド

WindowsでビルドするにはGypを利用する必要がありますが、Microsoftが下記のリポジトリを作成してくれているので、こちらを利用します。
GitHub - Microsoft/angle: ANGLE: OpenGL ES to DirectX translation for Windows Store, Windows Phone and Desktop

ダウンロードしたら、src/angle.slnを開いてビルドします。
f:id:any-programming:20170419223925p:plain

gypフォルダの下にビルド結果が配置されます。

基本的に利用するのはlibEGL.dllとlibGLESv2.dllの2つです。
f:id:any-programming:20170419224050p:plain

ウィンドウ準備

今回はWin32APIでウィンドウを作成します。

ちなみにANGLEではウィンドウ(HWND)を準備することなくオフスクリーンレンダリングすることも可能です。

WNDCLASSEX wndclass;
wndclass.cbSize        = sizeof(WNDCLASSEX);
wndclass.style         = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
wndclass.lpfnWndProc   = WndProc;
wndclass.cbClsExtra    = 0;
wndclass.cbWndExtra    = 0;
wndclass.hInstance     = GetModuleHandle(NULL);
wndclass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor       = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = NULL;
wndclass.lpszMenuName  = NULL;
wndclass.lpszClassName = L"GL Window";
wndclass.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

RegisterClassEx(&wndclass);

HWND hwnd = CreateWindow(
    L"GL Window",
    L"GL Window",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT,
    NULL,
    NULL,
    wndclass.hInstance,
    NULL);

ShowWindow(hwnd, SW_SHOWNORMAL);

WndProc

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 
{
    switch (msg) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}


OpenGLES初期化

display、config、contextの順に作成し、最後にsurfaceを作成しMakeCurrentを行います。

今回はES3.0を利用するよう初期化しています。

hwndは上記で作成したものを利用します。

#include <EGL\egl.h>
#pragma comment(lib, "libEGL.lib")

// display
EGLDisplay display = eglGetDisplay(GetDC(hwnd));
assert(display);

EGLint major, minor;
assert(eglInitialize(display, &major, &minor));

assert(eglBindAPI(EGL_OPENGL_ES_API));

// config
EGLint configAttributes[] =
{
    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
    EGL_BUFFER_SIZE, 32,
    EGL_NONE
};
EGLConfig config;
EGLint num_config;
assert(eglChooseConfig(display, configAttributes, &config, 1, &num_config));
    
// context
EGLint contextAttributes[] =
{
    EGL_CONTEXT_CLIENT_VERSION, 3,
    EGL_NONE
};
EGLContext context = eglCreateContext(display, config, NULL, contextAttributes);
assert(context);

// surface
EGLSurface surface = eglCreateWindowSurface(display, config, hwnd, NULL);
assert(surface);

assert(eglMakeCurrent(display, surface, surface, context));


描画

WndProcのWM_PAINTに描画処理を追加します。

今回は背景を灰色に塗るだけです。

#define GL_GLEXT_PROTOTYPES
#include <GLES3\gl3.h>
#pragma comment(lib, "libGLESv2.lib")

case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    glClearColor(0.5, 0.5, 0.5, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    assert(eglSwapBuffers(display, surface));

    EndPaint(hwnd, &ps);
}


Run

メッセージループを開始します。

灰色に塗りつぶされたウィンドウが表示されます。

MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

f:id:any-programming:20170420000211p:plain





Dlib --- 環境準備

DlibはC++で記述された機械学習用のライブラリです。

SVMやDeep Learning等、よく聞くアルゴリズムのものは一通り準備されています。

ドキュメントやサンプルが整っているのが一番嬉しい点だと思います。

Dlibダウンロード

下記サイトよりファイル一式をダウンロードし、好きな場所に解凍してください。

dlib C++ Library
f:id:any-programming:20170317171903p:plain

ビルドせずに利用

一般的にはCMakeを利用してlibファイルを作成し、それをリンクして利用します。

他の方法として、cppをそのままプロジェクトに追加して利用することも可能です。

ビルド自体は10秒程度で終わりますので、libのVC++のバージョン管理が面倒な場合はcpp追加で利用するのもいいと思います。

今回はcppを利用する方法で進めていきます。

プロジェクトの準備

VC++のWin32コンソールプロジェクトを想定しています。

ソースに、既存のファイルの追加で、dlib解凍先/dlib/all/source.cpp を追加します。
f:id:any-programming:20170319121011p:plain

プリコンパイルのエラー回避のため、source.cppの先頭に #include "stdafx.h" を追加します。

また今回はGUIを利用しないため、#define DLIB_NO_GUI_SUPPORT も追加しておきます。

#include "stdafx.h"
#define DLIB_NO_GUI_SUPPORT

// Copyright (C) 2006  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_ALL_SOURCe_
#define DLIB_ALL_SOURCe_
...


最後にinclude参照先として、dlib解凍先を指定します。
f:id:any-programming:20170319121605p:plain

アプリケーションコード

今回はdlibのサンプル画像で顔認識をしてみます。

frontal_face_detectorを利用して、顔のrectangleを取得しています。

今回はdlibのjpeg読み込みをONにしていないため、bmpに変換して読み込んでいます。

source.cppに #define DLIB_JPEG_SUPPORT を追加して、さらに dlib解凍先/dlib/external/libjpeg の全cppファイルをプロジェクトに追加すればjpegを読み込めるようになりますが、ここまでくればCMakeでlibを作成する方が楽だと思います。

#include "stdafx.h"

#include "dlib\image_processing\frontal_face_detector.h"
#include "dlib\image_io.h"

int main()
{
    dlib::array2d<dlib::rgb_pixel> image;
    dlib::load_bmp(image, "C:\\lib\\dlib-19.4\\examples\\faces\\2008_001322.bmp");

    auto detector = dlib::get_frontal_face_detector();
    auto rectangles = detector(image);

    for (auto& rect : rectangles)
       dlib::draw_rectangle(image, rect, dlib::rgb_pixel(255, 0, 0), 3);

    dlib::save_bmp(image, "output.bmp");

    return 0;
}
f:id:any-programming:20170319121911j:plain f:id:any-programming:20170319121920j:plain





GDI+ --- 画像ファイル読み書き

C++で画像ファイルを読み書きする際は、libjpegやlibpng等を利用します。

しかしWindowsでは標準ライブラリでないため、ライブラリを準備する必要があります。

ちょっと動作確認の時など、ライブラリの準備が面倒な時はGDI+を利用することができます。

コードはエラー処理を省いてあります。(返り値がGdiplus::Statusの関数は、Gdiplus::OKかどうか確認してください。)

画像読み込み

Gdiplus::Bitmapを利用して画像を読み込みます。

画素データは4Byte単位のStrideで保存されており、今回は余白を切り詰めて取得するよう実装してあります。

LockBits時に渡すフォーマットは、元データのフォーマットである必要はなく、可能であれば自動で変換してくれます。

void GdiplusLoadImage(std::wstring path, int& width, int&height, std::vector<byte>& pixels)
{
    auto image = Gdiplus::Bitmap::FromFile(path.c_str());
    width = image->GetWidth();
    height = image->GetHeight();
    auto stride = 3 * width;

    pixels.resize(stride * height);
    Gdiplus::Rect rect(0, 0, width, height);
    Gdiplus::BitmapData bmpData;
    image->LockBits(&rect, Gdiplus::ImageLockModeRead, PixelFormat24bppRGB, &bmpData);

    for (int y = 0; y < height; ++y)
        memcpy(pixels.data() + y * stride, (byte*)bmpData.Scan0 + y * bmpData.Stride, stride);
    
    image->UnlockBits(&bmpData);
}


画像保存(jpeg

読み込み時と同様に、Gdiplus::Bitmapを用いて画像を保存します。

画素データは4Byte単位である必要があるので、余白を追加したものを作成しています。

エンコーダのIDはGdiplus::GetImageEncodersで探すのが普通ですが、IDが変わる可能性はほぼ無いと考えられるため直指定しています。(その他例えばpngの場合は0x557cf406...となります。)

void GdiplusSaveJpeg(std::wstring path, int width, int height, const std::vector<byte>& pixels)
{
    auto stride = 3 * width;
    auto gdiStride = stride % 4 == 0
        ? stride
        : stride + 4 - stride % 4;

    std::vector<byte> gdiPixels(gdiStride * height);
    for (int y = 0; y < height; ++y)
        memcpy(gdiPixels.data() + y * gdiStride, pixels.data() + y * stride, stride);

    Gdiplus::Bitmap image(width, height, gdiStride, PixelFormat24bppRGB, gdiPixels.data());
    CLSID id = { 0x557cf401, 0x1a04, 0x11d3,{ 0x9a, 0x73, 0x00, 0x00, 0xf8, 0x1e, 0xf3, 0x2e } };
    image.Save(path.c_str(), &id);
}


アプリケーションコード

GDI+を利用するには、GdiplusStartupとGdiplusShutdownを呼ぶ必要があります。

今回は入力画像を赤青反転して出力するコードを作成してみました。

#include <Windows.h>
#include <gdiplus.h>
#include <gdipluspixelformats.h> // PixelFormat24bppRGB
#pragma comment(lib, "gdiplus.lib")

int main()
{
    Gdiplus::GdiplusStartupInput input;
    ULONG_PTR token;
    Gdiplus::GdiplusStartup(&token, &input, NULL);

    int width, height;
    std::vector<byte> pixels;
    GdiplusLoadImage(L"Parrots.bmp", width, height, pixels);

    for (int i = 0; i < width * height; ++i)
        std::swap(pixels[3 * i], pixels[3 * i + 2]);

    GdiplusSaveJpeg(L"output.jpg", width, height, pixels);

    Gdiplus::GdiplusShutdown(token);

    return 0;
}
f:id:any-programming:20170319005947j:plain f:id:any-programming:20170319010003j:plain