作者:Line_cut_feng
連結:https://www.jianshu.com/p/a14f1ac558e1
概述
看到一個幾年前就感覺有意思的一個技術,那就是圖片轉Ascii碼,記得上大學時玩過windows的圖片或影片轉ascii碼,可惜那個軟體不好用,有bug,轉影片的時候動不動就卡死,5分鐘的影片,轉碼百分之7,8十的時候有一半機率卡死- -,總有意猶未盡的感覺。
去年的時候,自己從java移植過一種演演算法到android,大概思路如下:
首先固定字號,然後計算這個字號下繪製出一個字母需要的畫素(長x寬),然後對於圖片:取出同等大小的圖片碎片,然後列出每一個備選的字母繪製出來以後的畫素rgb值(一般是ascii碼,當然也可以是漢字,不過肯定效果不好),計算每個替換字的rgb轉灰色畫素陣列 相對 圖片碎片畫素陣列的標準差(還有幾個備選演演算法不記得了,這不是重點~),標準差最小的,作為圖片碎片的替換字。
最後像國際象棋格子一樣,一塊一塊的替換掉,由於計算相對比較複雜,所以耗時比較長,因此當時那個demo也讓我擱置了。
最近看到這篇日推,不由得眼前一亮,因為很少有人在android端做這種東西,因為演演算法方案是一大堆,不過很少有感興趣的人去移植到android- -,我就參考了這篇文章的方案,不由得贊嘆這個方法的巧妙,避免了大量的計算,圖片轉化率大大提高了,可以看看效果圖:
ccg和修政
哈哈哈,是不是很酷炫?
為了看清每一個字母,特意上傳了一個大圖(ps:抖音上竟然有人手動敲的ascii碼,而且敲了幾天,真是喪心病狂)。好了,下麵進入正題~
1、圖片轉ascii
巧婦難為無米之炊,既然要圖片/影片轉化 ascii碼,要有對應的媒體檔案,選擇一個圖片,相信每一個android開發者都或多或少有個趁手的圖片選擇庫,這裡使用了 ‘com.github.LuckSiege.PictureSelector:picture_library:v2.2.3‘,持續更新的庫,比較好用。
用法大概如下~
public static void choosePhoto(Activity context, int requestCode) {
PictureSelector.create(context)
.openGallery(PictureMimeType.ofAll())//全部.PictureMimeType.ofAll()、圖片.ofImage()、影片.ofVideo()、音訊.ofAudio()
.maxSelectNum(1)// 最大圖片選擇數量 int
.imageSpanCount(4)// 每行顯示個數 int
.selectionMode(PictureConfig.SINGLE)// 多選 or 單選 PictureConfig.MULTIPLE or PictureConfig.SINGLE
.isCamera(true)// 是否顯示拍照按鈕 true or false
.imageFormat(PictureMimeType.PNG)// 拍照儲存圖片格式字尾,預設jpeg
.isZoomAnim(true)// 圖片串列點選 縮放效果 預設true
.sizeMultiplier(0.5f)// glide 載入圖片大小 0~1之間 如設定 .glideOverride()無效
.openClickSound(true)// 是否開啟點選聲音 true or false
.minimumCompressSize(500)// 小於100kb的圖片不壓縮
.forResult(requestCode);//結果回呼onActivityResult code
}
接著進行下一步操作,上程式碼:
public static Bitmap createAsciiPic(final String path, Context context) {
final String base = "#8XOHLTI)i=+;:,.";// 字串由複雜到簡單
StringBuilder text = new StringBuilder();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
int width = dm.widthPixels;
int height = dm.heightPixels;
Bitmap image = BitmapFactory.decodeFile(path); //讀取圖片
int width0 = image.getWidth();
int height0 = image.getHeight();
int width1, height1;
int scale = 7;
if (width0 <= width / scale) {
width1 = width0;
height1 = height0;
} else {
width1 = width / scale;
height1 = width1 * height0 / width0;
}
image = scale(path, width1, height1); //讀取圖片
//輸出到指定檔案中
for (int y = 0; y 2) {
for (int x = 0; x final int pixel = image.getPixel(x, y);
final int r = (pixel & 0xff0000) >> 16, g = (pixel & 0xff00) >> 8, b = pixel & 0xff;
final float gray = 0.299f * r + 0.578f * g + 0.114f * b;
final int index = Math.round(gray * (base.length() + 1) / 255);
String s = index >= base.length() ? " " : String.valueOf(base.charAt(index));
text.append(s);
}
text.append("
");
}
return textAsBitmap(text, context);
}
我來說下程式碼的意義~
首先會得到螢幕寬高,接著正規操作,對圖片進行縮放,如果圖片大小過大,就對圖片進行縮放,最大是螢幕的1/7,接著就是for迴圈巢狀長寬,這裡為什麼y是y+=2呢?因為ascii碼一般都比較長吧~,按照android的標準來看ascii碼繪製出來的效果比較長。
我們看for迴圈裡面做了什麼:對拿到的每個畫素點進行灰度轉化,這裡就用到影象學的知識了,為什麼是0.229:0.578:0.114呢?因為據研究(不是我研究的~),按照這樣的配比rgb轉化以後,人眼看到的是灰度影象。。。。。開個玩笑,這就是rgb轉灰度的公式之一。然後根據灰度值,在0到255之間的位置,來配對應的ascii碼,這裡 final String base = “#8XOHLTI)i=+;:,.”;(字串由複雜到簡單) 所謂的簡單到複雜其實想的不用那麼複雜,就是相同體積內,繪製出這些字母,哪一個黑色畫素更多,僅此而已。直到遍歷所有的畫素點以後,拼成一個Stringbuffer,這裡每次讀取一個width的畫素以後都要加上一個換行以區分一行。接著放到一個text轉bitmap的方法裡:
public static Bitmap textAsBitmap(StringBuilder text, Context context) {
TextPaint textPaint = new TextPaint();
textPaint.setColor(Color.BLACK);
textPaint.setAntiAlias(true);
textPaint.setTypeface(Typeface.MONOSPACE);
textPaint.setTextSize(12);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
int width = dm.widthPixels;
StaticLayout layout = new StaticLayout(text, textPaint, width,
Layout.Alignment.ALIGN_CENTER, 1f, 0.0f, true);
Bitmap bitmap = Bitmap.createBitmap(layout.getWidth() + 20,
layoutgetHeight() + 20, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.translate(10, 10);
canvas.drawColor(Color.WHITE);
layout.draw(canvas);
return bitmap;
}
這裡用到了StaticLayout去繪製文字,textpaint 設定單間隔的文字,設定好引數以後,在canvas上繪製,透過bitmap初始化的canvas,其實也會反應在bitmap上。(我一年前應該是沒設定好這樣的引數,所以當時畫出來的ascii碼圖片,文字間隔比較大,當時就棄坑了)得到bitmap以後,可以顯示在介面上了,也可以輸出到文字裡,對於圖片轉ascii碼的步驟就到此為止了。
2、影片轉ascii 碼
其實影片可看做是一幀一幀的圖片,那麼接下來的思路就清晰了吧~
首先將影片抓幀,可以按照你設定好的每秒抓多少幀,這樣得到一堆影象序列。而這裡得到影片幀用到了android原生的api,需要android5.0以上:MediaMetadataRetriever 這個類可以得到影片的時長,以及第多少毫秒的圖片預覽幀。於是我先拿到影片的時長,比如10000毫秒,也就是10秒,那麼接下來如果我每秒要取15張圖片,那麼就每(1000/15)毫秒取一張預覽幀,直到10000毫秒為止,首先需要強調下,這個操作是十分耗時的,因此必須將這個操作放到執行緒裡將這些圖片儲存到一個路徑下,具體程式碼如下(MediaDecoder是對於MediaMetadataRetriever 稍微封裝了一下)
@Override
public void run() {
mediaDecoder = new MediaDecoder(path);
String videoFileLength = mediaDecoder.getVideoFileLength();
if (videoFileLength != null) {
try {
int length = Integer.parseInt(videoFileLength);
encodeTotalCount = length / (1000 / fps);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
for (int i = 0; i Log.i("icv", "第" + i + "張解碼開始----------------
");
Bitmap bitmap = mediaDecoder.decodeFrame(i * (1000 / fps));
if (bitmap == null) continue;
Log.i("icv", "第" + i + "張解碼結束
");
Log.i("icv", "第" + i + "張轉換開始
");
if (weakReference == null || weakReference.get() == null) return;
bitmapTemp = CommonUtil.createAsciiPic(bitmap, weakReference.get());
Log.i("icv", "第" + i + "張轉換結束
");
FileOutputStream fos;
try {
String format = String.format("%05d", i);
fos = new FileOutputStream(MainActivity.picListPath + File.separator + "test" + format + ".png", false);
bitmapTemp.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
if (onEncoderListener != null) {
onEncoderListener.onProgress(((int) (100 * ((float) i / encodeTotalCount))));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
mHandler.post(new Runnable() {
@Override
public void run() {
if (onEncoderListener != null) {
onEncoderListener.showImg(bitmapTemp);
}
}
});
}
Log.i("icv", "處理完成");
mHandler.post(new Runnable() {
@Override
public void run() {
if (onEncoderListener != null) {
onEncoderListener.onComplish();
}
}
});
}
這裡我直接儲存的轉換成ascii碼圖片之後的檔案了,圖片轉ascii碼的步驟見文章上半部分。
接下來就是最後一步了,將分割轉換的圖片再合成成影片,合成影片的方法我網上也找了很多,不過基本都是2個方式:第一個就是javacodec這個庫,可是這個庫發現控制不了幀率,也就是說一個影片如果你轉化成圖片設定的fps比較少的話,比如fps=5,那麼合成影片的時候,他會按照fps = 25預設的去合成影片,那麼會出現的問題就是合成的影片的播放速度會是原先的5倍- -,當然也可以改這個庫的原始碼,不過因為這個專案以後還有可能加其他的好玩的功能,於是選擇了第二種方案。第二種方案:用ffmpeg進行合成,ffmpeg是一個用c寫的跨平臺的影片處理庫,裡麵包含了強大的,影片編解碼,推流,加水印,濾鏡等強大的功能,這也是我選擇它的原因,由於編譯ffmpeg也是個大坑,所以直接拿來了別人編好的移植過來了。
這裡使用了ffmpeg庫裡ffmpeg.c的run方法去跑你拼接的命令,他也是透過java層傳遞過來一個陣列,這個陣列裝有ffmpeg的要執行的命令,再傳到jni裡,在這裡面變成一個char陣列傳遞到ffmpeg的run方法,,jni檔案如下:
JNIEXPORT jint JNICALL Java_codepig_ffmpegcldemo_FFmpegKit_run
(JNIEnv *env, jclass obj, jobjectArray commands){
//FFmpeg av_log() callback
int argc = (*env)->GetArrayLength(env, commands);
char *argv[argc];
LOGD("Kit argc %d
", argc);
int i;
for (i = 0; i jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
argv[i] = (char*) (*env)->GetStringUTFChars(env, js, 0);
LOGD("Kit argv %s
", argv[i]);
}
return run(argc, argv);
}
而java拼成ffmpeg的命令的方法如下:
public static String[] concatVideo(String _filePath, String _outPath,String fps){
ArrayList _commands = new ArrayList<>();
{
_commands.add("ffmpeg");
_commands.add("-f");
_commands.add("image2");
_commands.add("-framerate");
_commands.add(fps);
_commands.add("-i");
_commands.add(_filePath+"/test%05d.png");
_commands.add("-b");
_commands.add("1000k");
_commands.add("-ss");
_commands.add("0:00:00");
_commands.add("-r");
_commands.add("50");
_commands.add(_outPath);
}
String[] commands = new String[_commands.size()];
String _pr = "";
for (int i = 0; i commands[i] = _commands.get(i);
_pr += commands[i];
}
Log.d("LOGCAT", "ffmpeg command:" + _pr + "-" + commands.length);
return commands;
}
簡略的說下各種引數 -f是他規定的圖片格式,-framerate就是幀率啦,fps就是一個int值,一般5到25都行,太少會影響影片的流暢,太多會導致影片播放過快,當然這個fps一定要和當時分割成圖片的fps是一模一樣的,當時分割的如果太細,會導致後來合成影片的檔案過大,因為按照視覺殘留原理,15fps就會看做是連續的畫面了,無停頓感。這裡我預設選擇5fps是因為200毫秒取一幀省時間,幀數少,一會轉化影片耗時時間少啊。-i表示輸入的媒體檔案,一般是avi或mp4的影片.-b是位元速率,這個可以設定小一點,就是1秒的媒體所佔的大小限制,-ss是開始的時間,-r是輸出的幀率控制,這裡是硬控制,這裡我設定個大於framerate的數就行了,拼好命令以後,就可以傳給ffmpeg進行合成了。合成過程比較慢,因為一涉及到影片處理一般都會慢,靜靜等待執行完之後就行了,到對應目錄上檢視合成之後的檔案。
效果圖如下:
fzk.gif
3、不足與改進
這個demo的不足以及以後將會改進的地方:
1、影片分割成圖片使用的是系統的api,並沒有,相當於重覆呼叫android native的介面,反覆的建立,銷毀資源,耗時比較多。過一陣將會改成使用ffmpeg來進行幀分解,我已經跑過單獨的測試demo,效率是目前的10倍 – -。
2、以後會增加彩色ascii碼的功能,現在是黑白的ascii碼,其實在圖片成ascii碼圖片之後,再增加一步就行了,和原先的圖片進行相交處理,如果是黑色的,就取原先圖片的彩色rgb,如果是白色的,就不做處理。
目前支援影片avi,mp4等常見格式轉化成avi,mp4,gif。後續會支援gif轉ascii 的gif或影片。
專案地址:
https://github.com/GodFengShen/PicOrVideoToAscii
歡迎star,你的收藏是我更新的動力。
●編號370,輸入編號直達本文
●輸入m獲取到文章目錄
Java程式設計
更多推薦《18個技術類公眾微信》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。