Có nhiều cách tiếp cận hơn để chuyển đổi hình ảnh sang nghệ thuật ASCII chủ yếu dựa trên việc sử dụng các phông chữ có khoảng cách đơn . Để đơn giản, tôi chỉ bám vào những điều cơ bản:
Dựa trên cường độ pixel / khu vực (đổ bóng)
Cách tiếp cận này xử lý mỗi pixel của một vùng pixel dưới dạng một chấm duy nhất. Ý tưởng là tính toán cường độ thang màu xám trung bình của dấu chấm này và sau đó thay thế nó bằng ký tự có cường độ đủ gần với cường độ đã tính. Để làm được điều đó, chúng tôi cần một số danh sách các ký tự có thể sử dụng, mỗi ký tự có cường độ được tính toán trước. Hãy gọi nó là một nhân vật map
. Để chọn nhanh hơn nhân vật nào phù hợp nhất với cường độ, có hai cách:
Bản đồ ký tự cường độ phân bố tuyến tính
Vì vậy, chúng tôi chỉ sử dụng các ký tự có sự khác biệt về cường độ với cùng một bước. Nói cách khác, khi sắp xếp tăng dần thì:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
Ngoài ra khi ký tự của chúng tôi map
được sắp xếp thì chúng tôi có thể tính toán ký tự trực tiếp từ cường độ (không cần tìm kiếm)
character = map[intensity_of(dot)/constant];
Bản đồ ký tự cường độ phân bố tùy ý
Vì vậy, chúng tôi có một loạt các ký tự có thể sử dụng và cường độ của chúng. Chúng ta cần tìm cường độ gần nhất với cường độ intensity_of(dot)
Vì vậy, một lần nữa nếu chúng ta sắp xếp map[]
, chúng ta có thể sử dụng tìm kiếm nhị phân, nếu không, chúng ta cần O(n)
tìm kiếm một vòng lặp khoảng cách tối thiểu hoặc O(1)
từ điển. Đôi khi để đơn giản, ký tự map[]
có thể được xử lý như phân bố tuyến tính, gây ra sự biến dạng gamma nhẹ, thường không thể nhìn thấy trong kết quả trừ khi bạn biết mình cần tìm kiếm gì.
Chuyển đổi dựa trên cường độ cũng rất tốt cho hình ảnh thang xám (không chỉ đen trắng). Nếu bạn chọn dấu chấm là một pixel, kết quả sẽ lớn (một pixel -> ký tự đơn), vì vậy đối với hình ảnh lớn hơn, một vùng (nhân kích thước phông chữ) được chọn thay thế để giữ nguyên tỷ lệ khung hình và không phóng to quá mức.
Làm thế nào để làm nó:
- Chia đều hình ảnh thành các pixel (thang màu xám) hoặc các khu vực (hình chữ nhật) chấm s
- Tính toán cường độ của từng pixel / khu vực
- Thay thế nó bằng ký tự từ bản đồ ký tự có cường độ gần nhất
Là ký tự, map
bạn có thể sử dụng bất kỳ ký tự nào, nhưng kết quả sẽ tốt hơn nếu ký tự có các pixel phân tán đều dọc theo vùng ký tự. Để bắt đầu, bạn có thể sử dụng:
char map[10]=" .,:;ox%#@";
được sắp xếp giảm dần và giả vờ được phân phối tuyến tính.
Vì vậy, nếu cường độ của pixel / khu vực i = <0-255>
thì ký tự thay thế sẽ là
Nếu i==0
thì pixel / khu vực là màu đen, nếu i==127
thì pixel / khu vực là màu xám, và nếu i==255
thì pixel / khu vực là màu trắng. Bạn có thể thử nghiệm với các ký tự khác nhau bên trong map[]
...
Đây là một ví dụ cổ xưa của tôi trong C ++ và 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;
Bạn cần thay thế / bỏ qua nội dung VCL trừ khi bạn sử dụng môi trường Borland / Embarcadero .
mm_log
là bản ghi nhớ nơi văn bản được xuất ra
bmp
là bitmap đầu vào
AnsiString
là một chuỗi kiểu VCL được lập chỉ mục từ 1, không phải từ 0 như char*
!!!
Đây là kết quả: Hình ảnh ví dụ về cường độ NSFW hơi
Ở bên trái là đầu ra nghệ thuật ASCII (cỡ chữ 5 pixel), và ở bên phải hình ảnh đầu vào được phóng to một vài lần. Như bạn có thể thấy, đầu ra là ký tự pixel -> lớn hơn. Nếu bạn sử dụng các khu vực lớn hơn thay vì pixel thì thu phóng sẽ nhỏ hơn, nhưng tất nhiên kết quả đầu ra kém đẹp mắt hơn. Cách tiếp cận này rất dễ dàng và nhanh chóng để viết mã / xử lý.
Khi bạn thêm những thứ nâng cao hơn như:
- tính toán bản đồ tự động
- lựa chọn kích thước pixel / vùng tự động
- hiệu chỉnh tỷ lệ khung hình
Sau đó, bạn có thể xử lý các hình ảnh phức tạp hơn với kết quả tốt hơn:
Đây là kết quả theo tỷ lệ 1: 1 (thu phóng để xem các ký tự):
Tất nhiên, để lấy mẫu khu vực, bạn sẽ mất các chi tiết nhỏ. Đây là hình ảnh có cùng kích thước với ví dụ đầu tiên được lấy mẫu với các khu vực:
Hình ảnh ví dụ nâng cao về cường độ NSFW nhẹ
Như bạn có thể thấy, điều này phù hợp hơn với hình ảnh lớn hơn.
Phù hợp ký tự (kết hợp giữa đổ bóng và nghệ thuật ASCII rắn)
Cách tiếp cận này cố gắng thay thế khu vực (không còn các chấm pixel đơn lẻ) bằng ký tự có cường độ và hình dạng tương tự. Điều này dẫn đến kết quả tốt hơn, ngay cả với các phông chữ lớn hơn được sử dụng so với cách tiếp cận trước đây. Mặt khác, cách tiếp cận này tất nhiên là chậm hơn một chút. Có nhiều cách khác để làm điều này, nhưng ý tưởng chính là tính toán sự khác biệt (khoảng cách) giữa vùng hình ảnh ( dot
) và ký tự được hiển thị. Bạn có thể bắt đầu với tổng sai lệch tuyệt đối giữa các pixel một cách ngây thơ, nhưng điều đó sẽ dẫn đến kết quả không tốt lắm vì ngay cả sự thay đổi một pixel cũng sẽ làm cho khoảng cách lớn hơn. Thay vào đó, bạn có thể sử dụng tương quan hoặc các số liệu khác nhau. Thuật toán tổng thể gần giống như cách tiếp cận trước đây:
Vì vậy, chia đều hình ảnh thành các khu vực hình chữ nhật (tỷ lệ xám) dot 's
lý tưởng nhất là có cùng tỷ lệ khung hình với các ký tự phông chữ được hiển thị (nó sẽ bảo toàn tỷ lệ khung hình. Đừng quên rằng các ký tự thường chồng lên nhau một chút trên trục x)
Tính toán cường độ của từng khu vực ( dot
)
Thay thế nó bằng một ký tự từ ký tự map
có cường độ / hình dạng gần nhất
Làm thế nào chúng ta có thể tính toán khoảng cách giữa một ký tự và một dấu chấm? Đó là phần khó nhất của cách tiếp cận này. Trong khi thử nghiệm, tôi phát triển sự thỏa hiệp giữa tốc độ, chất lượng và sự đơn giản:
Chia vùng ký tự thành các vùng
- Tính toán cường độ riêng biệt cho trái, phải, lên, xuống và vùng trung tâm của mỗi ký tự từ bảng chữ cái chuyển đổi của bạn (
map
).
- Chuẩn hóa tất cả các cường độ, do đó chúng độc lập về kích thước khu vực ,
i=(i*256)/(xs*ys)
.
Xử lý hình ảnh nguồn trong các khu vực hình chữ nhật
- (với cùng tỷ lệ khung hình với phông chữ đích)
- Đối với mỗi khu vực, tính toán cường độ theo cách tương tự như trong dấu đầu dòng số 1
- Tìm kết quả phù hợp nhất từ các cường độ trong bảng chữ cái chuyển đổi
- Xuất ra ký tự vừa vặn
Đây là kết quả cho font size = 7 pixel
Như bạn có thể thấy, kết quả hiển thị đẹp mắt, ngay cả khi sử dụng kích thước phông chữ lớn hơn (ví dụ cách tiếp cận trước là với kích thước phông chữ 5 pixel). Đầu ra có cùng kích thước với hình ảnh đầu vào (không thu phóng). Kết quả tốt hơn đạt được vì các ký tự gần với hình ảnh gốc hơn, không chỉ theo cường độ, mà còn bởi hình dạng tổng thể, và do đó bạn có thể sử dụng phông chữ lớn hơn mà vẫn bảo toàn chi tiết (tất nhiên).
Đây là mã hoàn chỉnh cho ứng dụng chuyển đổi dựa trên 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();
}
//---------------------------------------------------------------------------
Nó đơn giản là một ứng dụng biểu mẫu ( Form1
) với một đơn TMemo mm_txt
trong đó. Nó tải một hình ảnh, "pic.bmp"
và sau đó tùy theo độ phân giải, chọn phương pháp sử dụng để chuyển đổi thành văn bản được lưu vào "pic.txt"
và gửi đến bản ghi nhớ để trực quan hóa.
Đối với những người không có VCL, hãy bỏ qua nội dung VCL và thay thế AnsiString
bằng bất kỳ loại chuỗi nào bạn có, cũng như Graphics::TBitmap
bằng bất kỳ lớp bitmap hoặc hình ảnh nào bạn có với khả năng truy cập pixel.
Một lưu ý rất quan trọng là điều này sử dụng cài đặt của mm_txt->Font
, vì vậy hãy đảm bảo bạn đã đặt:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
để làm cho điều này hoạt động bình thường, nếu không, phông chữ sẽ không được xử lý như một khoảng cách đơn. Con lăn chuột chỉ cần thay đổi kích thước phông chữ lên / xuống để xem kết quả trên các kích thước phông chữ khác nhau.
[Ghi chú]
- Xem hình ảnh chân dung Word
- Sử dụng ngôn ngữ có khả năng truy cập bitmap / tệp và xuất văn bản
- Tôi thực sự khuyên bạn nên bắt đầu với cách tiếp cận đầu tiên vì nó rất dễ dàng và đơn giản, sau đó chuyển sang cách thứ hai (có thể được thực hiện như sửa đổi của cách đầu tiên, vì vậy hầu hết mã vẫn như vậy)
- Bạn nên tính toán với cường độ đảo ngược (pixel màu đen là giá trị tối đa) vì bản xem trước văn bản tiêu chuẩn nằm trên nền trắng, do đó dẫn đến kết quả tốt hơn nhiều.
- bạn có thể thử nghiệm với kích thước, số lượng và bố cục của các vùng chia nhỏ hoặc sử dụng một số lưới như
3x3
thay thế.
So sánh
Cuối cùng, đây là sự so sánh giữa hai cách tiếp cận trên cùng một đầu vào:
Các hình ảnh được đánh dấu chấm màu xanh lá cây được thực hiện với phương pháp # 2 và các hình ảnh màu đỏ với # 1 , tất cả đều có kích thước phông chữ sáu pixel. Như bạn có thể thấy trên hình ảnh bóng đèn, phương pháp tiếp cận nhạy cảm với hình dạng tốt hơn nhiều (ngay cả khi phương pháp # 1 được thực hiện trên hình ảnh nguồn được thu phóng 2x).
Ứng dụng tuyệt vời
Trong khi đọc các câu hỏi mới của ngày hôm nay, tôi đã nảy ra ý tưởng về một ứng dụng thú vị lấy một vùng được chọn của màn hình và liên tục đưa nó vào bộ chuyển đổi ASCIIart và xem kết quả. Sau một giờ viết mã, nó đã hoàn thành và tôi rất hài lòng với kết quả mà tôi chỉ cần thêm nó vào đây.
OK, ứng dụng chỉ bao gồm hai cửa sổ. Cửa sổ chính đầu tiên về cơ bản là cửa sổ chuyển đổi cũ của tôi mà không có lựa chọn và xem trước hình ảnh (tất cả những thứ ở trên đều nằm trong đó). Nó chỉ có cài đặt xem trước và chuyển đổi ASCII. Cửa sổ thứ hai là một hình thức trống với bên trong trong suốt để lựa chọn vùng lấy (không có chức năng nào).
Bây giờ trong bộ đếm thời gian, tôi chỉ cần lấy khu vực đã chọn theo biểu mẫu lựa chọn, chuyển nó sang chuyển đổi và xem trước ASCIIart .
Vì vậy, bạn bao quanh một khu vực bạn muốn chuyển đổi bằng cửa sổ lựa chọn và xem kết quả trong cửa sổ chính. Nó có thể là một trò chơi, người xem, v.v. Nó giống như sau:
Vì vậy, bây giờ tôi có thể xem ngay cả video trong ASCIIart để giải trí. Một số thực sự tốt đẹp :).
Nếu bạn muốn thử thực hiện điều này trong GLSL , hãy xem phần này: