フィルタクラスをGitHubに公開する
■前回記事からの続き
JEPG画像の入出力に関する記事を書いた後、
mame-mame.hatenadiary.com
JPEG画像に対してフィルタ処理を実行する記事を書きました。
フィルタ処理用にフィルタクラスを定義しました。
mame-mame.hatenadiary.com
そして、フィルタクラスをライブラリ化して利用するように編集しました。
mame-mame.hatenadiary.com
①JPEG画像を取り扱うためのライブラリの取得
②JPEG画像を取得して画素値を抽出する
★画素値にフィルタをかける
③JPEG画像を新たに保存する
①⇒②⇒★⇒③
という処理の流れをC/C++で記述していました。
★を前回、ライブラリ化しました。
ライブラリ化する事で他のプロジェクトにも利用できます。
今回のテーマは、
フィルタクラスをGitHubに公開する
です。
■前置き:GitHubとは
Gitというリポジトリ管理システムのソフトウェアがあります。
Git上でソースコードを管理する事で、ソースコードの修正履歴を記録できます。
特に効果的な事は集団での編集作業です。
・誰かが編集中に別の誰かがファイル内容を編集してしまった。
・誰かが誤ってファイルを消してしまった。
・編集した内容に間違いがある事が分かり、前の状態に戻さなくて原ならない。
こういったケースにレポジトリ管理システムは効果を発揮します。
そしてGitHubはウェブ上で、そうした管理をしていく仕組みです。
決まった複数人とファイル編集をしても良し、
或いは全く知らない一般の方までコードを公開して編集権限を持たせるも良しです。
昨今のソフト開発はOSS利用が盛んです。
「車輪の再発明はしない」という言葉の通り、誰かが開発済みの物は完成物を利用してしまえば良いのです。
ソフト開発はこうしたOSSや協調の文化により加速度的に進化してきています。
今回は出来上がった(※稚拙な)ライブラリをGitHubに公開します。
ライブラリにする事で自身の他プロジェクトだけでなく、他人のプロジェクトでも転用可能となったからです。
■GitHubの公開方法
まずGitHubのアカウントを登録しましょう。
以下のサイトで新規アカウントを作成します。
github.com
次にGitをローカル上で管理/アップロードするための、
GitHub Desktopをダウンロードします。
desktop.github.com
GitHub Desktopで新しいレポジトリを作成します。
LocalPathで指定した場所にフォルダが作られます。
今回は、前回記事で作ったFilterLIBのコードなどをフォルダにコピーしていきます。
VS環境ですぐにビルドできるようにコードだけでなくビルド環境もコピーします。
今回、コピーしたのは以下ファイルです。
・FilterLIB.sln
・FilterLIB.vcxproj
・FilterLIB.vcxproj.filters
・Filter.h
・Filter.cpp
・stdafx.h
・stdafx.cpp
反対に、コピーしなかったのは以下ファイルです。
これらはビルド時に作成されるためアップの必要がないと判断しました。
・FilterLIB.vcxproj.user
・targetver.h
・Debugフォルダ
・Releaseフォルダ
フォルダにコピーすると、
GitHub DesktopのGUI表示にも同期されます。
ではこれらをGitHubにアップします。
まず、GitHub DesktopのGUI上でコミットを実施します。
SummaryとDescriptionに何等かの記載をしなければ、
Commit to masterが押下できない仕組みになっています。
「複数人で履歴管理する場合にこうした記述を実施するようにしましょう」
というメッセージなのでしょう。
次にCommitした内容をGitHubと共有できる形にします。
GitHub DesktopのGUIに「Fetch origin」ボタンがあります。
これを押下する事でGitHubへのアップロードが実行できます。
GitHub上で確認してみましょう。
作られているはずです。
ちなみに、この状態は上の写真の通り、「Private」設定となっています。
これを広く人に公開したい場合は
Repositories(今回で言えばFilter)をクリックし、
選択タブの「Setting」を選択。
Setting画面をずっと下スクロールしたら以下の選択肢があります。
Make publicを選択しましょう。
これで万事完了、どんどん出来上がったプロジェクトはアップしましょう。
■おわりに
今回は作ったライブラリをGitHubで外部公開しました。
上で万事完了と言いましたが、外部に公開するにはもっと多くの気にかけなければならない事があるはずです。
例えば、ReadMeで「このレポジトリの目的や使用方法、転載の可否」などを記載する事はした方が良いです。
これらはマナーに関わる項目だと思います。
人に使ってもらいたい内容はより良く整備をしましょう。
私自身、やり方が分からない部分が多いので、色々調べてやってみます。
フィルタクラスをスタティックライブラリ化する
■前回記事からの続き
JEPG画像の入出力に関する記事を書いた後、
mame-mame.hatenadiary.com
JPEG画像に対してフィルタ処理を実行する記事を書きました。
フィルタ処理用にフィルタクラスを定義しました。
mame-mame.hatenadiary.com
①JPEG画像を取り扱うためのライブラリの取得
②JPEG画像を取得して画素値を抽出する
★画素値にフィルタをかける
③JPEG画像を新たに保存する
①⇒②⇒★⇒③
という処理の流れをC/C++で記述していました。
★は何でも変えられるのでお好きな画像処理が出来ますよ~、という説明をしました。
①②★③の実施命令を出しているのはmain.cppです。
しかし①②③の動作を担っているのはjpeg.libというスタティックライブラリです。
今回は
★も自身でスタティックライブラリにしてしまえ
という記事になります。
■前置き:スタティック(静的)ライブラリについて
スタティックライブラリはWin32向けには「.lib拡張子」で扱われるファイルです。
(Win64bit版やArm系では.a拡張子だったりします)
「.c/.cpp」で記述される実装部をひとまとめにしたものになります。
クラスは宣言と実装でファイルが分かれていますが、.libファイルには宣言が含まれません。
なので、ライブラリの利用者側は.libファイルとヘッダファイルも併せて参照することになります。
利用者が上記2点を参照して、実行用のアプリケーションをビルドした場合は、ビルド結果の実行ファイル(.exe)が生成されます。
ライブラリファイルは実行ファイルに一体となり取り込まれます。
そしてアプリ実行時はライブラリのすべてのコードがメモリに展開されます。
スタティックライブラリのメリットは、”一体”である事です。
実際のアプリ実行時に.exeファイルのみを扱えば良いので管理が楽です。
実行時にすべてのコードがメモリ展開されるため、後々の関数実行時にメモリ展開が要らない分が高速です。
一方でデメリットは一体である事からファイル容量が大きくなりやすく、起動時の展開時間が掛かります。
メモリ使用量も、次に述べる「ダイナミックライブラリ」より余分に消費される事になります。
スタティックの反対でダイナミック(動的)があります。
ダイナミックライブラリは「.dll拡張子」で扱われます。
こちらは反対に、利用者が実行用のアプリケーションでビルドしても別体です。
こちらを選択するメリットは、”別体”である事です。
ライブラリを差し替えても実行用のアプリケーションをビルドし直さなくて良いです。
別体であるためアプリ実行時はライブラリの関数が必要になった時だけメモリに展開されます、従ってメモリ効率が良いです。
利用者にとっては、ライブラリの独立性が高いため扱いやすいでしょう。
一方でデメリットは別体である事からメモリのロードに時間がかかる点です。
■スタティックライブラリの作り方
実行環境はVSを想定します。
VSで
「ファイル」→「新規作成」→「プロジェクト」を選択します。
「スタティックライブラリ」を選択して、お好きなプロジェクト名を指定します。
OKを押すと、ソリューションエクスプローラがこんな画面になります。
(今回、FilterLIBというプロジェクト名を付けました)
ソースファイルに「Filter.cpp」を追加します。
ヘッダーファイルに「Filter.h」を追加します。
それぞれ、前回記事で作ったファイルと同様です。
1点の変更点として、「Filter.cpp」の冒頭に「#include "stdafx.h"」と記述します。
※ソースコードに記述しない方法として、
「プロジェクトのプロパティ」→「構成プロパティ」→「C/C++」
→「詳細設定」→「必ず使用されるインクルードファイル」
の欄内に stdafx.h を追加する方法でも良いです。
stdafx.hファイルはプリコンパイル済みのヘッダーファイルをincludeしたヘッダーファイルです。
プリコンパイル済みヘッダーファイルはプロジェクトのビルド時間短縮化が目的です。
プロジェクトが十分に小規模である場合は、その恩恵は感じられません。
多くの.cppファイルを扱う際に、それぞれがiostreamやstring、vectorや、listなどのヘッダーファイルをインクルードします。
またそれらのインクルード先でも他のヘッダーファイルをインクルードしています。
これらをコンパイルする際に、プリプロセッサが何度も同じファイルを読み込む事は時間がかかるという訳です。
今回は、プロジェクト作成時に「空のプロジェクト」でなくライブラリ作成用の選択をしたため、プロジェクト内にstdafx.h/.cppが自動挿入されていたのです。
さて、以上の方法でFilterLIBプロジェクトをビルドすると、.libファイルが作成されます。
今度は利用者側がこれを参照します。
新しいプロジェクトを作成し、main.cppに以下を記述します。
#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; }
前回記事と違いはありません。
(main.cppと同一直下にFilter.hがある想定です)
プロジェクトの設定で、プロパティページを開き、
「プロジェクトのプロパティ」→「構成プロパティ」→「リンカー」
→「入力」→「追加の依存ファイル」
に、FilterLIB.libを追記します。
これでビルド&実行できるはずです。
■おわりに
今回は、スタティックライブラリの作り方をまとめました。
非常に簡単です。
これで★がライブラリ化されました。
①⇒②⇒★⇒③となるコーリングアプリだけでなく、
別のプロジェクトでも★がパーツとして再利用できるようになります。
※参考記事
tooljp.com
qiita.com
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])。
実行結果は以下のようになります。
少しシャープネスが分かりにくいかもしれません…。
JPEG画像の入出力
■ソフトのお勉強
外出自粛期間であるため、お勉強をしています。
私、情報系でありながらソフトウェア開発が不得意です。
このコンプレックス解消のため良い機会として勉強し直します。
今回選んだテーマは、
「JPEG画像の入出力」
です。
JPEGの扱いに関しては、
①JPEG画像を取り扱うためのライブラリの取得
②JPEG画像を取得して画素値を抽出する方法
③JPEG画像を新たに保存する方法
が学べれば応用が利くと思います。
例えば、画像に何らかのフィルタをかけたい、というのであれば。
①⇒②⇒「画素値にフィルタをかける」⇒③とやれば良いのです。
「画素値にフィルタをかける」をモジュール化すると汎用性も高くなります。
この記事では、①②③について扱います。
開発環境はVS2017で、OSはWindows10です。
開発言語はC++を想定しています。
■JPEGライブラリの取得
JPEGライブラリは様々ありますが、今回は「libjpeg」を使います。
(その名の通りですね)
libjpegはオープンソースとなります。
(オープンソースですので、これを使ったアプリケーションなど再配布をする場合は取り扱いにご注意下さい。)
今回はVS2017を使用して、libjpegをビルドします。
以下のHPを参考にすれば万事うまくいきました。
hnakai0909.hatenablog.com
ここでは、jpegsr9b.zipを展開されていました。
私が実施した時は9dバージョンとなっておりました。
参考サイトと同様に、renを使いVS用のファイルを生成していきます。
jpeg.slnとapps.slnの2つのプロジェクトファイルが作られます。
(生成する、作ると言っていますが、実態は名前変更しただけです)
jpeg.slnをビルドすると、jpeg.libが生成されます。
②③はこのjpeg.libと、各種ヘッダファイルを使います。
※プラットフォームツールセットやWindowsSDKバージョンがお使いの環境からズレている場合があります、自身の環境に合わせてビルドしましょう。
■JPEGファイルの入出力
手っ取り早くサンプルコードを載せます。
JPEG画像の読み込みと、JPEG画像の生成を記述しています。
JPEG画像生成時は、JPEG画像の読み込み時に取得した画素値を変調しています。
(変調が非常にテキトーですみません…)
#include <stdio.h> #include <cstdlib> #include <cstring> #include "jpeglib.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]; //出力ファイル名 //------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分の画素値配列を確保する 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); //------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)); } 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); return RET_TRUE; }
画素値の変更は非常にシンプルです。
sという変数に与えた数だけ画素値にプラスします。
255を上回るようであれば、255に丸めます。
これを実行すると、JPEG画像は入力/出力で以下のように変わります。当然ですが、画像が白っちゃけます。
フィルタをかけたい場合は、画素ごとに異なる値を足すなり掛ければ良いです。
画素値が取れたのでいかようにも編集は出来ます。