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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u010308894/article/details/102769197

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

去年的时候,自己从java移植过一种算法到android,大概思路如下:首先固定字号,然后计算这个字号下绘制出一个字母需要的像素(长x宽),然后对于图片:取出同等大小的图片碎片,然后列出每一个备选的字母绘制出来以后的像素rgb值(一般是ascii码,当然也可以是汉字,不过肯定效果不好),计算每个替换字的rgb转灰色像素数组 相对 图片碎片像素数组的标准差(还有几个备选算法不记得了,这不是重点~),标准差最小的,作为图片碎片的替换字。最后像国际象棋格子一样,一块一块的替换掉,由于计算相对比较复杂,所以耗时比较长,因此当时那个demo也让我搁置了。最近看到这篇日推,不由得眼前一亮,因为很少有人在android端做这种东西,因为算法方案是一大堆,不过很少有感兴趣的人去移植到android- -,我就参考了这篇文章的方案,不由得赞叹这个方法的巧妙,避免了大量的计算,图片转化率大大提高了,可以看看效果图 :
    在这里插入图片描述
    在这里插入图片描述
    哈哈哈,是不是很酷炫?为了看清每一个字母,特意上传了大图(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进行合成了。合成过程比较慢,因为一涉及到视频处理一般都会慢,静静等待执行完之后就行了,到对应目录上查看合成之后的文件。

效果图如下:
在这里插入图片描述
最后说下这个demo的不足以及以后将会改进的地方:

  1. 视频分割成图片使用的是系统的api,并没有,相当于重复调用android native的接口,反复的创建,销毁资源,耗时比较多。过一阵将会改成使用ffmpeg来进行帧分解,我已经跑过单独的测试demo,效率是目前的10倍 - -。

  2. 以后会增加彩色ascii码的功能,现在是黑白的ascii码,其实在图片成ascii码图片之后,再增加一步就行了,和原先的图片进行相交处理,如果是黑色的,就取原先图片的彩色rgb,如果是白色的,就不做处理。

目前支持视频avi,mp4等常见格式转化成avi,mp4,gif。后续会支持gif转ascii 的gif或视频。
系列文章:
Android平台下的图片/视频转Ascii码图片/视频 (一)
Android平台下的图片/视频转Ascii码图片/视频 (二)

项目地址:https://github.com/LineCutFeng/PlayPicdio

猜你喜欢

转载自blog.csdn.net/u010308894/article/details/102769197
今日推荐