Android 實現 視頻 轉 字符畫效果

2021-02-13 秦子帥

上一篇文章我們講到圖片轉字符畫,這篇文章要實現 視頻 轉 字符畫效果。

我們看一下實現出來的效果圖:

效果圖有點糊,原文的效果圖會更好

實現的效果還是讓人挺滿意的。我們下面說一下具體的實現步驟,

我們分開一步一步的講:

視頻取幀

視頻取幀的整個功能最麻煩的一步,目前Android視頻取幀的方法有好幾種。其中有使用SDK自帶的MediaMetadataRetriever直接獲取bimap的,但是缺點就是慢。

也有使用強大的FFmpeg庫的,但是需要針對編譯不同架構的CPU編譯不同的so文件十分的麻煩。

也有人推薦使用一個名為Jcodec的庫,開發效率上來說這個工具確實十分的好,但是運行起來真的十分的慢,我寫了個Demo取一幀大概要我4s的時間(測試手機是Redmi note 7 pro),所以只用他的視頻合成功能(雖然仍然很慢具體的解決辦法還沒找到)。

後來在別的大佬博客裡面找到一篇使用原生接口MediaCodec硬解碼視頻的文章,用該方法取幀完美解決對不同機型的兼容性問題,因為使用的原生接口速度也是可以保證的。

但主要的問題點是 MediaCodec 解碼返回的幀圖片數據是YUV格式的,它跟我們平時使用的 RGB 格式很不一樣的是它的三個值表示的是亮度,色度,飽和度。

YUV下也分不同的格式分別有:Y'UV, YUV, YCbCr,YPbPr等,安卓設備因為 API 21 統一的原因都能使用 COLOR_FormatYUV420Flexible 格式,使得 MediaCodec 的所有硬體解碼都支持這種格式。

但這樣解碼後得到的 YUV420 的具體格式又會因設備而異,有:YUV420Planar,YUV420SemiPlanar,YUV420PackedSemiPlanar 等,我們可以使用 Image 類來處理這些格式統一處理向 NV21 進行轉換

然後我們可以對 Image 類進行轉換成 Bitmap,再對 Bimap 的進行像素轉換成字符數組再繪製成圖片保存作為轉換字符畫視頻 的其中一幀。

具體實現,首先我們在解碼的過程的中需要獲取設備是否支持 COLOR_FormatYUV420Flexible 幀格式,然後初始化幾個重要的對象:

...
MediaExtractor extractor = null;
MediaFormat mediaFormat = null;
MediaCodec decoder = null;

extractor = initMediaExtractor(file);//使用視頻文件對象初始化extractor
mediaFormat = initMediaFormat(videoPath, extractor);
decoder = initMediaCodec(mediaFormat);
//初始化解碼配置
decoder.configure(mediaFormat, null, null, 0);
decoder.start();
//開始解碼
...

static private MediaExtractor initMediaExtractor(File path) throws IOException {
        MediaExtractor extractor = null;
        extractor = new MediaExtractor();
        extractor.setDataSource(path.toString());
        return extractor;
    }

static private MediaFormat initMediaFormat(String path, MediaExtractor extractor) {
        //選擇解碼通道
        int trackIndex = selectTrack(extractor);
        if (trackIndex < 0) {
            throw new RuntimeException("No video track found in " + path);
        }
        extractor.selectTrack(trackIndex);
        MediaFormat mediaFormat = extractor.getTrackFormat(trackIndex);
        return mediaFormat;
    }

static public MediaCodec initMediaCodec(MediaFormat mediaFormat) throws IOException {
        MediaCodec decoder = null;
        String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
        decoder = MediaCodec.createDecoderByType(mime);
        //showSupportedColorFormat(decoder.getCodecInfo().getCapabilitiesForType(mime));
        if (isColorFormatSupported(decodeColorFormat, decoder.getCodecInfo().getCapabilitiesForType(mime))) {
            //  設置 解碼格式        
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, decodeColorFormat);
        } else {
        }
        return decoder;
    }

初始化完成後我們可以利用這三個對象進行關鍵幀獲取:

   private static Bitmap getBitmapBySec(MediaExtractor extractor, MediaFormat mediaFormat, MediaCodec decoder, long sec) throws IOException {

        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        Bitmap bitmap = null;
        boolean sawInputEOS = false;
        boolean sawOutputEOS = false;
        boolean stopDecode = false;
        final int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
        final int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
        long presentationTimeUs = -1;
        int outputBufferId;
        Image image = null;

        //視頻定位到指定的時間的上一幀
        extractor.seekTo(sec, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);

        //因為extractor定位的幀不是準確的,所以我們要用一個循環不停讀取下一幀來獲取我們想要的時間畫面。
        while (!sawOutputEOS && !stopDecode) {
            if (!sawInputEOS) { 
                int inputBufferId = decoder.dequeueInputBuffer(-1);                
                if (inputBufferId >= 0) {
                    ByteBuffer inputBuffer = decoder.getInputBuffer(inputBufferId);
                    int sampleSize = extractor.readSampleData(inputBuffer, 0);
                    if (sampleSize < 0) {                        
                        decoder.queueInputBuffer(inputBufferId, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                        sawInputEOS = true;
                    } else {
                        //獲取定位的幀的時間
                        presentationTimeUs = extractor.getSampleTime();                       
                        //把定位的幀壓入隊列
                        decoder.queueInputBuffer(inputBufferId, 0, sampleSize, presentationTimeUs, 0);
                        //跳到下一幀
                        extractor.advance();
                    }
                }
            }
            outputBufferId = decoder.dequeueOutputBuffer(info, DEFAULT_TIMEOUT_US);
            if (outputBufferId >= 0) {
                //能夠有效輸出
                if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 | presentationTimeUs >= sec) {
                    //時間是指定時間或者已經是視頻結束時間,停止循環
                    sawOutputEOS = true;
                    boolean doRender = (info.size != 0);
                    if (doRender) {
                        //獲取指定時間解碼出來的Image對象。
                        image = decoder.getOutputImage(outputBufferId);
                        //將Image轉換成Bimap
                        stream.close();
                        image.close();
                    }
                }
                decoder.releaseOutputBuffer(outputBufferId, true);
            }
        }

        return bitmap;
    }

獲取到了幀畫面數據,下面我們可以做關於 Image 對 Bimap 的轉換,主要是用到 YuvImage 這個類,在使用 YuvImage 這個類前需要把 YUV_420_888 的編碼格式轉成 NV21 格式:

...
image = decoder.getOutputImage(outputBufferId);
YuvImage yuvImage = new YuvImage(YUV_420_888toNV21(image), ImageFormat.NV21, width, height, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, stream);
bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
...
 private static byte[] YUV_420_888toNV21(Image image) {
        Rect crop = image.getCropRect();
        int format = image.getFormat();
        int width = crop.width();
        int height = crop.height();
        Image.Plane[] planes = image.getPlanes();
        byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
        byte[] rowData = new byte[planes[0].getRowStride()];
        //if (VERBOSE) Log.v("YUV_420_888toNV21", "get data from " + planes.length + " planes");
        int channelOffset = 0;
        int outputStride = 1;
        for (int i = 0; i < planes.length; i++) {
            switch (i) {
                case 0:
                    channelOffset = 0;
                    outputStride = 1;
                    break;
                case 1:
                    channelOffset = width * height + 1;
                    outputStride = 2;

                    break;
                case 2:
                    channelOffset = width * height;
                    outputStride = 2;

                    break;
            }
            ByteBuffer buffer = planes[i].getBuffer();
            int rowStride = planes[i].getRowStride();
            int pixelStride = planes[i].getPixelStride();

            int shift = (i == 0) ? 0 : 1;
            int w = width >> shift;
            int h = height >> shift;
            buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
            for (int row = 0; row < h; row++) {
                int length;
                if (pixelStride == 1 && outputStride == 1) {
                    length = w;
                    buffer.get(data, channelOffset, length);
                    channelOffset += length;
                } else {
                    length = (w - 1) * pixelStride + 1;
                    buffer.get(rowData, 0, length);
                    for (int col = 0; col < w; col++) {
                        data[channelOffset] = rowData[col * pixelStride];
                        channelOffset += outputStride;
                    }
                }
                if (row < h - 1) {
                    buffer.position(buffer.position() + rowStride - length);
                }
            }
           // if (VERBOSE) Log.v("", "Finished reading data from plane " + i);
        }
        return data;
    }

這樣我們就能獲取到了幀圖片的 Bitmap 數據了,剩下的步驟都跟上一篇文章的圖片轉換差不多,當我們所有的幀都轉換完以後,我們就可以把這些圖片按順序合成視頻了,這裡我調用的是上面提到的 Jcodec 這個工具,它有支持圖片合成視頻的功能,代碼如下:

 static public String convertVideoBySourcePics(Context context, String picsDri) {
        SeekableByteChannel out = null;
        //找到輸出目錄,沒有的話創建
        File destDir = new File(Environment.getExternalStorageDirectory() + "/FunVideo_Video");
        if (!destDir.exists()) {
            destDir.mkdirs();
        }
        //新建輸出文件
        File file = new File(destDir.getPath() + "/funvideo_" + System.currentTimeMillis() + ".mp4");
        try {
            file.createNewFile();
            // for Android use: AndroidSequenceEncoder
            File _piscDri = new File(picsDri);
            //創建編碼對象
            AndroidSequenceEncoder encoder = AndroidSequenceEncoder.createSequenceEncoder(file, 5);
            for (File childFile : _piscDri.listFiles()) {
                Bitmap bitmap = BitmapUtils.getBitmapByUri(context, Uri.fromFile(childFile));
                encoder.encodeImage(bitmap);
                bitmap.recycle();
            }
            //結束編碼
            encoder.finish();
            //通知系統添加了視頻文件。
            Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
            Uri contentUri = Uri.fromFile(file);
            mediaScanIntent.setData(contentUri);
            context.sendBroadcast(mediaScanIntent);
           //Log.i("addGraphToGallery", "ok");
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            NIOUtils.closeQuietly(out);
        }
        //轉化完成輸出文件路徑
        return file.getPath();
    }

調用 Jcodec 的轉換如果視頻在 15s 以內轉換的效率還是可以的,大於 15s 的視頻轉換就會變得十分的慢,可能是我自己的原因也可能是這個工具本來也存在一些優化的問題。

鑑於上面的視頻解碼取幀,最好的視頻編碼合成當然也是用原生的 MediaMetadataRetriever 來做。

思路大概跟上面的方法反著來,看著是不是很清晰了,具體實現方法我就不細說了,因為我也還沒做,後面會基於這個思路來優化合成視頻這一模塊。

字符畫轉換的全部內容大概都到這裡了,謝謝大家閱讀,喜歡的話可以給個贊。

完整項目源碼地址:

https://github.com/452kinton/CharacterDance

作者:Kinton

來源:https://www.jianshu.com/p/16ef3bf9ac5c

相關焦點

  • 圖片轉字符畫
    運行平臺: Windows  Python版本: Python3.6  IDE: Sublime Text一、實驗原理字符畫是一系列字符的組合,我們可以把字符看作是比較大塊的像素,一個字符能表現一種顏色(暫且這麼理解吧),字符的種類越多,可以表現的顏色也越多,圖片也會更有層次感。
  • Python 實現圖片轉字符畫,靜態圖、GIF 都能轉
    字符畫是一種由字母、標點或其他字符組成的圖畫,它產生於網際網路時代,在聊天軟體中使用較多,本文我們看一下如何將自己喜歡的圖片轉成字符畫。靜態圖片首先,我們來演示將靜態圖片轉為字符畫,功能實現主要用到的 Python 庫為 OpenCV,安裝使用 pip install opencv-python 命令即可。功能實現的基本思路為:利用聚類將像素信息聚為 3 或 5 類,顏色最深的一類用數字密集度表示,陰影的一類用橫槓(-)表示,明亮部分用空白表示。
  • Android 實現一個立方體旋轉效果
  • Android 兩種方式實現類似水波擴散效果
    兩種方式實現類似水波擴散效果,先上圖為敬自定義view實現動畫實現自定義view實現思路分析:通過canvas畫圓,每次改變圓半徑和透明度,當半徑達到一定程度,再次從中心開始繪圓,達到不同層級的效果,通過不斷繪製達到view擴散效果private Paint centerPaint; private int
  • Android之AppBarLayout實現懸停吸附伸縮效果
    前幾天看到這樣一個UI效果,然後自己也仿照實現了下:開眼app個人中心看著挺酷的,也有很多App都用到了這個UI效果,比如開眼App和滬江開心詞場就用到了.所以下面就來簡單實現一下這個UI效果吧.組合三劍客1.AppBarLayout2.CoordinatorLayout3.CollapsingToolbarLayout實現上面的UI效果需要將這三劍客的組合起來用,下面先介紹下這三個控制項:AppBarLayout:1.AppBarLayout簡單介紹AppBarLayout是android.support:design包中的支持的控制項
  • Android實現界面切換時的共享動畫效果
    最近看到一個項目上的界面切換時的過渡效果很炫,決定實現一下,先放上效果圖:效果就是在跳轉到另一個Activity時
  • 用Python把圖片轉成字符畫
    自己畫是肯定看不出的。然而幼稚的我們被騙了原圖是這樣的通過下面的代碼生成了字符畫,什麼原理呢?我們知道顯示器是由一個個像素點組成的,每個像素點可以顯示不同的顏色,這樣就可以顯示彩色的照片。最早的黑白顯示器雖然只能顯示白和黑,但因為亮度的不同就可以顯示信息較豐富的黑白圖像,這個就是灰度值。
  • Android實現人臉識別效果
    收錄於話題 #androidnull){ int dstWidthAndHeight = (int) (getWidth() / 1.5f + getWidth() / 1.5f / 4); mOutCircleBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_checkface_outcircle); m
  • 厲害了,用Android自定義View實現八大行星繞太陽3D旋轉效果
    作者:史蒂芬諾夫斯基連結:https://www.jianshu.com/p/2954f2ef8ea5好久沒寫View了,最近恰巧遇到一個八大行星繞太陽旋轉的假3D效果,寫完之後感覺效果還不錯。能玩十分鐘的那種。
  • Android使用RecyclerViewHeader+ViewFlipper實現淘寶頭條滾動效果
    >這裡有兩個坑需要注意:3、如果想實現輪播時的效果,需要加上android:inAnimation="@anim/anim_marquee_in"android:outAnimation="@anim/anim_marquee_out" />anim_marquee_in:
  • Android使用ViewPager + Fragment實現無限滑動效果
    一、實現效果圖二、實現方式第一種是採用Adapter內的getCount()方法返回Integer.MAX_VALUE。
  • Android使用LabelsView實現標籤列表控制項功能
    這樣的效果用Android源生的控制項很不好實現,所以往往需要我們自己去自定義控制項。我在開發中就遇到過幾次要實現這樣的標籤列表效果,所以就自己寫了個控制項,下面我將為大家介紹該控制項的具體實現和使用。要實現這樣一個標籤列表其實並不難,列表中的item可以直接用TextView來實現,我們只需要關心列表控制項的大小和標籤的擺放就可以了。
  • Android實現豎著的滑動刻度尺效果,選擇身高(豎向的)
    點擊「閱讀原文」,可查看更多內容和乾貨這次是你想要的效果哦!群裡有人問我要豎著的滑動尺效果,前天我賤賤地分享了一個橫向的滑動效果,讓大家模仿者,自己嘗試著去改編一下,不知道有多少人弄出來了,嘗試著去弄得請舉手,好吧,我也不用說放下了,根本就沒人舉。我再說一句廢話吧,努力去嘗試,才會有進步,不要坐等著別人來幫助你,這樣你會餓死的,因為很多人不會像我一樣去給你分享。
  • Android 界面高亮,如何優雅實現呢?
    預期效果:在不使用純圖的前提下實現一個全屏的蒙層上制定的一個或者多個View的高亮最初嘗試的方案A:首先在整個界面畫出一個半透明的全屏蒙層通過View.getDrawingCache() 獲取該目標View的bitmap
  • Android仿全歷史——全沉浸時間軸實現
    今天良心發現,來更一個比較有意思的東西,如題——仿全歷史APP的全沉浸時間軸實現。這張圖呢,就是全歷史App中全古古蹟功能的界面圖。顯然,它通過沉浸狀態欄、透明背景、recyclerView的自定義Item等,實現了一個很優秀的界面效果。今天,我要做的就是猜測這效果背後的實現原理,並仿製一個類似的界面正文解構
  • Android實現帶有粘性頭部的ScrollView
    前言一天在點外賣的時候,注意到餓了麼列表頁的滑動效果不錯,但是覺得其中的手勢滑動還是挺複雜的,正好又碰到了在熟悉Touch事件的理解當中,所以就抽空對著餓了麼的列表頁面嘗試寫寫這個效果1.先貼一個實現的效果圖邏輯是當外部的ScrollView沒有滑到底部的時候,往上滑動的時候,是滑動外部的ScrollView,當外部的ScrollView到達底部的時候,我們再網上滑
  • Android實現仿淘寶物流追蹤功能
    今天我們來聊聊物流追蹤效果,效果圖如下,有需要的朋友,可以直接帶走,實現也沒有想像中的那麼複雜,特別是左邊那個時間軸線,沒那麼複雜拿到這個圖,大家首先想到的是這是一個
  • Android自定義實現酷炫的提交完成按鈕
    地址 |  https://www.jianshu.com/p/68101033d2b7有個需求是做一個點擊完成的動畫,所以就運用上了,效果如下
  • 用MotionLayout實現這些不可思議的效果
    本文你將學到使用代碼操控和其它組件配合使用仿華為撥號界面動畫效果Android 11 彩蛋製作使用代碼操控為什麼要用代碼控制轉場呢,xml寫著不香嗎😜 ?xml寫著很方便,但是有時我們需要動態的改變轉場的效果,就需要通過代碼來實現了。
  • 技術一面:說說Android動態換膚實現原理
    我們只需要實現我們的Factory然後設置給mFactory2就可以採集到所有的View了,這裡是一個Hook點。當我們採集完了需要換膚的view,下一步就是加載皮膚包資源。當我們拿到當前View的資源名稱時就會先去皮膚插件中的資源文件裡找Android加載資源的流程圖:1.採集換膚控制項android解析xml創建view的步驟:所以我們複寫了Factory的onCreateView之後,就可以不通過系統層而是自己截獲從xml映射的View進行相關View創建的操作,包括對View的屬性進行設置(比如背景色,字體大小,顏色等