github twitter email
Xem ảnh trên Terminal và câu chuyện đằng sau
Mar 1, 2019
5 minutes read

Đối với mình, terminal đóng vai trò trọng tâm trong flow làm việc. Tất cả mọi thứ đều có thể reachable và controlable bằng terminal. Nhưng khi làm việc với những file ảnh thì đó không còn là trải nghiệm dễ chịu nữa. Một trong những use case mình hay gặp phải là mở nội dung file ảnh xem coi nó là gì, để từ đó có những quyết định đơn giản như xóa/ di chuyển vào thư mục phù hơp. Việc này rất đơn giản, chỉ cần gọi trình xem ảnh như Eye Of Gnome (eog) hoặc GPicView (gpicview) lên là xong. Ví dụ:

gpicview image.png

Nhưng một ngày đẹp trời, mình nghĩ, vậy thì bình thường quá rồi. Hay là mình tìm cách khác hay hơn (và dị hơn) xem sao, như là hiển thị ảnh trên terminal chẳng hạn. Và hành trình khám phá bắt đầu.

Sau một hồi tìm hiểu thì thì ra có một vài dự án phần mềm có ý tưởng như vậy, nhưng mình vẫn quyết định sẽ viết lại một cái. Và bài blog này là những ghi chép mình có được khi xây dựng nó. Và đây là kết quả của mình:

Điểm khác biệt của imgcat so với những chương trình khác trên thị trường:
- Hỗ trợ xem ảnh terminal với chế độ màu 8-bit lẫn 24-bit (true color).
- Hỗ trợ fetch ảnh trực tiếp từ url.
- Portable (không phải cài thư viện, compile này nọ linh tinh).

Và mình sẽ cài đặt chương trình trên bằng Go.

Nguyên lý

Một pixel mỗi glyph

Giả sử chúng ta có một tấm ảnh màu RGB. Chúng ta có thể chọn một giải pháp vô cùng trực quan là map từng pixel với từng glyph, là một ô để hiển thị chữ trên terminal.

Và để in ra một ô màu, ta dùng ANSI escape code.

\033[48;5;196m \033[0m

Trong đó, \033[48;5;196m là để chỉ thị những chữ tiếp tiếp theo sẽ có màu nền là 196, tức là màu đỏ. Và \033[0m để reset lại màu mặc định, và ở chính giữ đó là một khoảng trắng. Khi render ra thì khoảng trắng đó sẽ có màu đỏ.

Vì font chữ của mình có kích thước là 8x16 nên mỗi glyph có hình chữ nhật đứng, và kết quả là ảnh bị bóp lại như sau:

Vậy ta có thể workaround bằng cách mỗi một pixel sẽ map với 2 glyph liền nhau, 2 glyph 8x16 cạnh nhau sẽ ra một pixel vuông như thế này.

Nhìn ổn hơn rồi, tuy nhiên ảnh lúc này bự lên một cách không cần thiết.

Hai pixel mỗi glyph

Chúng ta có thể giải quyết vấn đề này bằng cách sử dụng một ký tự đặc biệt là <U+2580> hay còn gọi là “upper half block”, nó trông thế này: . Ký tự này có phân nữa trên được tô đầy bởi màu foreground, nữa dưới được tô đầy bởi màu background. Và nếu font chữ bạn có tỉ lệ là 8x16, nó sẽ là 2 ô vuông, hoàn hảo để thể hiện 2 pixel trong một tấm ảnh chỉ với 1 glyph.

\033[38;5;46m\033[48;5;196m▀\033[0m

Kết quả:

Vì ảnh đầu vào hiện tại chỉ có kích thước 32x32 nên ta không cảm nhận được sự khác biệt, thử với tấm ảnh lớn và chi tiết hơn (1280x720)

Dùng phương pháp đầu tiên với 2 glyph ứng với 1 pixel:

Dùng phương pháp cải tiến mỗi glyph thể hiện 2 pixel:

Lấy kích cỡ terminal và scale ảnh

Tuy nhiên, vấn đề phát sinh nếu terminal của chúng ta chiều ngang chỉ có thể hiển thị 80 cột, mà tấm ảnh có hơn 80 pixel thì sao? Từ đó phát sinh thêm 2 bài toán:

  • Lấy được kích thước của terminal
  • Scale ảnh (downsampling) ảnh để có thể “lọt” vừa terminal.

Ở bài toán đầu, chúng ta lấy số cột mà terminal có thể hiển thị bằng:

tput cols

Sau khi có được kích thước chiều dài tối đa mà tấm ảnh có thể hiển thị (ta không cần quan tâm đến chiều dài vì terminal có thể scroll lên xuống dễ dàng), ta tiến hành scale ảnh theo tỷ lệ. Bài toán này có thể dễ dàng giải quyết bằng việc gọi thư viện/ công cụ Image Magick hoặc OpenCV. Tuy nhiên, mình mục tiêu đề ra là ứng dụng cần phải portable, với những điều kiện có thể chạy được ở mức tối thiểu nhất, nên mình cần impliment lại thuật toán scale ảnh, hay còn gọi là downsampling ảnh. Ở đây mình chọn thuật toán đơn giản nhất là Nearest-neighbor interpolation.

Mã giả của thuật toán:

Input:
- tấm ảnh đầu vào biểu diễn bằng mảng 2 chiều các pixel: InputImage
- kích thước chiều ngang và dài của ảnh sau khi scale: Width, Height

Output:
- tấm ảnh đầu ra biểu diễn bằng mảng 2 chiều các pixel: OutputImage

Khởi tại một OutputImage rỗng có kích thước Width x Height

for i := 0 --> height
    for j := 0 --> width
        new_y := 
        new_x :=
        ResultImage[i][j] := InputImage[y][x]

Các vấn đề khác

  • Ngoài hình khối , bạn có thể sử dụng các hình khối khác như: , , , , , … Để có thể ra được một tấm ảnh mượt hơn. Tuy nhiên thì ảnh render ra vẫn chỉ có 2 màu, nên với thẩm mỹ của mình thì mình không thấy đáng.

Kết quả

Vậy là chúng ta đã đi qua nguyên lý hoạt động của việc hiển thị ảnh lên terminal, sản phẩm cuối cùng được mình public ở đây: stephentt-me/imgcat

  • Viết bằng Go, chỉ dùng mỗi thư viện chuẩn.
  • Hỗ trợ đọc file ảnh từ ổ cứng và tự động load link url.
  • Tự động scale ảnh theo kích thước terminal.

go back


comments powered by Disqus