Dạo gần đây tự nhiên rộ lên cái trend convert ảnh sang ASCII art [ảnh dưới dạng các ký tự ASCII]. Sau khi tìm hiểu thì mình thấy cách phổ biến nhất mà mọi người thường làm là lên google search "convert image to ASCII art" rồi upload ảnh lên, sau đó copy các ký tự ASCII về rồi in ra hoặc dùng một lib nào đó. Những cách đó cũng được thôi, nhưng bạn sẽ không thể custom cũng như là không hiểu các tool đó họ làm như thế nào.
Bạn nào nôn nóng có thể xem trước demo hoặc Github Repo
Trong bài viết này mình sẽ hướng dẫn các bạn convert từ ảnh sang ASCII art chỉ bằng javascript và không dùng bất kỳ thư viện nào cả. Ok bắt đầu nhé.
Thuật toán chuyển ảnh sang ASCII art
Có một bài post thú vị các bạn có thể xem How do ASCII art image conversion algorithms work?
Chuyển sang ASCII art cơ bản có 2 bước:
Chuyển ảnh màu sang ảnh trắng đen [gray colors]
Map từng pixel sang các ký tự dựa trên giá trị grayscale
Ví dụ,
6 thì tối màu hơnconst canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
7, cũng tối màu hơnconst canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
8.const canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
Trong bài viết này, mình sẽ triển khai thuật toán này bằng JS thuần và chạy trên trình duyệt.
Upload ảnh sang Canvas
Bước đầu tiên chúng ta cần cho phép người dùng upload ảnh, vì thế ta cần một input file. Sau đó mình sẽ phân tích ảnh sang từng pixel, mình sẽ dùng một
9. Dưới đây là HTML mà mình sử dụng.const canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
html
DOCTYPE html>
Ascii Art Converter
Ascii Art Converter
Tại bước này, mình có thể upload một ảnh lên input nhưng chưa có gì xảy ra cả. Đó là bởi vì mình cần chuyển file ảnh đó sang
9 element. Mình sẽ sử dụngconst canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
1 API:GrayScale = 0.21 R + 0.72 G + 0.07 B
js
const canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
Khi input thay đổi, mình sẽ tạo một
1 object mới, điều này sẽ giúp mình đọc file và chuyển vàoGrayScale = 0.21 R + 0.72 G + 0.07 B
9. Mình đã đặt size canvas bằng với size ảnh upload để tránh bị giảm chất lượng. 2 tham số cuối củaconst canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
4 xác định nơi đặt ảnh trong canvas. Trong trường hợp này, mình muốn vẽ một bức 'ảnh' từ vị trí góc trên cùng bên trái. [tọa độ [0,0]].GrayScale = 0.21 R + 0.72 G + 0.07 B
Một khi mình đã nhúng đoạn script này vào trang HTML, mình có thể upload một bức ảnh bất kì và nó sẽ hiển thị trong
9 element.const canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
Chuyển ảnh sang ảnh trắng đen Gray Colors
Bây giờ bức ảnh đã được upload, mình cần chuyển nó sang ảnh trắng đen. Màu của mỗi pixel được tạo từ 3 màu cơ bản: red, green, blue như trong giá trị hexadicimal color css [
6].GrayScale = 0.21 R + 0.72 G + 0.07 B
Một cách đơn giản để tính được thang màu xám tương ứng là lấy trung bình 3 giá trị này. Tuy nhiên, mắt con người chúng ta thì không nhạy cảm như nhau với 3 màu này. Ví dụ, mắt chúng ta rất nhạy cảm với màu green, trong khi blue thì sẽ hời hợt 1 chút. Do đó, chúng ta cần cân nhắc sử dụng các màu với các tỉ lệ khác nhau. Sau khi nghiên cứu chi tiết Grayscale Wikipedia Page, mình quyết định tính giá trị grayscale dựa trên công thức dưới đây:
bash
GrayScale = 0.21 R + 0.72 G + 0.07 B
Vì thế mình cần lặp qua mỗi pixel của bức ảnh và thay thế nó bằng giá trị grayscale.
9 API cung cấp functionconst canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
8 để chúng ta phân tách từng pixel của bức ảnh.GrayScale = 0.21 R + 0.72 G + 0.07 B
js
9. Mình đã đặt size canvas bằng với size ảnh upload để tránh bị giảm chất lượng. 2 tham số cuối củaconst toGrayScale = [r, g, b] => 0.21 * r + 0.72 * g + 0.07 * b
const convertToGrayScales = [context, width, height] => {
const imageData = context.getImageData[0, 0, width, height]
const grayScales = []
for [let i = 0; i {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
4 xác định nơi đặt ảnh trong canvas. Trong trường hợp này, mình muốn vẽ một bức 'ảnh' từ vị trí góc trên cùng bên trái. [tọa độ [0,0]].GrayScale = 0.21 R + 0.72 G + 0.07 B
Một khi mình đã nhúng đoạn script này vào trang HTML, mình có thể upload một bức ảnh bất kì và nó sẽ hiển thị trong
9 element.const canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
Chuyển ảnh sang ảnh trắng đen Gray Colors
Bây giờ bức ảnh đã được upload, mình cần chuyển nó sang ảnh trắng đen. Màu của mỗi pixel được tạo từ 3 màu cơ bản: red, green, blue như trong giá trị hexadicimal color css [
6].GrayScale = 0.21 R + 0.72 G + 0.07 B
Một cách đơn giản để tính được thang màu xám tương ứng là lấy trung bình 3 giá trị này. Tuy nhiên, mắt con người chúng ta thì không nhạy cảm như nhau với 3 màu này. Ví dụ, mắt chúng ta rất nhạy cảm với màu green, trong khi blue thì sẽ hời hợt 1 chút. Do đó, chúng ta cần cân nhắc sử dụng các màu với các tỉ lệ khác nhau. Sau khi nghiên cứu chi tiết Grayscale Wikipedia Page, mình quyết định tính giá trị grayscale dựa trên công thức dưới đây:
bash
Vì thế mình cần lặp qua mỗi pixel của bức ảnh và thay thế nó bằng giá trị grayscale.
9 API cung cấp functionconst canvas = document.getElementById['preview']
const fileInput = document.querySelector['input[type="file"]']
const context = canvas.getContext['2d']
fileInput.onchange = [e] => {
const file = e.target.files[0]
const reader = new FileReader[]
reader.onload = [event] => {
const image = new Image[]
image.onload = [] => {
canvas.width = image.width
canvas.height = image.height
context.drawImage[image, 0, 0]
}
image.src = event.target.result
}
reader.readAsDataURL[file]
}
8 để chúng ta phân tách từng pixel của bức ảnh.GrayScale = 0.21 R + 0.72 G + 0.07 B
getImageData sẽ cho ra một object chứa thuộc tính
9 là một Uint8ClampedArray. Data này là một mảng một chiều chứa các giá trị red, green, blue, alpha [RGBA] theo thứ tự, với các số nguyên có giá trị từ 0 - 255. Vì thế mình lặp qua mảng bằng cách tăng lên 4 mỗi lần lặp, lấy giá trị RGB từ 3 phần tử đầu tiên, tính toán tỉ lệ gray và sau đó tiếp tục cho đến hết.GrayScale = 0.21 R + 0.72 G + 0.07 B
grayRamp1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|[]1{}[]?-_+~i!lI;:,\"^`'. "
Lưu ý là RGBA khác RGBa nha. Với RGBA thì A là alpha đại diện cho độ trong suốt có giá trị là số nguyên từ 0 - 255. Còn RGBa được sử dụng phổ biến trong CSS với a cũng là alpha nhưng giá trị là số thập phân từ 0 - 1. Nghe có vẻ RGBa có thể biểu thị được alpha nhiều hơn vì không giới hạn về mặt lý thuyết nhưng điều này về mặt phần cứng thì không phải vậy. Có sự khác biệt nào giữa alpha
const toGrayScale = [r, g, b] => 0.21 * r + 0.72 * g + 0.07 * b
const convertToGrayScales = [context, width, height] => {
const imageData = context.getImageData[0, 0, width, height]
const grayScales = []
for [let i = 0; i 0.21 * r + 0.72 * g + 0.07 * b
const convertToGrayScales = [context, width, height] => {
const imageData = context.getImageData[0, 0, width, height]
const grayScales = []
for [let i = 0; i 0.21 * r + 0.72 * g + 0.07 * b
const convertToGrayScales = [context, width, height] => {
const imageData = context.getImageData[0, 0, width, height]
const grayScales = []
for [let i = 0; i 0.21 * r + 0.72 * g + 0.07 * b
const convertToGrayScales = [context, width, height] => {
const imageData = context.getImageData[0, 0, width, height]
const grayScales = []
for [let i = 0; i