歡迎光臨
每天分享高質量文章

抖音上很火的字符畫 Android 實現 |  視頻轉換實現

作者: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, 1f0.0ftrue);
    Bitmap bitmap = Bitmap.createBitmap(layout.getWidth() + 20,
    layoutgetHeight() + 20, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.translate(1010);
   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 == nullcontinue;
        Log.i("icv""第" + i + "張解碼結束 ");
        Log.i("icv""第" + i + "張轉換開始 ");
        if (weakReference == null || weakReference.get() == nullreturn;
        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、資料庫、運維等。

赞(0)

分享創造快樂