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

asciiart.png

🌳 구성

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());
}

joker

길쭉하게 나오는 것 같아 옆으로 늘렸더니 조커가 납작하게 표현되었네요.. 😨

📝 마무리

오늘 간단하게 CLI 환경에서 이미지를 아스키코드로 변환하는 프로그램을 만들어 봤습니다.

fastfetch

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