JPEG画像にフィルタをかけてみる
■前回記事からの続き
JPEG画像の入出力について記事を書きました。
mame-mame.hatenadiary.com
今回のテーマは、
読み込んだJPEG画像に適当なフィルタをあてて出力
です。
C++らしくクラスを微妙に使ってみます。
前回記事では、
①JPEG画像を取り扱うためのライブラリの取得
②JPEG画像を取得して画素値を抽出する方法
③JPEG画像を新たに保存する方法
とすれば、
画像に何らかのフィルタをかけたい、というのであれば。
①⇒②⇒「画素値にフィルタをかける」⇒③とやれば良い、と言いました。
今回は「画素値にフィルタをかける」を別関数で定義しました。
■フィルタ処理実装
今回もmain関数の所をペタッとまず貼ってしまいます。
ほとんど「JPEG入出力」の記事から変えていません。
#include <stdio.h> #include <cstdlib> #include <cstring> #include "jpeglib.h" #include "Filter.h" #define RET_TRUE 0 #define RET_FALSE -1 //JPG画像を読み込み&書き込み処理する関数を実行する int main(int argc, char* argv[]) { //エラー用変数 errno_t error; //入力変数を格納 char *inFileName = argv[1]; //入力ファイル名 char *outFileName = argv[2]; //出力ファイル名 int type = atoi(argv[3]); //フィルタタイプ //------JPEG読み込み------// // (1)画像データを扱う構造体を確保 struct jpeg_decompress_struct inInfo; //画像データ構造体を定義 jpeg_create_decompress(&inInfo); //構造体初期化処理 struct jpeg_error_mgr jErr; //エラー情報構造体を定義 inInfo.err = jpeg_std_error(&jErr); //デフォルト値で設定 // (2)JPEGファイルをオープン FILE *inFile; error = fopen_s(&inFile, inFileName, "rb"); //ファイル読み込み if (error != 0) { return RET_FALSE; } jpeg_stdio_src(&inInfo, inFile); //ファイルポインタとinInfoを渡す // (3)ヘッダ読み込み if (jpeg_read_header(&inInfo, TRUE) != JPEG_HEADER_OK) { return RET_FALSE; } int width = inInfo.image_width; //幅 int height = inInfo.image_height; //高さ int ch = inInfo.num_components; //チャネル数 // (4)展開処理を開始 jpeg_start_decompress(&inInfo); if (inInfo.out_color_space != JCS_RGB) { //sRGB以外を受け付けない return RET_FALSE; } // (5)画素値を取得 unsigned char *inFileData = (unsigned char*)calloc(sizeof(unsigned char) * width * height * ch, 1); //3ch分の画素値配列を確保する unsigned char *outFileData = (unsigned char*)calloc(sizeof(unsigned char) * width * height * ch, 1); //3ch分の画素値配列を確保する JSAMPROW tmpR = NULL; //1行分のメモリ if ((tmpR = (JSAMPROW)calloc(sizeof(JSAMPLE) * width * ch, 1) )== NULL) { return RET_FALSE; } JSAMPROW assign; for (int h = 0; h < height; h++) { jpeg_read_scanlines(&inInfo, &tmpR, 1); //1行分の画素値を代入 assign = tmpR; for (int w = 0; w < width; w++) { for (int c = 0; c < ch; c++) { inFileData[(h * width + w)* ch + c] = *assign++; //確保した配列に1行ずつ代入していく } } } // (6)構造体の破棄、メモリ解放 jpeg_finish_decompress(&inInfo); //展開処理を終了 jpeg_destroy_decompress(&inInfo); //JPEG構造体の破棄 free(tmpR); fclose(inFile); //------フィルタ関数実行------// //インスタンス生成 Filter filter(inFileData, outFileData, width, height, ch); filter.FilteringImage(type); //------JPEG書き込み------// // (1)画像データを扱う構造体を確保 struct jpeg_compress_struct outInfo; //画像データ構造体を定義 jpeg_create_compress(&outInfo); //構造体初期化処理 outInfo.err = jpeg_std_error(&jErr); //デフォルト値で設定 // (2)JPEGファイルをオープン FILE *outFile; error = fopen_s(&outFile, outFileName, "wb"); //ファイル生成 if (error != 0) { return RET_FALSE; } jpeg_stdio_dest(&outInfo, outFile); //ファイルポインタとoutInfoを渡す // (3)ヘッダ書き込み outInfo.image_width = width; //幅 outInfo.image_height = height; //高さ outInfo.input_components = ch; //チャネル数 outInfo.in_color_space = JCS_RGB; //色空間 jpeg_set_defaults(&outInfo); //その他のパラメータ設定 // (4)展開処理を開始 jpeg_start_compress(&outInfo, TRUE); // (5)画素値を書き込み JSAMPARRAY tmpO = (JSAMPARRAY)malloc(sizeof(JSAMPROW) * height); //1画像分のメモリ for (int h = 0; h < height; h++) { if ((tmpO[h] = (JSAMPROW)calloc(sizeof(JSAMPLE), width * ch)) == NULL) { return RET_FALSE; } } for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { for (int c = 0; c < ch; c++) { //画素値を変調(XXX足して255を超えるようなら255にクリップする //unsigned char s = 100; //unsigned char t = (inFileData[(h * width + w)* ch + c]); //if ((int)(s)+(int)(t) > 255) { // t = 255; //} //else { // t = (unsigned char)((int)(s)+(int)(t)); //} unsigned t = outFileData[(h * width + w) * ch + c]; tmpO[h][w * ch + c] = t; } } } jpeg_write_scanlines(&outInfo, tmpO, height); //書き込み // (6)構造体の破棄、メモリ解放 jpeg_finish_compress(&outInfo); //展開処理を終了 jpeg_destroy_compress(&outInfo); //JPEG構造体の破棄 fclose(outFile); free(tmpO); free(inFileData); free(outFileData); return RET_TRUE; }
今回、変えているのはargv[3]、outFileDataの定義部、
そしてインスタンス生成してメソッドを呼ぶ所、
//------フィルタ関数実行------// //インスタンス生成 Filter filter(inFileData, outFileData, width, height, ch); filter.FilteringImage(type);
それと、JPEG画像生成時に画素値代入する値を変えています。
for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { for (int c = 0; c < ch; c++) { //画素値を変調(XXX足して255を超えるようなら255にクリップする //unsigned char s = 100; //unsigned char t = (inFileData[(h * width + w)* ch + c]); //if ((int)(s)+(int)(t) > 255) { // t = 255; //} //else { // t = (unsigned char)((int)(s)+(int)(t)); //} unsigned t = outFileData[(h * width + w) * ch + c]; tmpO[h][w * ch + c] = t; } } } jpeg_write_scanlines(&outInfo, tmpO, height); //書き込み
コメントアウトされている箇所が、前回記事で書いていた内容です。
つまり今回は、outFileDataに画素値を入れるメソッドを呼び、その結果を新しく作成するJPEG画像領域に格納にしています。
では、メソッドでは何をしているか、
Filter.hから紹介します。
#pragma once #include <stdio.h> #define RET_TRUE 0 #define RET_FALSE -1 class Filter { private: unsigned char* inFile; unsigned char* outFile; int width, height, ch, type; public: //コンストラクタ Filter(unsigned char* inFile, unsigned char* outFile, int width, int height, int ch); //フィルタ実行 int FilteringImage(int type); //デストラクタ ~Filter(); };
次に、Filter.cpp。
#include "Filter.h" enum { LeftTop, CenterTop, RightTop, LeftMiddle, CenterMiddle, RightMiddle, LeftBottom, CenterBottom, RightBottom, }; Filter::Filter(unsigned char* inFile, unsigned char* outFile, int width, int height, int ch) { this->inFile = inFile; this->outFile = outFile; this->width = width; this->height = height; this->ch = ch; } int Filter::FilteringImage(int type) { if (type < 0 && type > 2) { return RET_FALSE; } int gyoretsu[3][9] = {{1, 0, -1, //縦方向ソーベルフィルタ 2, 0, -2, 1, 0, -1}, { 1, 2, 1, //横方向ソーベルフィルタ 0, 0, 0, -1, -2, -1}, { 0, -1, 0, //シャープネスフィルタ -1, 5, -1, 0, -1, 0} }; for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { for (int c = 0; c < ch; c++) { if ((w < 1) || (w > width - 2) || (h < 1) || (h > height -2)) { //wは横端またはhが縦端であるときは何もしない outFile[(h * width + w) * ch + c] = inFile[(h * width + w) * ch + c]; } else { int tmp = (int)inFile[((h - 1) * width + (w - 1)) * ch + c] * gyoretsu[type][LeftTop] + (int)inFile[((h - 1) * width + (w - 0)) * ch + c] * gyoretsu[type][CenterTop] + (int)inFile[((h - 1) * width + (w + 1)) * ch + c] * gyoretsu[type][RightTop] + (int)inFile[((h - 0) * width + (w - 1)) * ch + c] * gyoretsu[type][LeftMiddle] + (int)inFile[((h - 0) * width + (w - 0)) * ch + c] * gyoretsu[type][CenterMiddle] + (int)inFile[((h - 0) * width + (w + 1)) * ch + c] * gyoretsu[type][RightMiddle] + (int)inFile[((h + 1) * width + (w - 1)) * ch + c] * gyoretsu[type][LeftBottom] + (int)inFile[((h + 1) * width + (w - 0)) * ch + c] * gyoretsu[type][CenterBottom] + (int)inFile[((h + 1) * width + (w + 1)) * ch + c] * gyoretsu[type][RightBottom]; //フィルタ処理 if (tmp > 255) tmp = 255; else if (tmp < 0) tmp = 0; outFile[(h * width + w) * ch + c] = (unsigned char)tmp; } } } } return RET_TRUE; } Filter::~Filter() { }
■要点
今回は、FilteringImageメソッドの中で示されている通り、
3種類のフィルタを上位からの指示に従って適用しています。
0:縦方向のソーベルフィルタ
1:横方向のソーベルフィルタ
2:シャープネスフィルタ
呼び分けはコーリング側の引数に与えています(argv[3])。
実行結果は以下のようになります。
少しシャープネスが分かりにくいかもしれません…。