이미지를 ASCII 아트로 변환
프롤로그
이 주제는 여기에 팝업 SO 때때로, 그러나 보통 때문에 잘못 작성된 질문 인의 제거됩니다. 나는 그러한 질문을 많이 보았고 추가 정보가 요청되면 OP (일반적으로 낮은 응답)에서 침묵했습니다 . 때때로 입력이 나에게 충분하다면 나는 답변으로 응답하기로 결정하고 일반적으로 활성화 된 동안 하루에 몇 개의 찬성표를 얻지 만 몇 주 후에 질문이 제거 / 삭제되고 모든 것이 처음부터 시작됩니다. . 그래서이 Q & A 를 작성하기로 결정했습니다 . 답을 계속해서 다시 쓰지 않고도 이러한 질문을 직접 참조 할 수 있습니다.
또 다른 이유는 나를 겨냥한 이 META 스레드 이므로 추가 입력이 있으면 자유롭게 의견을 말하십시오.
질문
C ++를 사용하여 비트 맵 이미지를 ASCII 아트 로 변환하는 방법은 무엇입니까?
몇 가지 제약 :
- 그레이 스케일 이미지
- 고정 폭 글꼴 사용
- 간단하게 유지 (초급 프로그래머에게는 너무 고급 기능을 사용하지 않음)
다음은 관련 Wiki 페이지 ASCII 아트입니다 (@RogerRowland 덕분에).
단순성을 위해 대부분 고정 폭 글꼴 을 사용하는 ASCII 아트 변환에 대한 더 많은 접근 방식이 있습니다 .
픽셀 / 영역 강도 기반 (음영)
이 접근 방식은 픽셀 영역의 각 픽셀을 단일 점으로 처리합니다. 아이디어는이 점의 평균 회색조 강도를 계산 한 다음 계산 된 점에 가까운 강도를 가진 문자로 대체하는 것입니다. 이를 위해 미리 계산 된 강도를 가진 사용 가능한 문자 목록이 필요합니다 map
. 어떤 캐릭터가 어떤 강도에 가장 적합한 지 더 빨리 선택하려면 두 가지 방법이 있습니다.
선형 분포 강도 문자 맵
그래서 우리는 같은 스텝에서 강도 차이가있는 캐릭터 만 사용합니다. 즉, 오름차순으로 정렬하면 다음과 같습니다.
intensity_of(map[i])=intensity_of(map[i-1])+constant;
또한 캐릭터
map
가 정렬 되면 강도에서 직접 캐릭터를 계산할 수 있습니다 (검색 할 필요 없음).character=map[intensity_of(dot)/constant];
임의 분포 강도 문자 맵
그래서 우리는 사용 가능한 문자와 그 강도의 배열을 가지고 있습니다. 우리는 가장 가까운 강도를 찾을 필요가
intensity_of(dot)
그래서 다시 우리가 분류하는 경우map[]
우리는 그렇지 않으면 우리가 필요 이진 검색을 사용할 수 있습니다O(n)
분 거리 루프 또는 검색O(1)
사전을. 간결함을 위해 캐릭터map[]
는 선형 분포로 처리되어 검색 대상을 알지 못하는 한 일반적으로 결과에서 볼 수없는 약간의 감마 왜곡을 유발할 수 있습니다.
강도 기반 변환은 흑백 이미지뿐만 아니라 회색조 이미지에도 좋습니다. 도트를 단일 픽셀로 선택하면 결과가 커지므로 (1 픽셀-> 단일 문자) 더 큰 이미지의 경우 가로 세로 비율을 유지하고 너무 많이 확대하지 않도록 영역 (글꼴 크기의 곱)이 선택됩니다.
방법 :
- (계조)의 화소 또는 (장방형) 영역에 매우 균등 분할 화상 도트 의
- 각 픽셀 / 영역의 강도 계산
- 가장 가까운 강도로 캐릭터 맵의 캐릭터로 대체
캐릭터로 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
입력 비트 맵입니다.AnsiString
0에서가 아닌 형식 1 인덱싱 된 VCL 유형 문자열입니다char*
!!!
이것이 결과입니다 : 약간의 NSFW 강도 예제 이미지
왼쪽은 ASCII 아트 출력 (글꼴 크기 5px)이고 오른쪽 입력 이미지는 몇 배 확대되었습니다. 보시다시피 출력은 더 큰 픽셀-> 문자입니다. 픽셀 대신 더 큰 영역을 사용하면 확대 / 축소가 작아 지지만 출력은 시각적으로 덜 만족 스럽습니다. 이 접근 방식은 코드 / 처리가 매우 쉽고 빠릅니다.
다음과 같은 고급 기능을 추가 할 때 :
- 자동화 된지도 계산
- 자동 픽셀 / 영역 크기 선택
- 종횡비 보정
그런 다음 더 나은 결과로 더 복잡한 이미지를 처리 할 수 있습니다.
여기에서 1 : 1 비율이됩니다 (문자를 보려면 확대).
물론 영역 샘플링의 경우 작은 세부 사항이 손실됩니다. 다음은 영역으로 샘플링 된 첫 번째 예제와 동일한 크기의 이미지입니다.
보시다시피 이것은 더 큰 이미지에 더 적합합니다
문자 맞춤 (셰이딩과 솔리드 ASCII 아트 간의 하이브리드)
이 접근 방식은 영역 (더 이상 단일 픽셀 점 없음)을 유사한 강도와 모양을 가진 문자로 바꾸려고합니다. 이것은 이전 접근 방식과 비교하여 더 큰 글꼴을 사용하더라도 더 나은 결과를 가져옵니다. 반면에이 접근 방식은 물론 조금 느립니다. 이를 수행하는 더 많은 방법이 있지만 주요 아이디어는 이미지 영역 ( dot
)과 렌더링 된 캐릭터 간의 차이 (거리)를 계산하는 것입니다 . 픽셀 간 복근 차이의 순진한 합계로 시작할 수 있지만 1 픽셀 이동으로도 거리가 커지므로 상관 관계 나 다른 메트릭을 사용할 수 있으므로 좋은 결과를 얻지 못합니다. 전체 알고리즘은 이전 접근 방식과 거의 동일합니다.
- 그래서 균등 분할 이미지 (계조)의 직사각형 영역은 도트 의은
- 이상적으로는 렌더링 된 글꼴 문자 와 동일한 종횡비를 사용합니다 (종횡비를 유지합니다. 문자가 일반적으로 x 축에서 약간 겹치는 것을 잊지 마십시오)
- 각 영역의 강도 계산 (
dot
) map
가장 가까운 강도 / 모양을 가진 캐릭터의 캐릭터로 대체
문자와 점 사이의 거리를 계산하는 방법은 무엇입니까? 이것이이 접근법의 가장 어려운 부분입니다. 실험하는 동안 속도, 품질 및 단순성 사이에서이 절충안을 개발합니다.
문자 영역을 영역으로 나누기
- 변환 알파벳 (
map
) 에서 각 문자의 왼쪽, 오른쪽, 위, 아래 및 중앙 영역에 대해 별도의 강도를 계산합니다. - 모든 강도를 정규화하여 면적 크기에 독립적입니다.
i=(i*256)/(xs*ys)
- 변환 알파벳 (
직사각형 영역에서 소스 이미지 처리
- (대상 글꼴과 동일한 종횡비)
- 각 영역에 대해 글 머리 기호 1과 같은 방식으로 강도를 계산합니다.
- 변환 알파벳의 강도에서 가장 가까운 일치를 찾습니다.
- 맞는 문자 출력
이것은 글꼴 크기 = 7px의 결과입니다.
보시다시피 더 큰 글꼴 크기를 사용하더라도 출력이 시각적으로 만족 스럽습니다 (이전 접근 예제는 5px 글꼴 크기였습니다). 출력은 입력 이미지와 거의 같은 크기입니다 (줌 없음). 문자가 강도뿐 아니라 전체적인 모양에 의해서도 원본 이미지에 더 가깝기 때문에 더 나은 결과를 얻을 수 있으며 따라서 더 큰 글꼴을 사용하고 세부 사항을 계속 유지할 수 있습니다 (거친 지점까지).
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) // charcter 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; // temp 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 init buffers
tmp=new Graphics::TBitmap; if (tmp==NULL) break;
// allow 32bit 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 image by zoomed character size step
xf-=xf/3; // characters are usually overlaping 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 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"
제대로 작동하지 않으면 글꼴이 고정 폭으로 처리되지 않습니다. 마우스 휠은 글꼴 크기를 위 / 아래로 변경하여 다양한 글꼴 크기의 결과를 확인합니다.
[노트]
- Word Portraits 시각화 참조
- 비트 맵 / 파일 액세스 및 텍스트 출력 기능과 함께 언어 사용
- 매우 간단하고 간단하기 때문에 첫 번째 접근 방식으로 시작한 다음 두 번째 접근 방식으로 이동하는 것이 좋습니다 (첫 번째 방법을 수정하여 수행 할 수 있으므로 대부분의 코드가 그대로 유지됨).
- 표준 텍스트 미리보기가 흰색 배경에 있으므로 훨씬 더 나은 결과를 얻을 수 있기 때문에 반전 된 강도 (검은 색 픽셀이 최대 값)로 계산하는 것이 좋습니다.
- 세분 영역의 크기, 개수 및 레이아웃을 실험하거나
3x3
대신 그리드를 사용할 수 있습니다 .
[Edit1] 비교
마지막으로 동일한 입력에 대한 두 가지 접근 방식을 비교합니다.
녹색 점으로 표시된 이미지는 접근 # 2 로 수행 되고 # 1 의 빨간색 이미지는 모두 6
픽셀 글꼴 크기로 수행됩니다. 전구 이미지에서 볼 수 있듯이 모양에 민감한 접근 방식이 훨씬 더 좋습니다 ( 1 위 가 2 배 확대 된 소스 이미지에서 수행 된 경우에도 ).
[Edit2] 멋진 앱
오늘의 새로운 질문을 읽는 동안 데스크탑의 선택한 영역을 잡고 ASCIIart 변환기에 계속 공급 하고 결과를 보는 멋진 앱에 대한 아이디어를 얻었습니다 . 한 시간의 코딩 후 완료되고 결과에 너무 만족하여 여기에 추가해야합니다.
확인 앱은 2 개의 창으로 구성됩니다. 첫 번째 마스터 창은 기본적으로 이미지 선택 및 미리보기가없는 이전 변환기 창입니다 (위의 모든 항목이 포함되어 있음). ASCII 미리보기 및 변환 설정 만 있습니다. 두 번째 창은 잡기 영역 선택을 위해 내부가 투명한 빈 양식입니다 (기능 없음).
이제 타이머에서 선택 양식으로 선택한 영역을 잡고 변환으로 전달하고 ASCIIart를 미리 봅니다 .
따라서 변환하고자하는 영역을 선택 창으로 감싸고 결과를 마스터 창에서 볼 수 있습니다. 게임, 뷰어, ... 다음과 같이 보일 수 있습니다.
그래서 이제는 ASCIIart의 비디오도 재미있게 볼 수 있습니다 . 일부는 정말 좋습니다 :).
[편집 3]
이를 GLSL 에서 구현하려면 다음을 살펴보십시오.
참고 URL : https://stackoverflow.com/questions/32987103/image-to-ascii-art-conversion
'program story' 카테고리의 다른 글
String (& String), Vec (& Vec) 또는 Box (& Box)에 대한 참조를 함수 인수로 받아들이지 않는 이유는 무엇입니까? (0) | 2020.08.22 |
---|---|
Node.js에서 여러 콜백을 기다리는 관용적 방법 (0) | 2020.08.22 |
내가 할 수 없어야하는데 왜 TypeScript 개인 멤버에 액세스 할 수 있습니까? (0) | 2020.08.22 |
CSS Z- 인덱스 역설 꽃 (0) | 2020.08.22 |
'size_t'대 'container :: size_type' (0) | 2020.08.22 |