mameブログ

mameという男がやりたい事をやっている記録

フィルタクラスをスタティックライブラリ化する

■前回記事からの続き

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を押すと、ソリューションエクスプローラがこんな画面になります。
f:id:mizzzo:20200410211143p:plain
(今回、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

独習C++ 第4版

独習C++ 第4版