美文网首页
Android平台下的图片/视频转Ascii码图片/视频 (一)

Android平台下的图片/视频转Ascii码图片/视频 (一)

作者: LineCutFeng | 来源:发表于2018-09-13 14:21 被阅读0次

        前一阵看鸿洋公众号日推,看到一个几年前就感觉有意思的一个技术,那就是图片转Ascii码,记得上大学时玩过windows的图片或视频转ascii码,可惜那个软件不好用,有bug,转视频的时候动不动就卡死,5分钟的视频,转码百分之7,8十的时候有一半概率卡死- -,总有意犹未尽的感觉。

        去年的时候,自己从java移植过一种算法到android,大概思路如下:首先固定字号,然后计算这个字号下绘制出一个字母需要的像素(长x宽),然后对于图片:取出同等大小的图片碎片,然后列出每一个备选的字母绘制出来以后的像素rgb值(一般是ascii码,当然也可以是汉字,不过肯定效果不好),计算每个替换字的rgb转灰色像素数组 相对 图片碎片像素数组的标准差(还有几个备选算法不记得了,这不是重点~),标准差最小的,作为图片碎片的替换字。最后像国际象棋格子一样,一块一块的替换掉,由于计算相对比较复杂,所以耗时比较长,因此当时那个demo也让我搁置了。最近看到这篇日推,不由得眼前一亮,因为很少有人在android端做这种东西,因为算法方案是一大堆,不过很少有感兴趣的人去移植到android- -,我就参考了这篇文章的方案,不由得赞叹这个方法的巧妙,避免了大量的计算,图片转化率大大提高了,可以看看效果图 :


    ccg和修政

        哈哈哈,是不是很酷炫?为了看清每一个字母,特意上传了一个大图(ps:抖音上竟然有人手动敲的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()
    //                .theme()//主题样式(不设置为默认样式) 也可参考demo values/styles下 例如:R.style.picture.white.style
                    .maxSelectNum(1)// 最大图片选择数量 int
    //                .minSelectNum()// 最小选择数量 int
                    .imageSpanCount(4)// 每行显示个数 int
                    .selectionMode(PictureConfig.SINGLE)// 多选 or 单选 PictureConfig.MULTIPLE or PictureConfig.SINGLE
    //                .previewImage()// 是否可预览图片 true or false
    //                .previewVideo()// 是否可预览视频 true or false
    //                .enablePreviewAudio() // 是否可播放音频 true or false
                    .isCamera(true)// 是否显示拍照按钮 true or false
                    .imageFormat(PictureMimeType.PNG)// 拍照保存图片格式后缀,默认jpeg
                    .isZoomAnim(true)// 图片列表点击 缩放效果 默认true
                    .sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
    //                .setOutputCameraPath("/CustomPath")// 自定义拍照保存路径,可不填
    //                .enableCrop(true)// 是否裁剪 true or false
    //                .compress(false)// 是否压缩 true or false
    //                .glideOverride()// int glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
    //                .withAspectRatio(1, 1)// int 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
    //                .hideBottomControls()// 是否显示uCrop工具栏,默认不显示 true or false
    //                .isGif()// 是否显示gif图片 true or false
    //                .compressSavePath(context.getFilesDir().getAbsolutePath())//压缩图片保存地址
    //                .freeStyleCropEnabled(true)// 裁剪框是否可拖拽 true or false
    //                .circleDimmedLayer(true)// 是否圆形裁剪 true or false
    //                .showCropFrame(false)// 是否显示裁剪矩形边框 圆形裁剪时建议设为false   true or false
    //                .showCropGrid(false)// 是否显示裁剪矩形网格 圆形裁剪时建议设为false    true or false
                    .openClickSound(true)// 是否开启点击声音 true or false
    //                .selectionMedia()// 是否传入已选图片 List<LocalMedia> list
    //                .previewEggs()// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中) true or false
    //                .cropCompressQuality(90)// 裁剪压缩质量 默认90 int
                    .minimumCompressSize(500)// 小于100kb的图片不压缩
    //                .synOrAsy(true)//同步true或异步false 压缩 默认同步
    //                .cropWH()// 裁剪宽高比,设置如果大于图片本身宽高则无效 int
    //                .rotateEnabled() // 裁剪是否可旋转图片 true or false
    //                .scaleEnabled(true)// 裁剪是否可放大缩小图片 true or false
    //                .videoQuality()// 视频录制质量 0 or 1 int
    //                .videoMaxSecond(15)// 显示多少秒以内的视频or音频也可适用 int
    //                .videoMinSecond(10)// 显示多少秒以内的视频or音频也可适用 int
    //                .recordVideoSecond()//视频秒数录制 默认60s int
    //                .isDragFrame(false)// 是否可拖动裁剪框(固定)
                    .forResult(requestCode);//结果回调onActivityResult code
        }
    

        接着进行下一步操作,上代码:

    public static Bitmap createAsciiPic(final String path, Context context) {
            final String base = "#8XOHLTI)i=+;:,.";// 字符串由复杂到简单
    //        final String base = "#,.0123456789:;@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";// 字符串由复杂到简单
            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 < image.getHeight(); y += 2) {
                for (int x = 0; x < image.getWidth(); 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("\n");
            }
            return textAsBitmap(text, context);
    //        return image;
        }
    

        我来说下代码的意义~
        首先会得到屏幕宽高,接着正规操作,对图片进行缩放,如果图片大小过大,就对图片进行缩放,最大是屏幕的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.setARGB(0x31, 0x31, 0x31, 0);
    
            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);
    //        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//绘制透明色
            layout.draw(canvas);
            Log.d("textAsBitmap",      String.format("1:%d %d", layout.getWidth(), layout.getHeight()));
            return bitmap;
        }
    

        这里用到了StaticLayout去绘制文字,textpaint 设置单间隔的文字,设置好参数以后,在canvas上绘制,通过bitmap初始化的canvas,其实也会反应在bitmap上。(我一年前应该是没设置好这样的参数,所以当时画出来的ascii码图片,文字间隔比较大,当时就弃坑了)得到bitmap以后,可以显示在界面上了,也可以输出到文字里,对于图片转ascii码的步骤就到此为止了。

    接下来是视频转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 < encodeTotalCount; i++) {
                Log.i("icv", "第" + i + "张解码开始----------------\n");
                Bitmap bitmap = mediaDecoder.decodeFrame(i * (1000 / fps));
                if (bitmap == null) continue;
                Log.i("icv", "第" + i + "张解码结束\n");
                Log.i("icv", "第" + i + "张转换开始\n");
                if (weakReference == null || weakReference.get() == null) return;
                bitmapTemp = CommonUtil.createAsciiPic(bitmap, weakReference.get());
                Log.i("icv", "第" + i + "张转换结束\n");     
                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\n", argc);
        int i;
        for (i = 0; i < argc; i++) {
            jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
            argv[i] = (char*) (*env)->GetStringUTFChars(env, js, 0);
            LOGD("Kit argv %s\n", argv[i]);
        }
        return run(argc, argv);
    }
    
        而java拼成ffmpeg的命令的方法如下:
        public static String[] concatVideo(String _filePath, String _outPath,String fps) {//-f concat -i list.txt -c copy concat.mp4
            ArrayList<String> _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("-filter_complex");
    //            _commands.add("[1:v]scale=1920:1080[s];[0:v][s]overlay=0:0");
                _commands.add("-b");
                _commands.add("1000k");
    //            _commands.add("-s");
    //            _commands.add("640x360");
                _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.size(); 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

    这个demo的不足以及以后将会改进的地方:

    1. 视频分割成图片使用的是系统的api,并没有,相当于重复调用android native的接口,反复的创建,销毁资源,耗时比较多。过一阵将会改成使用ffmpeg来进行帧分解,我已经跑过单独的测试demo,效率是目前的10倍 - -。
    2. 以后会增加彩色ascii码的功能,现在是黑白的ascii码,其实在图片成ascii码图片之后,再增加一步就行了,和原先的图片进行相交处理,如果是黑色的,就取原先图片的彩色rgb,如果是白色的,就不做处理。
      目前支持视频avi,mp4等常见格式转化成avi,mp4,gif。后续会支持gif转ascii 的gif或视频。

    项目地址:https://github.com/LineCutFeng/PlayPicdio
    欢迎star,你的收藏是我更新的动力

    系列文章:
    Android平台下的图片/视频转Ascii码图片/视频 (一)
    Android平台下的图片/视频转Ascii码图片/视频 (二)

    参考文章:
    在Android中使用FFmpeg(android studio环境)
    极乐净土----Android实现图片转ascii码字符图的一些尝试
    android_图片转视频_image2video

    相关文章

      网友评论

          本文标题:Android平台下的图片/视频转Ascii码图片/视频 (一)

          本文链接:https://www.haomeiwen.com/subject/hgfwgftx.html