본문 바로가기
College Computer Science/Graphic Programming Design

[그래픽스] 나만의 필터 만들기

by 2den 2022. 2. 8.
728x90

레트로 감성, 노이즈 필터를 만들어보았습니다.

 

 

먼저, 필터의 컨셉입니다.

 

 

최근 레트로, 복고 등의 열풍이 불면서 인기를 얻은 추억의 물건 중 하나는 바로 필름카메라인데요. 필름카메라의 색감과 빛을 담는 방식이 인기를 얻자, 사람들은 스마트폰과 컴퓨터를 이용해 '마치 필카로 찍은듯한' 보정을 하기 시작했습니다. 각종 필터 어플에서도 레트로 테마가 유행입니다. 레트로 테마 필터의 특징 중 가장 두드러지고, 또 제가 개인적으로 좋아하는 부분은 바로 노이즈, 잡음입니다. 지금 보여드리는 두 사진은 팬들이 레트로 감성으로 보정한 연예인들의 사진입니다.

 

 

사진을 자세히 보시면 먼지와 같은 필터가 껴있음을 알 수 있습니다. 이 노이즈는 옛날 카메라의 화질저하 현상을 인위적으로 구현했다고 생각하시면 됩니다. 이번 수업을 들으며 노이즈를 끼얹어 주는 필터를 만들면 재밌겠다고 생각했습니다.

 

 

기존 이미지의 픽셀의 크기를 조작하거나, 가까운 픽셀과의 색상을 조절하는 방법 등 노이즈를 생성하는 방법은 여러가지가 있습니다. 하지만 제가 이번에 사용한 방법은 랜덤한 색상을 이미지의 랜덤한 위치에 더해주는 방식입니다.

해당 과정에서 가우시안 노이즈를 사용했는데요, 가우시안 노이즈는 정규분포를 갖는 노이즈입니다.

 

 

가우시안 정규분포라고도 불리는 표준정규분포 안에서의 랜덤한 값을 도출하기 위해 통계학에서 사용되는 박스멀러 트랜스폼을 사용하였습니다. 0과 1 사이의 값을 보이는 식에 대입하면 랜덤한 표준정규분포 값을 구할 수 있습니다.

 

 

코드에서 더 자세히 설명 드리겠습니다.

 

 

우선 myfilter함수의 파라미터는 다음과 같습니다. 다른 필터 함수들이랑 거의 같습니다. ratio도 받아서 조절가능 하게끔 구현했습니다.

 

 

변수는 다음과 같습니다. 다른 함수에서 사용하지 않았던 변수는 size입니다. 가로세로 크기를 곱한 값입니다.
r1, r2는 랜덤한 위치를 선정하기 위한 랜덤 변수이고, u1, u2는 아까 보여드렸던 표준정규분포를 계산하기 위한 0과 1사이의 값입니다. snd는 표준정규분포를 갖도록 계산된 랜덤 변수이고 픽셀에 더해줄 노이즈 값을 갖습니다. nd는 snd에서 계산된 랜덤 값을 ratio 비율만큼 조작한 값입니다.

 

 

반복하여 랜덤값을 생성해야 해서 현재 시간을 seed로 갖도록 설정했습니다.

 

 

이제 핵심 코드인데요, u1,u2는 말씀드렸다시피 표준정규분포를 계산하기 위한 0과 1사이의 값입니다.

 

 

아래의 식에 u1 u2와 동일합니다. 원래 box-muller 변환식은 z0와 z1를 cos함수와 sin 함수를 사용하여 각각 독립적으로 구해 쌍으로 사용하지만, 저는 노이즈로 추가할 랜덤값 하나만 필요해서 z0식만 그대로 구현했습니다.

 

 

비율을 곱해서 ratio 값에 따라 노이즈 정도를 조절할 수 있게 했습니다.

 

 

r1, r2는 랜덤 위치를 선정하기 위한 변수입니다. r1은 가로길이로 나눈 나머지로 설정하여 0과 가로길이 사이값을
가지게 해주고, r2는 세로길이로 나눈 나머지로 설정하여 0과 세로길이 사이값을 가지게 하였습니다.

 

 

랜덤한 위치 값 두개를 가지고 이것과 같이 location 변수를 지정합니다. 원래 다른 함수에서는 x, y가 0과 가로길이, 0과 세로길이 사이에서 점점 커지며 갖던 위치값을 r1과 r2로 같은 범위에서 랜덤하게 지정되는 것 뿐입니다.

 

 

r1, r2를 나머지값으로 설정했기 때문에 이 if문이 실행될 일은 없지만, 혹시라도 이미지보다 위치값이 커진
경우 사이즈를 재설정하여 런타임 에러가 나지 않도록 해줍니다.

 

 

그다음엔 아까 위에서 비율을 곱해 지정한 노이즈 랜덤값 nd를

 

 

랜덤한 위치를 가진 loc의 i 채널 픽셀에 더해줍니다. 그 다음은 다른 함수에서처럼 255와 0을 넘어가지 않도록 설정하면 됩니다.

 

 

이 알고리즘을 반복해야하는데요, 이미지의 크기만큼 이미지의 랜덤한 점 개수를 골라 랜덤한 노이즈값을 더해주도록 설정해보았습니다. 반복을 더 많이하면 자글자글한 노이즈가 생기고, 반복을 덜 하면 더 듬성듬성 비어있는 노이즈가 생길 것입니다. x, y값은 사용하지 않고, 반복을 위해서만 정의된 변수입니다.

 

 

이 반복을 RGB 모두 해주기 위해 i값을 bytesperpixel만큼 더하며 반복해줍니다. 여기까지, 이 과정을 간단히 그림으로 설명하자면 다음과 같습니다.

 

 

첫번째 반복에서 이미지의 랜덤한 위치에 랜덤한 파란색값이 더해집니다. 실제로는 그림처럼 같은 파란색이 아니라 진한파랑과 옅은 파랑이 랜덤하게 있는 것이죠.

 

 

두번째 반복에서 가운데와 같이 랜덤한 위치에 랜덤한 초록색이 더해집니다. 이번에도 물론 초록의 진하기가 다를 것이고, 그림과는 다르게 파란색이 더해진 곳에 초록색이 더해질 수도 있습니다.

 

 

세번째 반복에서는 마지막과 같이 랜덤한 위치에 랜덤한 빨간색이 더해집니다.
이런 랜덤한 빨파초의 랜덤하게 위치된 랜덤하게 믹스된 필터가 기존 그림에 더해지는 것입니다.

 

 

// image.cpp
#pragma once

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <fstream>
#include <cmath> 
#include <cstdio> 
#include <cstdlib>

#define DATA_OFFSET_OFFSET 0x000A
#define WIDTH_OFFSET 0x0012
#define HEIGHT_OFFSET 0x0016
#define BITS_PER_PIXEL_OFFSET 0x001C
#define HEADER_SIZE 14
#define INFO_HEADER_SIZE 40
#define NO_COMPRESION 0
#define MAX_NUMBER_OF_COLORS 0
#define ALL_COLORS_REQUIRED 0

typedef struct {
	float r;       // 0 - 255 
	float g;       // 0 - 255
	float b;       // 0 - 255 
}rgb;

typedef struct {
	float h;       // 0.0 - 360.0 angle in degrees
	float s;       // 0.0 -1.0 percent
	float b;       // 0.0 -1.0 percent
}hsb;

typedef struct {
	float y;
	float cb;
	float cr;
}ycbcr;

hsb  rgb2hsb(rgb in);
rgb  hsb2rgb(hsb in);
ycbcr rgb2ycbcr(rgb in);
rgb ycbcr2rgb(ycbcr in);

void ReadImage(const char* fileName, unsigned char** pixels, int* width, int* height, int* bytesPerPixel);
void WriteImage(const char* fileName, unsigned char* pixels, int width, int height, int bytesPerPixel);

// My filter
unsigned char* myfilter(unsigned char** pixelsptr, int width, int height, int bytesPerPixel, int ratio);
//bmp.cpp
#include "image.h"

//***Inputs*****
//fileName: The name of the file to open 
//***Outputs****
//pixels: A pointer to a byte array. This will contain the pixel data
//width: An int pointer to store the width of the image in pixels
//height: An int pointer to store the height of the image in pixels
//bytesPerPixel: An int pointer to store the number of bytes per pixel that are used in the image

void ReadImage(const char* fileName, unsigned char** pixels, int* width, int* height, int* bytesPerPixel)
{
	//Open the file for reading in binary mode
	FILE* imageFile = fopen(fileName, "rb");

	//Read data offset
	int dataOffset;
	fseek(imageFile, DATA_OFFSET_OFFSET, SEEK_SET);
	fread(&dataOffset, 4, 1, imageFile);
	//Read width
	fseek(imageFile, WIDTH_OFFSET, SEEK_SET);
	fread(width, 4, 1, imageFile);
	//Read height
	fseek(imageFile, HEIGHT_OFFSET, SEEK_SET);
	fread(height, 4, 1, imageFile);
	//Read bits per pixel
	short bitsPerPixel;
	fseek(imageFile, BITS_PER_PIXEL_OFFSET, SEEK_SET);
	fread(&bitsPerPixel, 2, 1, imageFile);
	//Allocate a pixel array
	*bytesPerPixel = ((int)bitsPerPixel) / 8;

	//Rows are stored bottom-up
	//Each row is padded to be a multiple of 4 bytes. 
	//We calculate the padded row size in bytes
	int paddedRowSize = (int)(4 * ceil((float)(*width) / 4.0f)) * (*bytesPerPixel);
	//We are not interested in the padded bytes, so we allocate memory just for
	//the pixel data
	int unpaddedRowSize = (*width) * (*bytesPerPixel);
	//Total size of the pixel data in bytes
	int totalSize = unpaddedRowSize * (*height);
	*pixels = (unsigned char*)malloc(totalSize);
	//Read the pixel data Row by Row.
	//Data is padded and stored bottom-up
	int i = 0;
	//point to the last row of our pixel array (unpadded)
	unsigned char* currentRowPointer = *pixels + ((*height - 1) * unpaddedRowSize);
	for (i = 0; i < *height; i++)
	{
		//put file cursor in the next row from top to bottom
		fseek(imageFile, dataOffset + (i * paddedRowSize), SEEK_SET);
		//read only unpaddedRowSize bytes (we can ignore the padding bytes)
		fread(currentRowPointer, 1, unpaddedRowSize, imageFile);
		//point to the next row (from bottom to top)
		currentRowPointer -= unpaddedRowSize;
	}

	fclose(imageFile);
}

//***Inputs*****
//fileName: The name of the file to save 
//pixels: Pointer to the pixel data array
//width: The width of the image in pixels
//height: The height of the image in pixels
//bytesPerPixel: The number of bytes per pixel that are used in the image
void WriteImage(const char* fileName, unsigned char* pixels, int width, int height, int bytesPerPixel)
{
	//Open file in binary mode
	FILE* outputFile = fopen(fileName, "wb");
	//*****HEADER************//
	//write signature
	const char* BM = "BM";
	fwrite(&BM[0], 1, 1, outputFile);
	fwrite(&BM[1], 1, 1, outputFile);
	//Write file size considering padded bytes
	int paddedRowSize = (int)(4 * ceil((float)width / 4.0f)) * bytesPerPixel;
	int fileSize = paddedRowSize * height + HEADER_SIZE + INFO_HEADER_SIZE;
	fwrite(&fileSize, 4, 1, outputFile);
	//Write reserved
	int reserved = 0x0000;
	fwrite(&reserved, 4, 1, outputFile);
	//Write data offset
	int dataOffset = HEADER_SIZE + INFO_HEADER_SIZE;
	fwrite(&dataOffset, 4, 1, outputFile);

	//*******INFO*HEADER******//
	//Write size
	int infoHeaderSize = INFO_HEADER_SIZE;
	fwrite(&infoHeaderSize, 4, 1, outputFile);
	//Write width and height
	fwrite(&width, 4, 1, outputFile);
	fwrite(&height, 4, 1, outputFile);
	//Write planes
	short planes = 1; //always 1
	fwrite(&planes, 2, 1, outputFile);
	//write bits per pixel
	short bitsPerPixel = bytesPerPixel * 8;
	fwrite(&bitsPerPixel, 2, 1, outputFile);
	//write compression
	int compression = NO_COMPRESION;
	fwrite(&compression, 4, 1, outputFile);
	//write image size (in bytes)
	int imageSize = width * height * bytesPerPixel;
	fwrite(&imageSize, 4, 1, outputFile);
	//write resolution (in pixels per meter)
	int resolutionX = 11811; //300 dpi
	int resolutionY = 11811; //300 dpi
	fwrite(&resolutionX, 4, 1, outputFile);
	fwrite(&resolutionY, 4, 1, outputFile);
	//write colors used 
	int colorsUsed = MAX_NUMBER_OF_COLORS;
	fwrite(&colorsUsed, 4, 1, outputFile);
	//Write important colors
	int importantColors = ALL_COLORS_REQUIRED;
	fwrite(&importantColors, 4, 1, outputFile);
	//write data
	int i = 0;
	int unpaddedRowSize = width * bytesPerPixel;
	for (i = 0; i < height; i++)
	{
		//start writing from the beginning of last row in the pixel array
		int pixelOffset = ((height - i) - 1) * unpaddedRowSize;
		fwrite(&pixels[pixelOffset], 1, paddedRowSize, outputFile);
	}
	fclose(outputFile);
}
//main.cpp
#include "image.h"

int main()
{
	unsigned char* pixels;  // 동적배열
	int width;
	int height;
	int bytesPerPixel;
	ReadImage("v.bmp", &pixels, &width, &height, &bytesPerPixel);


	unsigned char* res = myfilter(&pixels, width, height, bytesPerPixel, 30);
	WriteImage("vmyfilter.bmp", res, width, height, bytesPerPixel);

	free(pixels);
	return 0;
}
//myfilter.cpp
#include "image.h"
#include <time.h>

// My filter
unsigned char* myfilter(unsigned char** pixelsptr, int width, int height, int bytesPerPixel, int ratio)
{
	int x, y; // kind of pointer
	int i, loc; // channel, location
	int size = width * height; // image size per channel
	unsigned char* pixels;

	int r1, r2;
	double u1, u2, nd, snd, temp; // nd : normal distribution, snd : standard normal distribution
	time_t nowTime;
	srand(time(&nowTime));

	pixels = *pixelsptr;

	for (i = 0; i < bytesPerPixel; i++) {
		for (y = 0; y < height; y++) {
			for (x = 0; x < width; x++) {

				u1 = (double)rand() / RAND_MAX; // 0 ~ 1
				u2 = (double)rand() / RAND_MAX; // 0 ~ 1

				snd = sqrt(-2.0 * log(u1)) * cos(2 * 3.14159 * u2); // box-muller transform

				nd = ratio * snd;

				r1 = rand() % width;
				r2 = rand() % height;

				loc = bytesPerPixel * (r2 * width + r1); // bytesPerpixel * (y * width + x)
				if (loc >= size * bytesPerPixel) {
					loc = (size - 1) * bytesPerPixel;
				}

				int l1 = (loc / bytesPerPixel) / width;
				int l2 = (loc / bytesPerPixel) % width;

				temp = pixels[loc + i] + nd;

				temp = (temp > 255) ? 255 : (temp < 0) ? 0 : temp;
				pixels[loc + i] = temp;
			}
		}
	}

	return pixels;
}

수업시간에 배웠던 blur함수로 이미지를 약간 뿌옇게 만들어주고, 밝기를 살짝 낮춰준 뒤 제가 만든 노이즈 필터를 사용하면

 

 

다음과 같이 정말 옛날 성능이 좋지않은 카메라로 찍은 듯한 편집이 가능합니다.

 

 

아까 배경으로 사용도 하고 방금 시물레이션도 했던 sails 사진입니다.

 

 

마찬가지로 몇가지 단계를 더 거치면 보다 더 레트로 감성을 입힐수 있습니다.

 

 

원본과 비교해보세요.

 

 

728x90

댓글