ビットマップファイルをSobel filterによるエッジ検出（C言語）

表題の通り、ビットマップファイルのエッジ検出を行うプログラムを書いてみた。

# ソースコード
```c:detect_edge.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>
#include "bitmap.h"
#include "grayscale.h"

// detecting by Sobel Filter
void apply_sobel_filter(unsigned char* grayData, unsigned char* edgeData, int width, int height) {
	int gx, gy;
	int edge_strength;
	int x, y;
	for (y = 1; y < height - 1; y++) {
	for (x = 1; x < width - 1; x++) {
		gx = -grayData[(y - 1) * width + (x - 1)] - 2 * grayData[y * width + (x - 1)] - grayData[(y + 1) * width + (x - 1)] +
		grayData[(y - 1) * width + (x + 1)] + 2 * grayData[y * width + (x + 1)] + grayData[(y + 1) * width + (x + 1)];

		gy = -grayData[(y - 1) * width + (x - 1)] - 2 * grayData[(y - 1) * width + x] - grayData[(y - 1) * width + (x + 1)] +
		grayData[(y + 1) * width + (x - 1)] + 2 * grayData[(y + 1) * width + x] + grayData[(y + 1) * width + (x + 1)];

		edge_strength = abs(gx) + abs(gy);
		edgeData[y * width + x] = (unsigned char)(edge_strength > 255 ? 255 : edge_strength);
		}
	}
}

void convert_to_grayscale_and_detect_edges(const char* input_filename, const char* output_filename) {
	FILE* input_file = fopen(input_filename, "rb");
	FILE* output_file = fopen(output_filename, "wb");
	BMPFileHeader fileHeader;
	BMPInfoHeader infoHeader;
	unsigned char pixelData[3];
	unsigned char* grayData = NULL;
	unsigned char* edgeData = NULL;
	unsigned char value;
	int x, y;

	if (!input_file || !output_file) {
		fprintf(stderr, "I could not open file!!\n");
		return;
	}

	// reading BMPHeader
	//fread(&fileHeader, sizeof(BMPFileHeader), 1, input_file);
	fread(&fileHeader.type, sizeof(fileHeader.type), 1, input_file);
	fread(&fileHeader.size, sizeof(fileHeader.size), 1, input_file);
	fread(&fileHeader.reserved1, sizeof(fileHeader.reserved1), 1, input_file);
	fread(&fileHeader.reserved2, sizeof(fileHeader.reserved2), 1, input_file);
	fread(&fileHeader.offset, sizeof(fileHeader.offset), 1, input_file);
	fread(&infoHeader, sizeof(BMPInfoHeader), 1, input_file);

	// malloc for pixel data
	grayData = (unsigned char*)malloc(infoHeader.width * infoHeader.height * sizeof(unsigned char));
	edgeData = (unsigned char*)malloc(infoHeader.width * infoHeader.height * sizeof(unsigned char));
	if (!grayData || !edgeData) {
		fprintf(stderr, "I could not allocate memory area!!\n");
		return;
	}

	// writing BMPHeader to new file
	//frite(&fileHeader, sizeof(BMPFileHeader), 1, output_file);
	fwrite(&fileHeader.type, sizeof(fileHeader.type), 1, output_file);
	fwrite(&fileHeader.size, sizeof(fileHeader.size), 1, output_file);
	fwrite(&fileHeader.reserved1, sizeof(fileHeader.reserved1), 1, output_file);
	fwrite(&fileHeader.reserved2, sizeof(fileHeader.reserved2), 1, output_file);
	fwrite(&fileHeader.offset, sizeof(fileHeader.offset), 1, output_file);
	fwrite(&infoHeader, sizeof(BMPInfoHeader), 1, output_file);

	// converting grayscale
	for (y = 0; y < infoHeader.height; y++) {
		for (x = 0; x < infoHeader.width; x++) {
			fread(&pixelData, sizeof(pixelData), 1, input_file);
			grayData[y * infoHeader.width + x] = rgb_to_gray(pixelData[2], pixelData[1], pixelData[0]);
		}
	}
	// deceting edge
	apply_sobel_filter(grayData, edgeData, infoHeader.width, infoHeader.height);

	// writing pixel date to new file
	for (y = 0; y < infoHeader.height; y++) {
		for (x = 0; x < infoHeader.width; x++) {
			value = edgeData[y * infoHeader.width + x];
			fwrite(&value, sizeof(unsigned char), 1, output_file);
			fwrite(&value, sizeof(unsigned char), 1, output_file);
			fwrite(&value, sizeof(unsigned char), 1, output_file);
		}
	}

	fclose(input_file);
	fclose(output_file);
	free(grayData);
	free(edgeData);

	printf("done.\n");
}

int main() {
	char input_filename[BUFSIZ];
	char output_filename[BUFSIZ];

	printf("input file?:");
	scanf("%s", input_filename);
	printf("output file?:");
	scanf("%s", output_filename);
	convert_to_grayscale_and_detect_edges(input_filename, output_filename);

	return 0;
}
```
```fread(&fileHeader, sizeof(BMPFileHeader), 1, input_file);```としなかったのは、構造体の各メンバのサイズが構造体全体のサイズと合わず、読み込んだヘッダ情報がおかしくなって正しく動作しなかったため。
パディングで合わせてコードの可搬性を落とすくらいならと、各メンバのアドレスを```fread```の第一引数とした。

```c:grayscale.c
#include "grayscale.h"
#include <stdio.h>

unsigned char rgb_to_gray(unsigned char r, unsigned char g, unsigned char b)
{
	return (unsigned char)(r + g + b) / 3;
}
```
これくらいなら別に分けなくてもよかった気がする。
grayscale.hは関数プロトタイプのみなので省略。

```c:bitmap.h
#ifndef BITMAP_H
#define BITMAP_H

// bitmap file header
typedef struct {
    uint16_t type;//BM
    uint32_t size;
    uint16_t reserved1;
    uint16_t reserved2;
    uint32_t offset;
} BMPFileHeader;

// bitmap info header
typedef struct {
    uint32_t size;
    int32_t width;
    int32_t height;
    uint16_t planes;
    uint16_t bitCount;
    uint32_t compression;
    uint32_t imageSize;
    int32_t xPixelsPerMeter;
    int32_t yPixelsPerMeter;
    uint32_t colorsUsed;
    uint32_t colorsImportant;
} BMPInfoHeader;

#endif
```
# 使い方と使用例
```console
[user@localhost detect_edge]$ ./filter 
Open input file?:sample.bmp
Open output file?:sample_detect.bmp
done.
```
（ビットマップといいつつ使ったビットマップ画像がうまくアップロード出来なかったので、PNGに変換してアップロードしています）
入力画像（Windows11付属のペイントで書いたもの）
![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807155/f10001e0-da7d-c97b-4806-bc33d2b5703d.png)

出力画像
![sample_detect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807155/cb185c06-2371-2102-25a2-29ebc7f0ae19.png)

# 他
当初はグレースケールに変換した部分もファイル出力したくて、変換→書き出し→書き出したファイルを読み込み→エッジ検出→ファイル書き出しという動作の流れだったが、コマンドラインでやるべきことじゃないと思い、メモリ動的確保で対応することにした。

## ノイズをのせた上でエッジ検出を行ってみる
思い付きで関数```convert_to_grayscale_and_detect_edges()```を改造し、指定した確率でランダムな値（```unsigned char```の範囲）のRGB値をピクセルデータとして書き込む関数を用意。

```convert_to_grayscale_and_detect_edges()```の```for```文ブロックを抜粋（他はほぼ同様のコードなので割愛）。

```c
	for (int y = 0; y < infoHeader.height; y++) {
		for (int x = 0; x < infoHeader.width; x++) {
			fread(&pixelData, sizeof(pixelData), 1, input_file);
			if((double)rand()/RAND_MAX <= prob){
				pixelData[2] = rand() % 256;
				pixelData[1] = rand() % 256;
				pixelData[0] = rand() % 256;
			}
			fwrite(&pixelData, sizeof(unsigned char), sizeof(pixelData), output_file);
		}
	}
```
（```prob```は```double```型の変数で、0から1の間をとる）

これに適当なビットマップファイルを通し、ノイズ発生確率を0.5%としてノイズを載せる。プログラムの動作コマンドは画像下。

変換前画像（Windows付属のペイント3Dで作成。ペイント3Dはじめて使った）
![paint3d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807155/826eca56-3b76-6a61-5271-43fd8b534002.png)

```console
[user@localhost detect_edge]$ ./noise 
input file?:paint3d.bmp
output file?:paint3d_noised.bmp
noise occurrence pribability?[min:max=0.0:1.0]:0.005
done.
[user@localhost detect_edge]$ ./detect_edge 
input file?:paint3d_noised.bmp
output file?:paint3d_noised_detect.bmp
done.
[user@localhost detect_edge]$ 
```

ノイズ入り画像
![paint3d_noised.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807155/213b09ee-c60c-8e09-925c-91c793d9a641.png)

これを前述の方法でエッジ検出する。

検出画像
![paint3d_noised_detect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807155/fe8cd22e-1a88-5349-2f1a-69ab8614d4f1.png)

ソーベルフィルタでエッジ検出を行う場合は、ガウシアンフィルタなどノイズ除去フィルタを通してからでないと案の定の結果になった。

## Makefile
```makefile
CC = gcc
CFLAGS = -Wall
SOURCE = detect_edge.c grayscale.c
OBJECT = $(SOURCE:%.c=%.o)
TARGET = detect_edge

.PHONY : clean

$(TARGET) : $(OBJECT)
	$(CC) -o $(TARGET) $(OBJECT)

$(OBJECT) : $(SOURCE)
	$(CC) $(CFLAGS) -c $(SOURCE)

noise : noise.c
	$(CC) $(CFLAGS) -o noise noise.c

clean : 
	rm -f detect_edge noise $(OBJECT)
 ```

 # おわりに
 せっかく（自分で載せたとはいえ）ノイズありの画像に対して特になにも施すことなくSobel filterを適用するということをしたのだから、比較対象としてなんらかのノイズ除去フィルタを通したパターンもやるべきだとは思う。けれども、大昔に大学の授業で習った「画像処理にはこんなフィルタがありますよ」程度の記憶しかないので、ガウシアンフィルタなどは今度また時間を見つけた折にでもやってみたい。

# 参考文献
1次微分（差分）によるエッジ検出

https://www.mis.med.akita-u.ac.jp/~kata/image/sobelprew.html