[DisplayImageInText] ASCII Art1: BitMap이미지를 콘솔창에 출력하기

이성훈·2022년 11월 9일
0

이 글을 https://www.youtube.com/watch?v=xB0ifokXdWs&t=3345s 이 유튜브를 보고 클론코딩한 내용을 정리한것입니다.

제작한 깃랩 >> https://gitlab.com/personal-study2/displayimageintext

  1. 전처리기
  • line6 : fopen사용할때 VisualStudio에서 오류가 발생하는것을 무시해준다.
  • line8 : FILE이나 Image등 사용하기위함


  1. 파일을 여는 부분

    읽기전용으로 파일을 불러와서, 그것을 가리키는 포인터를 선언한다.


  2. 헤더들을 저장하기위한 구조체 선언
    BitMap이미지의 헤더에는 이미지데이터외에 필요한 필수정보들이 기록되있는데, 위에서 아래순으로 차례대로 Byte에따라 담기도록 선언한것이다.
    그전에 위키피디아의 BitMap부분을 살펴봐야한다.
    https://en.wikipedia.org/wiki/BMP_file_format
    먼저 코드의 BITMAP_header 구조체에 해당하는 file header부분이다.
    가장먼저 등장하는 헤더로, 2 + 4 + 2+ 2+ 4 = 14byte의 길이를 가진다.
    여기서 중요한정보는 offset0x00, 0x02, 0x0A가 쓰일것이고,
    쓰이지않는 0x06, 0x08은 int garbage로 들어가고, 쓰지않을것이다.

다음으로 DIB_header이다.
물론 BitMap의요소에서 말하는 DIB헤더는 이외에도 있지만 우리는 file header다음에 오는 헤더를 볼 것이다.
보면 헤더이름이 제각기고, 사이즈도 다 다른데
여기서 첫번째 문제가 생긴다.
보통 40Byte의 'BITMAPINFOHEADER'를 갖는 bmp파일이있는가 한편
실험으로 쓰려고 만든 bmp파일이 'BITMAPV5HEADER'로, 그길이가 124Byte이다.
여기서 우리가 선언한 구조체는 내부요소들이 꽉찰정도로 딱 정해진만큼만 데이터를 읽을것인데,
여기서 더 읽거나 덜읽으면 실제 이미지에해당하는 데이터를 잘못읽거나,
반대로 헤더데이터가 넘어가는 수가 있어서 데이터크기에 되게 민감하게 코딩을 해야한다.
필자는 몇시간동안 이 방법때문에 구조체자체를 124Byte가되게 바꾸기도 했지만 아무것도출력되지않는오류를 겪다가
겨우 BITMAPINFOHEADER의 헤더를 갖는 bmp를 만드는 프로그램을 찾아서
이를 통해 해결했다.(>> https://www.nchsoftware.com/imageconverter/ko/index.html)

여튼 중요한건 DIB_header는 정확히 40Byte크기를 가져야함이다.


  1. 파일에서 헤더를 읽는부분
  • line123, 124 : 헤더선언
  • line132, 134 : 총 14Byte를 2Byte, 12Byte로 끊어서 읽고있다. 별다른 이유는없다.
    중요한점은 line134에서 header.size부분의 주소를 넘겼는데
    이부분부터 시작해서 int garbage, unsigned int imamge_offset이 연속된 주소로 존재한다는것이다. (하나의 객체이기때문에)

다음으로 중요한점은 앞서본 파일헤더에서

BM으로된 형식만을 다룬다는것이다.
따라서 line136에서 이를 체크해주고있다.

이번엔 DIB헤더를 읽어들인다.
이 또한 DIB헤더의크기가 40, compression(압축여부)가 0, bitsperpixel(1픽셀을 나타내는 바이트수)가 24로 고정적이어야한다.


  1. 이미지를 읽어들이는 부분
    앞서서 헤더를 읽어들였으면 이제는 이미지데이터를 읽을차례인데, 그전에 파일에서 정보를 읽는 대상인 fp포인터를 이미지데이터가 실제존재하는위치로 이동시켜야한다.

4-1. RGB, Image구조체
RGB의경우도 파일에서 읽어들이는데, blue, green, red순으로 정보가 저장되있어서 그대로 따라간다.
Image도 마찬가지.

4-2. Image를 읽는 함수

  • line 44 : 생성한 구조체의 객체인 Image객체를 만듭니다.
  • line 45 : Image.rgb는 2차원데이터이므로, 2차원중 하나의 차원을 먼저 동적할당합니다.
  • line 48 : 실제 픽셀데이터는 아래와 같은 공식으로 바이트로 변환됩니다.
  • line 51~ 53 : 콘솔창에서 위에서 아래로 데이터가 출력되도록 거꾸로 저장합니다.
  • line 60~ 63 : 동적할당한 메모리를 반환하는 함수. rgb가 2차원이므로 2번에걸쳐서 반환해줌


  1. RGB를 GrayScale로 변환하는 부분
    원리는 간단하다. RGB에해당하는 모든 정수값을 합해서 1인 값으로 치환할것이다.
    그리고 이 치환한값으로 이미지객체내의 rgb값을 모두 바꾸면 끝
    이번엔 이미지객체에 저장된값을 토대로 콘솔에 출력하는 부분이다.

마지막으로 이미지객체를 imageToText로 전달하면 끝이다.





원래 영상에서는 그레이스케일로 바꾼뒤 그것을 파일로 저장하는 함수도 있으나, 저장된 bmp파일이 제대로 열리지가않아서 설명을 뺏다.
아직 코드들을 제대로 다 이해하지못한것같은데, 호환되지않는 요소들이있어서 더 애먹는것같다.
앞으로 다른형태의 ASCII Art를 만드는방법을 더 찾아볼 생각이다.

전체코드

/*
BMP : 압축되지않은 이미지 파일
zip, gif : 허프만압축법, 비손실 압축 
jpeg : 손실압축 그러나 보기에는 문제X (생략해도 티안나는 정보는 없애거 축소함)
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

//헤더를 읽기위한 구조체
struct BITMAP_header { 
	char name[2]; //BM이라고 가정한다.
	unsigned int size; //unsigned 중요 sizeof(int) = 4 
	int garbage; //필요하지않은 중간 byte들
	unsigned int image_offset; //image가 시작되는 위치?
};

//두번째로 읽어야할 헤더DIB 40byte
struct DIB_header {
	unsigned int header_size; //4바이트
	unsigned int width, height; //2바이트씩
	unsigned short int colorplanes;
	unsigned short int bitsperpixel;
	unsigned int compression; //0:노압축
	unsigned int image_size;
	//이외에는 관심없기에 여기까지만
	unsigned int temp[4]; //1번
}; 

struct RGB {
	unsigned char blue;
	unsigned char green;
	unsigned char red;
};

struct Image {
	int height;
	int width;
	struct RGB** rgb; //2차원구조를 원했기에 => 동적할당
};


struct Image readImage(FILE* fp, int height, int width) {
	struct Image pic;
	pic.rgb = (struct RGB**)malloc(height * sizeof(void*)); //동적할당하는중
	pic.height = height;
	pic.width = width;
	int bytestoread = ((24 * width + 31) / 32) * 4;
	int numOfrgb = bytestoread / sizeof(struct RGB) + 1;

	for (int i = height - 1; i >= 0; i--) {  //위에서 아래로 보이게
		pic.rgb[i] = (struct RGB*) malloc(numOfrgb * sizeof(struct RGB));
		fread(pic.rgb[i], 1, bytestoread, fp); //실제로 이미지픽셀을 읽어드림
	}
	
	return pic; //만든 이미지 구조체를 리턴
}


void freeImage(struct Image pic) {
	for (int i = pic.height - 1; i >= 0; i--)  free(pic.rgb[i]);
	free(pic.rgb);
}

//RGB -> intensity -> 그레이스케일로 출력
unsigned char grayscale(struct RGB rgb) {
	//return (rgb.red + rgb.green + rgb.blue) / 3; //한가지 방법
	return (0.3*rgb.red + 0.6*rgb.green + 0.1*rgb.blue); //또다른 방법 : 가중치를 둠
}

//위의 그레이스케일함수를 사용한값으로 RGB값을 모두 변환
void RGBImageToGrayscale(struct Image pic) {
	for (int i = 0; i < pic.height; i++)
		for (int j = 0; j < pic.width; j++)
			pic.rgb[i][j].red = pic.rgb[i][j].green = pic.rgb[i][j].blue = grayscale(pic.rgb[i][j]);

}

//TEXT ART
void imageToText(struct Image img) {
	char textpixel[] = { '@', '#', '%', 'O' , 'a', '-', '.', ' '};

	for (int i = 0; i < img.height; i++) {
		for (int j = 0; j < img.width; j++) {
			unsigned char gs = grayscale(img.rgb[i][j]);
			putchar(textpixel[7 - gs / 32]);
		}
		putchar('\n');
	}
}

int createBWImage(struct BITMAP_header header, struct DIB_header dibheader
				, struct Image pic) {
	FILE* fpw = fopen("new.bmp", "w");
	if (fpw == NULL) return 1;
	RGBImageToGrayscale(pic);


	//새로운 BMP파일의 헤더 작성부분
	fwrite(header.name, 2, 1, fpw);
	fwrite(&header.size, 3 * sizeof(int), 1, fpw); 

	//dib 헤더 작성
	//실제 dibheader는 40byte이지만 우리는 24byte인 DIB_header만큼 기록했다.
	//추가로 작성해줘야함. => DIB_header구조체에 1번문항 작성
	fwrite(&dibheader, sizeof(struct DIB_header), 1, fpw); 

	//이미지를 저장하는부분
	for (int i = pic.height - 1; i >= 0; i--)
		fwrite(pic.rgb[i], ((24 * pic.width + 31) / 32) * 4, 1, fpw);

	fclose(fpw);
	return 0;
}

int openbmpfile(char *filename) {
	FILE* fp = fopen(filename, "r"); //읽기전용으로 열기
	if (fp == nullptr) return 1;

	//압축을사용한 BMP도 있으니 이런 여러 정보를 불러와야함
	//파일 헤더를 읽어서 그런 정보를 가져와야 한다. (14bit)
	//우리는 BM을 사용할것
	struct BITMAP_header header;
	struct DIB_header dibheader;
	//14바이트만을 읽어야하는데 16바이트를사용중임 .나머지2바이트는
	//이 구조체가 아닌 다른곳에 쓰여야한다.
	//sizeof(BITMAP_header); 

	//크기, 1:몇개... 결과 14바이트가아닌 16바이트가읽힘
	//fread(&header, sizeof(struct BITMAP_header), 1, fp); 
	//따라서 위 코드처럼 구조체크기만큼 읽으면 2바이트가 잘못읽혀들어옴
	fread(header.name, 2, 1, fp); //앞 2바이트먼저 읽고
	fread(&header.size, 3 * sizeof(int), 1, fp); //int 3개를 읽는다.
	printf("First Two Char %c%c\nHeader Size %d\nOffset %d\n",header.name[0], header.name[1]
		,header.size, header.image_offset);
	if (header.name[0] != 'B' || header.name[1] != 'M') {
		fclose(fp);
		return 1;
	}

	//이제 파일의 너비, 압축수행여부등을 확인해야함
	//또다른 헤더를 읽어야함. speak map , dib, dab등
	//dib헤더의 첫 4바이트는 dib헤더의 크기를 가짐
	fread(&dibheader, sizeof(struct DIB_header), 1, fp);	
	printf("DIB_header %lld\n", sizeof(struct DIB_header));
	//확인해보자
	printf("DIBHeader size %d\nWidth %d Height %d\nColor planes %d\nBits per pixel %d\nCompression %d\nImage size %d\n",
		dibheader.header_size, dibheader.width, dibheader.height, dibheader.colorplanes
		, dibheader.bitsperpixel, dibheader.compression, dibheader.image_size);
	if (dibheader.header_size != 40 || dibheader.compression != 0
		|| dibheader.bitsperpixel != 24) {
		fclose(fp);
		return 1;
	}


	//이제 이미지가 시작하는 위치로 이동해야한다. 그래서 그 위치가 나올때까지의
	//파일의 모든 정보는 다 필요없다.
	//픽셀정보는 24비트로 저장된다. 8x3 = 24bit
	fseek(fp, header.image_offset, SEEK_SET); //그 위치로 포인터를 이동시킴
	
	//2차원 배열로 가져올거다 왜?
	struct Image image = readImage(fp, dibheader.height, dibheader.width);

	//우리는 흑백이미지로 출력할것이다.
	//이제 새로운 파일에 쓸 차례이다.
	//RGB를 흑백으로 변환해야한다.
	//grayscale()과 RGBImageToGrayscale()함수를 작성했다.
	//이제 실제로 흑백전환된 새로운 이미지를 만드는 함수를 작성하자
	imageToText(image);

	//createBWImage(header, dibheader, image);


	fclose(fp);
	freeImage(image);
	return 0;
}


int main(void) {
	while (1) {
		printf("write filename.\n");
		char filename[100];
		scanf("%s", filename);
		openbmpfile(filename);
	}


	return 0;
}
profile
I will be a socially developer

0개의 댓글