mameブログ

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

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])。
実行結果は以下のようになります。

f:id:mizzzo:20200409120618j:plain
before

f:id:mizzzo:20200409152015j:plain
after(0:縦方向ソーベルフィルタ)

f:id:mizzzo:20200409152047j:plain
after(1:横方向ソーベルフィルタ)

f:id:mizzzo:20200409152123j:plain
after(2:シャープネスフィルタ)

少しシャープネスが分かりにくいかもしれません…。

■おわりに

今回はフィルタ処理を行うクラスを定義し、
JPEG画像で読み込んだファイルの画素値をメソッドに渡して処理しました。
処理結果を使って新規にJPEG画像を保存しました。

独習C++ 第4版

独習C++ 第4版