PHP實現生成ascii字元圖片
網上經常有一些字元做成的圖片,比如這樣:
細想一下,這裡面主要運用到了幾個知識,有:
- 從圖片解析出畫素顏色(也就是通常說的RGB值)
- 去色處理
- 畫素對映到字元
作為世界上最好的語言,用PHP實現有趣功能也是易如反掌。下面講解一下具體實現。
1. 解析圖片中的畫素顏色
解析圖片中的畫素顏色,我們需要了解圖片儲存的格式,這裡就以BMP圖片為例。什麼?網上找不到BMP圖片?用QQ的截個圖,儲存成BMP就行了。
BMP圖片並不是從檔案的第1個位元組開始就是畫素資料,而是一個個14位元組的檔案頭,儲存著檔案的元資訊。緊挨著的是一個40位元組的圖片頭結構,儲存著圖片相關的元資訊。
詳細列出檔案頭和圖片頭結構的每個欄位,就有些太無聊了(詳細的頭資訊可以參見附錄)。想要解析圖片的畫素,只需要這 4 個資訊:
- 圖片檔案的總體大小 (檔案的第3~6個位元組)
- 畫素資料從檔案的哪裡開始 (11~14個位元組處)
- 圖片的寬度和高度 (寬:19~22位元組處、高:23~26位元組處)
- 圖片的一個畫素佔幾個位元組 (29~30位元組處)
想要解析出二進位制中的資料,用 unpack() 結合 substr() 就能搞定:
$data = file_get_contents('image.bmp');
$ret = unpack('v/Vsize/v/v/VpixelStart/V/Vwidth/Vheight/v/vbytePerPixel/V*6' ,
substr($data, 0x0, 54));
/**
* $ret的內容:
* array (
* 'size' => 706554,
* 'pixelStart' => 54,
* 'width' => 500,
* 'height' => 471,
* 'bytePerPixel' => 24,
* );
*/
2. 獲得畫素顏色以及去色
從上一節可以知道,我們想處理的圖片,畫素資料從檔案的第54位元組開始,每個畫素資料佔據24 bit。這24 bit中R(紅)、G(綠)、B(藍)的值各佔8 bit(1位元組)。
假如我想獲得圖片第 x 行,第 y 列的RGB值,那麼對應的RGB值的位置應該這樣計算:
畫素(x, y)的 B 值偏移 = 畫素資料開始位置 + 3 * (圖片寬度 * x + y)
畫素(x, y)的 G 值偏移 = 畫素資料開始位置 + 3 * (圖片寬度 * x + y) + 1
畫素(x, y)的 R 值偏移 = 畫素資料開始位置 + 3 * (圖片寬度 * x + y) + 2
從式子中發現三原色的值是以BGR順序排列的,不是通常的RGB順序。
如果你按照這種方法,將畫素按順序一個個畫在一張畫布上,你會發現得到的圖片是顛倒的,這是因為圖片的畫素資訊是倒過來儲存的,最左上角的畫素實際位於檔案的最末尾,所以想得到一張正過來的圖片,畫素資料應該這麼取:
畫素(x, y)的 B 值偏移 = 檔案大小 - 3 * (圖片寬度 * x + y) - 3
畫素(x, y)的 G 值偏移 = 檔案大小 - 3 * (圖片寬度 * x + y) - 2
畫素(x, y)的 R 值偏移 = 檔案大小 - 3 * (圖片寬度 * x + y) - 1
畫素的顏色是取到了,但最終ascii圖是黑白的,怎麼進行去色呢?去色的演算法有很多,這裡就用最簡單粗暴的:
新的R、G、B值 = [min(R, G, B) + max(R, G, B)] / 2;
黑白的畫素,R、G、B都是一樣的值,這個值可以稱作畫素的 明亮度
最終,取畫素的操作可以定義成一個函式:
function getPixelColor($x, $y) {
global $width, $size, $data;
$b = ord($data[$size - 3 * ($width * $x + $y) - 3]);
$g = ord($data[$size - 3 * ($width * $x + $y) - 2]);
$r = ord($data[$size - 3 * ($width * $x + $y) - 1]);
return (min($r, $g, $b) + max($r, $g, $b)) >> 1;
}
3. 畫素到字元對映
到了最後一個環節,我們要將圖片每個畫素的深淺轉換成ascii字元。ascii字元本身沒有顏色深淺一說。但是如果你把”#”和”.”分別排列成100x100的正方形,從視覺上”#”會比”.”顏色更暗一點。
我們可以取若干個字元代表不同畫素的明亮度,某個畫素的明亮度處於某個區間時,就以相應等級的字元替換:
function getChar($colorValue) {
$map = '@#mdohsy+/-:.` ';
return $map[(int) ($colorValue / 18)];
}
還有一個問題:如果把每個畫素都用一個字元替換的話,那麼輸出的字元圖將會非常巨大。所以最好是用一個字元替代原圖中 NxN 的畫素塊。整個畫素塊的明亮度,就取塊中每個畫素明亮度的均值。
以上問題都解決了,最後拿一張蒙娜麗莎的微笑來測試:
效果還不錯:-)。如果終端背景是白色的,可以將表示明亮度的字元序列反過來:
// $map = '@#mdohsy+/-:.` ';
$map = ' `.:-/+yshodm#@'; // 反過來
附錄
完整程式碼
<?php
$data = file_get_contents('timg.bmp');
$ret = unpack('v/Vsize/v/v/VpixelStart/V/Vwidth/Vheight/v/vbytePerPixel/V*6', substr($data, 0x0, 54));
$size = $ret['size'];
$offset = $ret['pixelStart'];
$width = $ret['width'];
$height = $ret['height'];
$bitDepth = $ret['bytePerPixel'];
$pixelLenPerChar = 4;
$charImgWidth = (int) ($width / $pixelLenPerChar);
$charImgHeight = (int) ($height / $pixelLenPerChar);
for ($i = 0; $i !== $charImgHeight; $i++) {
$buf = '';
for ($j = 0; $j !== $charImgWidth; $j++) {
$sum = 0;
for ($k = 0; $k !== $pixelLenPerChar; $k++) {
for ($l = 0; $l !== $pixelLenPerChar; $l++) {
$sum += getPixelColor($pixelLenPerChar * $i + $k, $pixelLenPerChar * $j + $l);
}
}
$sum = (int) ($sum / $pixelLenPerChar / $pixelLenPerChar);
$buf = getChar($sum) . $buf;
}
echo $buf . PHP_EOL;
}
function getPixelColor($x, $y) {
global $width, $size, $data;
$b = ord($data[$size - 3 * ($width * $x + $y) - 3]);
$g = ord($data[$size - 3 * ($width * $x + $y) - 2]);
$r = ord($data[$size - 3 * ($width * $x + $y) - 1]);
return (min($r, $g, $b) + max($r, $g, $b)) >> 1;
}
function getChar($colorValue) {
$map = '@#mdohsy+/-:.` ';
return $map[(int) ($colorValue / 18)];
}
BMP檔案頭格式
偏移 | 大小(位元組) | 含義 | 本文中圖片示例值 |
---|---|---|---|
0 | 2 | 固定為”BM”兩個字元的編碼 | 0x42 0x4d |
2 | 4 | 檔案大小 | 0x000ac7fa |
6 | 4 | 保留欄位,一般為 0 | 0x00000000 |
10 | 4 | 畫素資料起始處偏移 | 0x00000036 |
BMP圖片頭格式
偏移 | 大小(位元組) | 含義 | 本文中圖片示例值 |
---|---|---|---|
14 | 4 | 圖片頭的大小(位元組) | 0x00000028 |
18 | 4 | 圖片的寬度 | 0x000001f4 |
22 | 4 | 圖片的高度 | 0x000001d7 |
26 | 2 | 影象的幀數(靜態圖都是1) | 0x0001 |
28 | 2 | 一個畫素佔的位元位數 | 0x0018 |
30 | 4 | 保留欄位,一般為 0 | 0x000000 |
34 | 4 | 畫素資料佔用的總位元組數 | 0x000ac7c4 |
38 | 4 | 保留欄位,一般為 0 | 0x000000 |
42 | 4 | 保留欄位,一般為 0 | 0x000000 |
46 | 4 | 保留欄位,一般為 0 | 0x000000 |
50 | 4 | 保留欄位,一般為 0 | 0x000000 |