이미지를 ASCII 아트로 변환


102

프롤로그

이 주제는 때때로 스택 오버플로에서 여기에 표시되지만 일반적으로 잘못 작성된 질문 때문에 제거됩니다. 나는 그러한 질문을 많이 보았고 추가 정보가 요청되면 OP (일반적으로 낮은 응답)에서 침묵했습니다 . 때때로 입력이 나에게 충분하다면 나는 대답으로 응답하기로 결정하고 일반적으로 활성화 된 동안 하루에 몇 개의 찬성표를 얻지 만 몇 주 후에 질문이 제거 / 삭제되고 모든 것이 시작됩니다. 처음. 그래서이 Q & A 를 작성하기로 결정했습니다 . 답을 계속해서 다시 쓰지 않고도 이러한 질문을 직접 참조 할 수 있습니다.

또 다른 이유는이 메타 스레드 가 나를 겨냥한 것이므로 추가 입력이 있으면 자유롭게 의견을 말하십시오.

질문

C ++를 사용하여 비트 맵 이미지를 ASCII 아트 로 변환하려면 어떻게 해야합니까?

몇 가지 제약 :

  • 그레이 스케일 이미지
  • 고정 폭 글꼴 사용
  • 간단하게 유지 (초급 프로그래머에게는 너무 고급 기능을 사용하지 않음)

다음은 관련 Wikipedia 페이지 ASCII 아트입니다 (@RogerRowland 덕분에).

여기 에 ASCII Art 변환 Q & A 와 유사한 미로가 있습니다.


이 위키 페이지를 참조 로 사용 하여 어떤 유형의 ASCII 아트를 참조하고 있는지 명확히 할 수 있습니까? 회색조 픽셀에서 해당 텍스트 문자로의 "간단한"조회 인 "이미지를 텍스트로 변환"처럼 들리므로 다른 의미인지 궁금합니다. 당신이 .....하지만 어쨌든 스스로 대답하는거야 것 같은데
로저 롤랜드


@RogerRowland 계정에 모두 간단한 (단 그레이 스케일 강도 기준) 및 고급 복용은 문자의 형상 (하지만 여전히 간단한만큼)
Spektre

1
귀하의 작업은 훌륭하지만 조금 더 SFW 인 샘플을 선택하면 감사하겠습니다.
kmote

당신이 프롤로그를 읽는다면 @TimCastelijns는이 답변의 같은 타입이 요청 된 처음이 아니다 볼 수있는이이기 때문에, (나머지는 그냥 따라 투표 수 있도록 관련 몇 가지 이전 문제에 대해 잘 알고 시작부터 대부분의 유권자) Q & A 단지를 Q Q 부분에 너무 많은 시간을 낭비하지 않았습니다. (내가 인정하는 잘못입니다) 질문에 제한을 거의 추가하지 않았습니다.
Spektre

답변:


152

주로 고정 폭 글꼴 사용을 기반으로하는 이미지를 ASCII 아트로 변환하는 방법이 더 많이 있습니다 . 단순성을 위해 기본 사항 만 고수합니다.

픽셀 / 영역 강도 기반 (음영)

이 접근 방식은 픽셀 영역의 각 픽셀을 단일 점으로 처리합니다. 아이디어는이 점의 평균 회색조 강도를 계산 한 다음 계산 된 점에 가까운 강도를 가진 문자로 대체하는 것입니다. 이를 위해 각각 미리 계산 된 강도를 가진 사용 가능한 문자 목록이 필요합니다. 문자라고합시다 map. 어떤 캐릭터가 어떤 강도에 가장 적합한 지 더 빨리 선택하려면 두 가지 방법이 있습니다.

  1. 선형 분포 강도 문자 맵

    그래서 같은 스텝에서 강도 차이가있는 캐릭터 만 사용합니다. 즉, 오름차순으로 정렬하면 다음과 같습니다.

     intensity_of(map[i])=intensity_of(map[i-1])+constant;

    또한 캐릭터 map가 정렬 되면 강도에서 직접 캐릭터를 계산할 수 있습니다 (검색 할 필요 없음).

     character = map[intensity_of(dot)/constant];
  2. 임의 분산 강도 문자 맵

    그래서 우리는 사용 가능한 문자와 그 강도의 배열을 가지고 있습니다. 에 가장 가까운 강도를 찾아야합니다. intensity_of(dot)그래서 다시 정렬하면 map[]이진 검색을 사용할 수 있습니다. 그렇지 않으면 O(n)검색 최소 거리 루프 또는 O(1)사전이 필요합니다 . 간결함을 위해 캐릭터 map[]를 선형 분포로 처리하여 약간의 감마 왜곡을 유발할 수 있습니다. 일반적으로 무엇을 찾아야할지 알지 못하는 경우 결과에서 볼 수 없습니다.

강도 기반 변환은 흑백 이미지뿐만 아니라 회색조 이미지에도 좋습니다. 도트를 단일 픽셀로 선택하면 결과가 커지므로 (1 픽셀-> 단일 문자) 더 큰 이미지의 경우 가로 세로 비율을 유지하고 너무 많이 확대하지 않도록 영역 (글꼴 크기의 곱)이 선택됩니다.

방법 :

  1. 균등 (계조) 픽셀로 화상을 분할 (사각형) 영역은 도트
  2. 각 픽셀 / 영역의 강도 계산
  3. 가장 가까운 강도를 가진 캐릭터 맵의 캐릭터로 대체

캐릭터 map는 모든 캐릭터를 사용할 수 있지만 캐릭터 영역을 따라 픽셀이 균등하게 분산되어 있으면 결과가 더 좋아집니다. 우선 다음을 사용할 수 있습니다.

  • char map[10]=" .,:;ox%#@";

내림차순으로 정렬하고 선형으로 분포 된 척합니다.

따라서 픽셀 / 영역의 강도가 i = <0-255>다음과 같은 경우 대체 문자는

  • map[(255-i)*10/256];

경우 i==0경우 다음 픽셀 / 지역, 검은 색 i==127다음 픽셀 / 영역은 회색이며, 경우 i==255다음 픽셀 / 지역은 흰색입니다. 내부의 다른 캐릭터를 실험 할 수 있습니다 map[]...

다음은 C ++ 및 VCL의 고대 예입니다.

AnsiString m = " .,:;ox%#@";
Graphics::TBitmap *bmp = new Graphics::TBitmap;
bmp->LoadFromFile("pic.bmp");
bmp->HandleType = bmDIB;
bmp->PixelFormat = pf24bit;

int x, y, i, c, l;
BYTE *p;
AnsiString s, endl;
endl = char(13); endl += char(10);
l = m.Length();
s ="";
for (y=0; y<bmp->Height; y++)
{
    p = (BYTE*)bmp->ScanLine[y];
    for (x=0; x<bmp->Width; x++)
    {
        i  = p[x+x+x+0];
        i += p[x+x+x+1];
        i += p[x+x+x+2];
        i = (i*l)/768;
        s += m[l-i];
    }
    s += endl;
}
mm_log->Lines->Text = s;
mm_log->Lines->SaveToFile("pic.txt");
delete bmp;

Borland / Embarcadero 환경 을 사용하지 않는 한 VCL 항목을 교체 / 무시해야 합니다.

  • mm_log 텍스트가 출력되는 메모입니다.
  • bmp 입력 비트 맵입니다.
  • AnsiString0에서 char*!!!가 아니라 1에서 인덱싱 된 VCL 유형 문자열입니다.

결과 : 약간의 NSFW 강도 예제 이미지

왼쪽에는 ASCII 아트 출력 (글꼴 크기 5 픽셀)이 있고 오른쪽 입력 이미지 는 몇 배 확대 되었습니다. 보시다시피 출력은 더 큰 픽셀-> 문자입니다. 픽셀 대신 더 큰 영역을 사용하면 확대 / 축소가 작아 지지만 출력은 시각적으로 덜 만족 스럽습니다. 이 접근 방식은 코드 / 처리가 매우 쉽고 빠릅니다.

다음과 같은 고급 기능을 추가 할 때 :

  • 자동화 된지도 계산
  • 자동 픽셀 / 영역 크기 선택
  • 종횡비 보정

그런 다음 더 나은 결과로 더 복잡한 이미지를 처리 ​​할 수 ​​있습니다.

다음은 1 : 1 비율의 결과입니다 (문자를 보려면 확대).

강도 고급 예

물론 영역 샘플링의 경우 작은 세부 사항이 손실됩니다. 다음은 영역으로 샘플링 된 첫 번째 예제와 동일한 크기의 이미지입니다.

약간의 NSFW 강도 고급 예제 이미지

보시다시피 이것은 더 큰 이미지에 더 적합합니다.

문자 맞춤 (음영과 단색 ASCII 아트 사이의 하이브리드)

이 접근 방식은 영역 (더 이상 단일 픽셀 점 없음)을 유사한 강도와 모양을 가진 문자로 바꾸려고합니다. 이는 이전 접근 방식과 비교하여 더 큰 글꼴을 사용하더라도 더 나은 결과를 가져옵니다. 반면에이 접근 방식은 물론 약간 느립니다. 이를 수행하는 더 많은 방법이 있지만 주요 아이디어는 이미지 영역 ( dot)과 렌더링 된 캐릭터 간의 차이 (거리)를 계산하는 것입니다 . 픽셀 간의 절대 차이의 순진한 합계로 시작할 수 있지만 1 픽셀 이동으로도 거리가 커지므로 좋은 결과가 나오지 않습니다. 대신 상관 관계 또는 다른 메트릭을 사용할 수 있습니다. 전체 알고리즘은 이전 접근 방식과 거의 동일합니다.

  1. 따라서 균일로 화상을 분할 (계조)의 직사각형 영역은 도트 의은

    이상적으로는 렌더링 된 글꼴 문자 와 동일한 종횡비를 사용합니다 (종횡비를 유지합니다. 문자는 일반적으로 x 축에서 약간 겹치는 것을 잊지 마십시오)

  2. 각 영역의 강도 계산 ( dot)

  3. map가장 가까운 강도 / 모양을 가진 캐릭터의 캐릭터 로 대체

문자와 점 사이의 거리를 어떻게 계산할 수 있습니까? 이것이이 접근법의 가장 어려운 부분입니다. 실험하는 동안 속도, 품질 및 단순성 사이에서이 절충안을 개발합니다.

  1. 문자 영역을 영역으로 나누기

    구역

    • 변환 알파벳 ( map) 에서 각 문자의 왼쪽, 오른쪽, 위, 아래 및 중앙 영역에 대해 별도의 강도를 계산합니다 .
    • 모든 강도를 정규화하여 면적 크기에 독립적입니다 i=(i*256)/(xs*ys).
  2. 직사각형 영역에서 소스 이미지 처리

    • (대상 글꼴과 동일한 종횡비)
    • 각 영역에 대해 글 머리 기호 # 1과 동일한 방식으로 강도를 계산합니다.
    • 변환 알파벳의 강도에서 가장 가까운 일치를 찾습니다.
    • 적합 문자 출력

이것은 글꼴 크기 = 7 픽셀의 결과입니다.

캐릭터 피팅 예

보시다시피, 더 큰 글꼴 크기를 사용하더라도 출력은 시각적으로 만족 스럽습니다 (이전 접근법 예제는 5 픽셀 글꼴 크기였습니다). 출력은 입력 이미지와 거의 같은 크기입니다 (줌 없음). 문자가 강도뿐 아니라 전체적인 모양에 따라 원본 이미지에 더 가깝기 때문에 더 나은 결과를 얻을 수 있으므로 더 큰 글꼴을 사용하고 세부 사항을 유지할 수 있습니다 (물론 어느 정도까지).

다음은 VCL 기반 변환 응용 프로그램의 전체 코드입니다.

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"

TForm1 *Form1;
Graphics::TBitmap *bmp=new Graphics::TBitmap;
//---------------------------------------------------------------------------


class intensity
{
public:
    char c;                    // Character
    int il, ir, iu ,id, ic;    // Intensity of part: left,right,up,down,center
    intensity() { c=0; reset(); }
    void reset() { il=0; ir=0; iu=0; id=0; ic=0; }

    void compute(DWORD **p,int xs,int ys,int xx,int yy) // p source image, (xs,ys) area size, (xx,yy) area position
    {
        int x0 = xs>>2, y0 = ys>>2;
        int x1 = xs-x0, y1 = ys-y0;
        int x, y, i;
        reset();
        for (y=0; y<ys; y++)
            for (x=0; x<xs; x++)
            {
                i = (p[yy+y][xx+x] & 255);
                if (x<=x0) il+=i;
                if (x>=x1) ir+=i;
                if (y<=x0) iu+=i;
                if (y>=x1) id+=i;

                if ((x>=x0) && (x<=x1) &&
                    (y>=y0) && (y<=y1))

                    ic+=i;
        }

        // Normalize
        i = xs*ys;
        il = (il << 8)/i;
        ir = (ir << 8)/i;
        iu = (iu << 8)/i;
        id = (id << 8)/i;
        ic = (ic << 8)/i;
        }
    };


//---------------------------------------------------------------------------
AnsiString bmp2txt_big(Graphics::TBitmap *bmp,TFont *font) // Character  sized areas
{
    int i, i0, d, d0;
    int xs, ys, xf, yf, x, xx, y, yy;
    DWORD **p = NULL,**q = NULL;    // Bitmap direct pixel access
    Graphics::TBitmap *tmp;        // Temporary bitmap for single character
    AnsiString txt = "";            // Output ASCII art text
    AnsiString eol = "\r\n";        // End of line sequence
    intensity map[97];            // Character map
    intensity gfx;

    // Input image size
    xs = bmp->Width;
    ys = bmp->Height;

    // Output font size
    xf = font->Size;   if (xf<0) xf =- xf;
    yf = font->Height; if (yf<0) yf =- yf;

    for (;;) // Loop to simplify the dynamic allocation error handling
    {
        // Allocate and initialise buffers
        tmp = new Graphics::TBitmap;
        if (tmp==NULL)
            break;

        // Allow 32 bit pixel access as DWORD/int pointer
        tmp->HandleType = bmDIB;    bmp->HandleType = bmDIB;
        tmp->PixelFormat = pf32bit; bmp->PixelFormat = pf32bit;

        // Copy target font properties to tmp
        tmp->Canvas->Font->Assign(font);
        tmp->SetSize(xf, yf);
        tmp->Canvas->Font ->Color = clBlack;
        tmp->Canvas->Pen  ->Color = clWhite;
        tmp->Canvas->Brush->Color = clWhite;
        xf = tmp->Width;
        yf = tmp->Height;

        // Direct pixel access to bitmaps
        p  = new DWORD*[ys];
        if (p  == NULL) break;
        for (y=0; y<ys; y++)
            p[y] = (DWORD*)bmp->ScanLine[y];

        q  = new DWORD*[yf];
        if (q  == NULL) break;
        for (y=0; y<yf; y++)
            q[y] = (DWORD*)tmp->ScanLine[y];

        // Create character map
        for (x=0, d=32; d<128; d++, x++)
        {
            map[x].c = char(DWORD(d));
            // Clear tmp
            tmp->Canvas->FillRect(TRect(0, 0, xf, yf));
            // Render tested character to tmp
            tmp->Canvas->TextOutA(0, 0, map[x].c);

            // Compute intensity
            map[x].compute(q, xf, yf, 0, 0);
        }

        map[x].c = 0;

        // Loop through the image by zoomed character size step
        xf -= xf/3; // Characters are usually overlapping by 1/3
        xs -= xs % xf;
        ys -= ys % yf;
        for (y=0; y<ys; y+=yf, txt += eol)
            for (x=0; x<xs; x+=xf)
            {
                // Compute intensity
                gfx.compute(p, xf, yf, x, y);

                // Find the closest match in map[]
                i0 = 0; d0 = -1;
                for (i=0; map[i].c; i++)
                {
                    d = abs(map[i].il-gfx.il) +
                        abs(map[i].ir-gfx.ir) +
                        abs(map[i].iu-gfx.iu) +
                        abs(map[i].id-gfx.id) +
                        abs(map[i].ic-gfx.ic);

                    if ((d0<0)||(d0>d)) {
                        d0=d; i0=i;
                    }
                }
                // Add fitted character to output
                txt += map[i0].c;
            }
        break;
    }

    // Free buffers
    if (tmp) delete tmp;
    if (p  ) delete[] p;
    return txt;
}


//---------------------------------------------------------------------------
AnsiString bmp2txt_small(Graphics::TBitmap *bmp)    // pixel sized areas
{
    AnsiString m = " `'.,:;i+o*%&$#@"; // Constant character map
    int x, y, i, c, l;
    BYTE *p;
    AnsiString txt = "", eol = "\r\n";
    l = m.Length();
    bmp->HandleType = bmDIB;
    bmp->PixelFormat = pf32bit;
    for (y=0; y<bmp->Height; y++)
    {
        p = (BYTE*)bmp->ScanLine[y];
        for (x=0; x<bmp->Width; x++)
        {
            i  = p[(x<<2)+0];
            i += p[(x<<2)+1];
            i += p[(x<<2)+2];
            i  = (i*l)/768;
            txt += m[l-i];
        }
        txt += eol;
    }
    return txt;
}


//---------------------------------------------------------------------------
void update()
{
    int x0, x1, y0, y1, i, l;
    x0 = bmp->Width;
    y0 = bmp->Height;
    if ((x0<64)||(y0<64)) Form1->mm_txt->Text = bmp2txt_small(bmp);
     else                  Form1->mm_txt->Text = bmp2txt_big  (bmp, Form1->mm_txt->Font);
    Form1->mm_txt->Lines->SaveToFile("pic.txt");
    for (x1 = 0, i = 1, l = Form1->mm_txt->Text.Length();i<=l;i++) if (Form1->mm_txt->Text[i] == 13) { x1 = i-1; break; }
    for (y1=0, i=1, l=Form1->mm_txt->Text.Length();i <= l; i++) if (Form1->mm_txt->Text[i] == 13) y1++;
    x1 *= abs(Form1->mm_txt->Font->Size);
    y1 *= abs(Form1->mm_txt->Font->Height);
    if (y0<y1) y0 = y1; x0 += x1 + 48;
    Form1->ClientWidth = x0;
    Form1->ClientHeight = y0;
    Form1->Caption = AnsiString().sprintf("Picture -> Text (Font %ix%i)", abs(Form1->mm_txt->Font->Size), abs(Form1->mm_txt->Font->Height));
}


//---------------------------------------------------------------------------
void draw()
{
    Form1->ptb_gfx->Canvas->Draw(0, 0, bmp);
}


//---------------------------------------------------------------------------
void load(AnsiString name)
{
    bmp->LoadFromFile(name);
    bmp->HandleType = bmDIB;
    bmp->PixelFormat = pf32bit;
    Form1->ptb_gfx->Width = bmp->Width;
    Form1->ClientHeight = bmp->Height;
    Form1->ClientWidth = (bmp->Width << 1) + 32;
}


//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
    load("pic.bmp");
    update();
}


//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
    delete bmp;
}


//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
    draw();
}


//---------------------------------------------------------------------------
void __fastcall TForm1::FormMouseWheel(TObject *Sender, TShiftState Shift, int WheelDelta, TPoint &MousePos, bool &Handled)
{
    int s = abs(mm_txt->Font->Size);
    if (WheelDelta<0) s--;
    if (WheelDelta>0) s++;
    mm_txt->Font->Size = s;
    update();
}

//---------------------------------------------------------------------------

하나가 포함 된 간단한 양식 응용 프로그램 ( Form1)입니다 TMemo mm_txt. 이미지를 불러 온 "pic.bmp"후 해상도에 따라 텍스트로 변환 할 때 사용할 방법을 선택 "pic.txt"하여 메모에 저장 하여 시각화합니다.

VCL이없는 사용자의 경우 VCL 항목을 무시하고 AnsiString가지고있는 문자열 유형으로 교체 Graphics::TBitmap하고 픽셀 액세스 기능을 사용할 수있는 비트 맵 또는 이미지 클래스로 교체하십시오 .

매우 중요한 점은이 설정이의 설정을 사용한다는 mm_txt->Font것이므로 다음을 설정해야합니다.

  • Font->Pitch = fpFixed
  • Font->Charset = OEM_CHARSET
  • Font->Name = "System"

제대로 작동하지 않으면 글꼴이 고정 폭으로 처리되지 않습니다. 마우스 휠은 글꼴 크기를 위 / 아래로 변경하여 다른 글꼴 크기의 결과를 확인합니다.

[노트]

  • 참조 워드 초상화 시각화
  • 비트 맵 / 파일 액세스 및 텍스트 출력 기능이있는 언어 사용
  • 매우 쉽고 간단하기 때문에 첫 번째 접근 방식으로 시작한 다음 두 번째 접근 방식으로 이동하는 것이 좋습니다 (첫 번째 수정으로 수행 할 수 있으므로 대부분의 코드는 그대로 유지됩니다).
  • 표준 텍스트 미리보기가 흰색 배경에 있으므로 훨씬 더 나은 결과를 얻을 수 있으므로 반전 된 강도 (검은 색 픽셀이 최대 값)로 계산하는 것이 좋습니다.
  • 세분 영역의 크기, 개수 및 레이아웃을 실험하거나 3x3대신 비슷한 그리드를 사용할 수 있습니다 .

비교

마지막으로 동일한 입력에 대한 두 가지 접근 방식을 비교합니다.

비교

녹색 점으로 표시된 이미지는 접근 방식 # 2 로 수행되고 빨간색은 # 1로 처리 되며 모두 6 픽셀 글꼴 크기입니다. 전구 이미지에서 볼 수 있듯이 모양에 민감한 접근 방식이 훨씬 좋습니다 ( 2 배 확대 된 소스 이미지 에서 # 1 이 수행 된 경우에도 ).

멋진 응용 프로그램

오늘의 새로운 질문을 읽으면서 데스크탑의 선택한 영역을 잡고 ASCIIart 변환기에 계속 공급 하고 결과를 보는 멋진 응용 프로그램에 대한 아이디어를 얻었습니다 . 한 시간 동안 코딩을 마치고 나면 결과가 너무 만족스러워서 여기에 추가하면됩니다.

응용 프로그램은 두 개의 창으로 구성됩니다. 첫 번째 마스터 창은 기본적으로 이미지 선택 및 미리보기가없는 이전 변환기 창입니다 (위의 모든 항목이 포함되어 있음). ASCII 미리보기 및 변환 설정 만 있습니다. 두 번째 창은 잡는 영역 선택을 위해 내부가 투명한 빈 양식입니다 (기능 없음).

이제 타이머에서 선택 양식으로 선택한 영역을 잡고 변환으로 전달하고 ASCIIart를 미리 봅니다 .

따라서 변환하려는 영역을 선택 창으로 묶고 결과를 마스터 창에서 볼 수 있습니다. 게임, 뷰어 등이 될 수 있습니다. 다음과 같습니다.

ASCIIart 그래버 예제

그래서 이제는 ASCIIart의 비디오도 재미있게 볼 수 있습니다 . 일부는 정말 좋습니다 :).

소유

이것을 GLSL 에서 구현하려면 다음을 살펴보십시오.


30
당신은 여기서 놀라운 일을했습니다! 감사! 그리고 저는 ASCII 검열을 좋아합니다!
Ander Biguri 2015 년

1
개선을위한 제안 : 강도뿐만 아니라 방향성 도함수를 해결하십시오.
Yakk-Adam Nevraumont 2015 년

1
@Yakk는 정교하게 신경 쓰나요?
tariksbl

2
@tarik은 강도뿐만 아니라 미분에 대해서도 일치합니다. 기본적으로 강도는 사람들이 보는 유일한 것이 아닙니다. 그들은 그라데이션과 가장자리를 봅니다.
Yakk-Adam Nevraumont

1
@Yakk 영역 세분화는 이러한 일을 간접적으로 수행합니다. 문자를 3x3영역 으로 처리 하고 DCT를 비교하는 것이 더 좋을 수 있지만 성능이 많이 저하 될 것이라고 생각합니다.
Spektre 2015 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.