Home / ぼやきごと / 2014-03-06 / C++:OpenCVでα付き画像の縁取りをしてみた
C++:OpenCVでα付き画像の縁取りをしてみた

C++:OpenCVでα付き画像の縁取りをしてみた

α付きのPNG画像素材をαで抜かれた縁に沿って縁取りしたいと思い、せっかくなので前から触ってみたかったOpenCVライブラリで簡単なツールを作ってみました。

左の天使のように可愛い天子の画像を作成したツールにドロップすると、右の天使のように可愛い天子の縁取り画像になります。
紫色の部分は実際には透過されています。*1

天使のように可愛い天子の画像天使のように可愛い天子の縁取り画像

ソースコードは下記の通り。
C++11にある程度対応したコンパイラが必要です。

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
-
-
|
|
|
!
|
-
-
!
|
|
-
!
|
|
|
|
-
|
|
|
!
|
|
!
|
-
|
|
|
!
|
-
|
|
|
|
|
!
|
-
|
|
|
|
|
|
|
!
|
|
|
|
-
-
!
|
|
-
!
|
-
!
|
|
|
|
|
|
-
|
!
|
|
!
|
-
|
|
|
|
!
|
-
-
|
|
|
!
|
|
-
!
|
|
|
!
|
-
|
|
|
|
|
|
!
|
|
|
|
-
-
!
|
-
!
-
|
!
|
-
!
|
|
-
!
-
|
|
|
!
|
-
!
|
-
!
|
-
!
|
-
!
|
-
!
!
!
 
-
!
-
-
!
-
|
!
|
|
!
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <algorithm>
#include <vector>
#include <cstdint>
 
// インポートライブラリ(VC++用)
#ifdef _MSC_VER
#ifdef NDEBUG
#define OPENCV_LIB(name) "opencv_" #name ".lib"
#else
#define OPENCV_LIB(name) "opencv_" #name "d.lib"
#endif // NDEBUG
#pragma comment(lib, OPENCV_LIB(core248))
#pragma comment(lib, OPENCV_LIB(imgproc248))
#pragma comment(lib, OPENCV_LIB(highgui248))
#endif // _MSC_VER
 
namespace
{
    /**
     * @brief 画像データを float 型のチャンネル平面配列に変換する。
     * @param[in] src 画像データ。
     * @return float 型のチャンネル平面配列。
     */
    std::vector<cv::Mat> to_32f_channels(const cv::Mat& src)
    {
        // チャンネルごとに分離
        std::vector<cv::Mat> planes;
        cv::split(src, planes);
 
        // 全チャンネルを float 型に変換
        std::transform(
            planes.begin(),
            planes.end(),
            planes.begin(),
            [](const cv::Mat& p)
            {
                cv::Mat dest;
                p.convertTo(dest, CV_32FC1);
                return dest;
            });
 
        return planes;
    }
 
    /**
     * @brief チャンネル平面配列を uint8 型の画像データに変換する。
     * @param[in] src_planes チャンネル平面配列。
     * @return uint8 型の画像データ。
     */
    cv::Mat to_8u_image(const std::vector<cv::Mat>& src_planes)
    {
        cv::Mat temp, dest;
        cv::merge(src_planes, temp);
        temp.convertTo(dest, CV_MAKETYPE(CV_8U, src_planes.size()));
 
        return dest;
    }
 
    /**
     * @brief チャンネル平面別のアルファブレンドを行う。
     * @param[in] back_planes ブレンドされる側のチャンネル平面配列。
     * @param[in] front_planes ブレンドする側のチャンネル平面配列。
     * @param[in] front_alpha αチャンネル平面。
     * @return ブレンド結果のチャンネル平面配列。
     *
     * back_planes と front_planes のうち要素数の少ない方に合わせる。
     */
    std::vector<cv::Mat> blend_for_channels(
        const std::vector<cv::Mat>& back_planes,
        const std::vector<cv::Mat>& front_planes,
        const cv::Mat& front_alpha)
    {
        // 0.0〜1.0 にスケールしたαチャンネル平面、およびその逆転値を作成
        const auto scale_alpha = front_alpha * (1.0 / 255);
        const auto scale_alpha_inv = cv::Scalar::all(1) - scale_alpha;
 
        // チャンネル平面数決定
        const auto plane_count = std::min(back_planes.size(), front_planes.size());
 
        // チャンネル平面ごとにアルファブレンド
        std::vector<cv::Mat> dest_planes(plane_count);
        std::transform(
            back_planes.begin(),
            back_planes.begin() + plane_count,
            front_planes.begin(),
            dest_planes.begin(),
            [&](const cv::Mat& back, const cv::Mat& front)
            {
                return back.mul(scale_alpha_inv) + front.mul(scale_alpha);
            });
 
        return dest_planes;
    }
 
    /**
     * @brief 画像データの各チャンネル平面成分を4近傍膨張させる。
     * @param[in] src 画像データ。
     * @param[in] size 膨張回数。
     * @return 膨張させた画像データ。
     */
    cv::Mat dilate_image(const cv::Mat& src, int size = 10)
    {
        // 膨張用マトリクス作成
        // [ 0, 1, 0
        //   1, 1, 1
        //   0, 1, 0 ]
        const cv::Mat elem =
            (cv::Mat_<std::uint8_t>(3, 3) << 0, 1, 0, 1, 1, 1, 0, 1, 0);
 
        // 膨張
        cv::Mat dest;
        cv::dilate(src, dest, elem, cv::Point(-1, -1), size);
 
        return dest;
    }
 
    /**
     * @brief α付き画像ファイルを読み込み、縁取りを施して書き戻す。
     * @param[in] filename ファイルパス。
     * @param[in] hem_color 縁取り色。既定値は白。
     * @param[in] hem_size αの膨張回数。縁取り幅の基となる。既定値は 10 。
     * @retval true  成功した場合。
     * @retval false 失敗した場合。
     */
    bool hem_alpha_image_file(
        const char* filename,
        const cv::Scalar& hem_color = cv::Scalar::all(255),
        int hem_size = 10)
    {
        // ファイル読み込み
        const auto src_img = cv::imread(filename, -1);
 
        // uint8 型の4チャンネル画像でなければダメ
        if (src_img.empty() || src_img.elemSize1() != 1 || src_img.channels() != 4)
        {
            return false;
        }
 
        // float 型の各チャンネル平面に分離
        const auto img_planes = to_32f_channels(src_img);
        const auto img_alpha = img_planes[3];
 
        // 元画像と同じサイズの縁取り色画像をチャンネル平面別に作成
        const std::vector<cv::Mat> hem_planes =
            {
                cv::Mat(src_img.rows, src_img.cols, CV_32FC1, hem_color[0]),
                cv::Mat(src_img.rows, src_img.cols, CV_32FC1, hem_color[1]),
                cv::Mat(src_img.rows, src_img.cols, CV_32FC1, hem_color[2]),
            };
 
        // 元画像を縁取り色画像へチャンネル平面ごとにアルファブレンド
        auto dest_planes = blend_for_channels(hem_planes, img_planes, img_alpha);
 
        // αチャンネル平面を膨張させる
        const cv::Mat dilate_alpha = dilate_image(img_alpha, hem_size);
 
        // 膨張させたαチャンネル平面を dest_planes に追加
        dest_planes.push_back(dilate_alpha);
 
        // uint8 型の4チャンネル画像にする
        const cv::Mat dest_img = to_8u_image(dest_planes);
 
        // 元ファイルに書き出す
        return cv::imwrite(filename, dest_img);
    }
}
 
/// メイン関数。
int main(int argc, char* argv[])
{
    // 引数に渡された分だけ処理する
    for (int i = 1; i < argc; ++i)
    {
        hem_alpha_image_file(argv[i]);
    }
 
    return 0;
}

処理としては、「縁取り色で塗り潰した画像に元画像を合成したRGB平面」と「α成分を膨張させたα平面」をそれぞれ作成し、最後にそれらを1つの画像にマージしています。

頭の中で考えた処理が大体考えた通りに書ける、良いライブラリですね。
ちょっとした画像処理ツールが欲しい時に便利そうです。

Category: [C++][プログラミング][CG技術] - 2014-03-06 23:13:48

*1 実際には半透明ピクセルもあるため、このページに貼った画像をそのまま紫透過させて利用することはできません。
添付ファイル: file天子_縁取り後.png 223件 [詳細] file天子_縁取り前.png 221件 [詳細]