오늘의 만들어볼 간단한 프로젝트는 바로 이미지를 아스키코드로 변환해 주는 img2ascii입니다.

🌳 구성#
1
2
3
4
5
6
7
8
9
| .
├── .git/
├── .gitattributes
├── third_party/
│ └── stb/
│ └── stb_image.h
├── src/
│ └── main.cpp
└── CMakeLists.txt
|
작은 프로젝트지만 git과 cmake를 활용했고 이미지 파일을 로드하기 위해 nothings/stb 라이브러리를 사용했습니다. 👍
🚀 핵심 내용#
💡 Info
전체 코드는 이곳에서 보실 수 있습니다.
2차원(가로, 세로)으로 이루어진 사진은 컴퓨터에서 1차원 배열로 이루어져 있습니다. (상향식, 하향식 등 더 많은 내용이 있지만, 이 글에서는 넘어가겠습니다.)
컬러 이미지(3채널)는 픽셀마다 RGB 컬러의 밝기 값이 들어있습니다. 한 픽셀에 (0~255) 값 색상마다 세 개씩 있다는 뜻입니다. 숫자가 높을수록 밝습니다. 만약 1920*1080 이미지에서 1234*123픽셀의 RGB 정보를 찾으려면 아래처럼 찾을 수 있습니다.
1
2
3
4
| int index = (1234 * 1920 + 123) * 3;
int r = pixels[index + 0];
int g = pixels[index + 1];
int b = pixels[index + 2];
|
위와 같이 생긴 컬러 이미지를 sRGB/BT.709 색 변환 기준에 따라 1채널로 만듭니다. 세 개였던 (0~255) 값을 한 개로 변환, 즉 흑백 사진이죠.
흑백 사진의 밝기 값을 이제 255로 나누면 이미지의 모든 밝기값이 0 ~ 1(정규화)로 됩니다.
1
| brightness = (0.2126 * pixels[index] + 0.7152 * pixels[index+1] + 0.0722 * pixels[index+2]) / 255.0;
|
이제 사용할 문자열을 정의합니다. 보통 하얀 배경에 까만색 글자가 찍힙니다. 즉 문자가 배경에 비해 많이 보이면 어둡고, 적게 보이면 밝습니다.
그렇다면 어두운 곳을 표현할 문자는 앞쪽에, 밝은 곳을 표현할 문자는 뒤에 표현해서 그 인덱스를 사용하면 될 것 같습니다. 😎
1
2
| // 오른쪽으로 갈수록 글자 영역은 줄어듦(밝아짐)
"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,\"^`'. "
|
🚀 구현#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| #define STB_IMAGE_IMPLEMENTATION
#include "stb/stb_image.h"
#include <iostream>
#include <string>
#include <sstream>
// forward declaration
const std::string img2ascii(const std::string& path);
int main(int argc, char* argv[]) {
if(0 < argc && argc < 2) {
std::cerr << "usage: img2ascii file...\n";
return 1;
}
for(int i=1; i<argc; i++) {
std::cout << img2ascii(argv[i]);
}
return 0;
}
const std::string img2ascii(const std::string& path) {
static const std::string kRamp = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,\"^`'. ";
int width, height, channel, index;
double brightness = 0.0;
unsigned char* pixels = stbi_load(path.c_str(), &width, &height, &channel, 3);
if(!pixels) {
std::cerr << "image loading failed: " << stbi_failure_reason() << "\n";
return "";
}
std::ostringstream ss;
if(channel == 3) {
for(int row=0; row<height; row++) {
for(int col=0; col<width; col++) {
index = (row * width + col) * 3;
brightness = (0.2126 * pixels[index] + 0.7152 * pixels[index+1] + 0.0722 * pixels[index+2]) / 255.0;
ss << std::string(3, kRamp[static_cast<int>(brightness * (kRamp.size() - 1))]);
}
ss << "\n";
}
} else if(channel == 1) {
for(int row=0; row<height; row++) {
for(int col=0; col<width; col++) {
index = (row * width + col);
brightness = pixels[index] / 255.0;
ss << std::string(3, kRamp[static_cast<int>(brightness * (kRamp.size() - 1))]);
}
ss << "\n";
}
}
ss << "\n";
stbi_image_free(pixels);
return std::move(ss.str());
}
|

길쭉하게 나오는 것 같아 옆으로 늘렸더니 조커가 납작하게 표현되었네요.. 😨
📝 마무리#
오늘 간단하게 CLI 환경에서 이미지를 아스키코드로 변환하는 프로그램을 만들어 봤습니다.

neofetch 또는 fastfetch를 실행해 보면 본인이 사용하는 운영체제의 로고가 아스키코드로 멋지게 표현되는 것을 볼 수 있는데 볼 때마다 어떻게 만들었을지 궁금증이 항상 있었는데 오늘 알게 된 것 같습니다. 👍