????前一陣看鴻洋公眾號(hào)日推摆寄,看到一個(gè)幾年前就感覺(jué)有意思的一個(gè)技術(shù),那就是圖片轉(zhuǎn)Ascii碼,記得上大學(xué)時(shí)玩過(guò)windows的圖片或視頻轉(zhuǎn)ascii碼排截,可惜那個(gè)軟件不好用,有bug辐益,轉(zhuǎn)視頻的時(shí)候動(dòng)不動(dòng)就卡死断傲,5分鐘的視頻,轉(zhuǎn)碼百分之7智政,8十的時(shí)候有一半概率卡死- -认罩,總有意猶未盡的感覺(jué)。
????去年的時(shí)候续捂,自己從java移植過(guò)一種算法到android垦垂,大概思路如下:首先固定字號(hào),然后計(jì)算這個(gè)字號(hào)下繪制出一個(gè)字母需要的像素(長(zhǎng)x寬)牙瓢,然后對(duì)于圖片:取出同等大小的圖片碎片劫拗,然后列出每一個(gè)備選的字母繪制出來(lái)以后的像素rgb值(一般是ascii碼,當(dāng)然也可以是漢字矾克,不過(guò)肯定效果不好)页慷,計(jì)算每個(gè)替換字的rgb轉(zhuǎn)灰色像素?cái)?shù)組 相對(duì) 圖片碎片像素?cái)?shù)組的標(biāo)準(zhǔn)差(還有幾個(gè)備選算法不記得了,這不是重點(diǎn)~),標(biāo)準(zhǔn)差最小的胁附,作為圖片碎片的替換字酒繁。最后像國(guó)際象棋格子一樣,一塊一塊的替換掉控妻,由于計(jì)算相對(duì)比較復(fù)雜州袒,所以耗時(shí)比較長(zhǎng),因此當(dāng)時(shí)那個(gè)demo也讓我擱置了弓候。最近看到這篇日推郎哭,不由得眼前一亮他匪,因?yàn)楹苌儆腥嗽赼ndroid端做這種東西,因?yàn)樗惴ǚ桨甘且淮蠖芽溲校贿^(guò)很少有感興趣的人去移植到android- -邦蜜,我就參考了這篇文章的方案,不由得贊嘆這個(gè)方法的巧妙陈惰,避免了大量的計(jì)算畦徘,圖片轉(zhuǎn)化率大大提高了,可以看看效果圖 :
????哈哈哈抬闯,是不是很酷炫井辆?為了看清每一個(gè)字母,特意上傳了一個(gè)大圖(ps:抖音上竟然有人手動(dòng)敲的ascii碼溶握,而且敲了幾天杯缺,真是喪心病狂)。好了睡榆,下面進(jìn)入正題~
????巧婦難為無(wú)米之炊萍肆,既然要圖片/視頻轉(zhuǎn)化 ascii碼,要有對(duì)應(yīng)的媒體文件胀屿,選擇一個(gè)圖片塘揣,相信每一個(gè)android開(kāi)發(fā)者都或多或少有個(gè)趁手的圖片選擇庫(kù),這里使用了 'com.github.LuckSiege.PictureSelector:picture_library:v2.2.3'宿崭,持續(xù)更新的庫(kù)亲铡,比較好用。
????用法大概如下~
public static void choosePhoto(Activity context, int requestCode) {
PictureSelector.create(context)
.openGallery(PictureMimeType.ofAll())//全部.PictureMimeType.ofAll()葡兑、圖片.ofImage()奖蔓、視頻.ofVideo()、音頻.ofAudio()
// .theme()//主題樣式(不設(shè)置為默認(rèn)樣式) 也可參考demo values/styles下 例如:R.style.picture.white.style
.maxSelectNum(1)// 最大圖片選擇數(shù)量 int
// .minSelectNum()// 最小選擇數(shù)量 int
.imageSpanCount(4)// 每行顯示個(gè)數(shù) int
.selectionMode(PictureConfig.SINGLE)// 多選 or 單選 PictureConfig.MULTIPLE or PictureConfig.SINGLE
// .previewImage()// 是否可預(yù)覽圖片 true or false
// .previewVideo()// 是否可預(yù)覽視頻 true or false
// .enablePreviewAudio() // 是否可播放音頻 true or false
.isCamera(true)// 是否顯示拍照按鈕 true or false
.imageFormat(PictureMimeType.PNG)// 拍照保存圖片格式后綴,默認(rèn)jpeg
.isZoomAnim(true)// 圖片列表點(diǎn)擊 縮放效果 默認(rèn)true
.sizeMultiplier(0.5f)// glide 加載圖片大小 0~1之間 如設(shè)置 .glideOverride()無(wú)效
// .setOutputCameraPath("/CustomPath")// 自定義拍照保存路徑,可不填
// .enableCrop(true)// 是否裁剪 true or false
// .compress(false)// 是否壓縮 true or false
// .glideOverride()// int glide 加載寬高讹堤,越小圖片列表越流暢吆鹤,但會(huì)影響列表圖片瀏覽的清晰度
// .withAspectRatio(1, 1)// int 裁剪比例 如16:9 3:2 3:4 1:1 可自定義
// .hideBottomControls()// 是否顯示uCrop工具欄,默認(rèn)不顯示 true or false
// .isGif()// 是否顯示gif圖片 true or false
// .compressSavePath(context.getFilesDir().getAbsolutePath())//壓縮圖片保存地址
// .freeStyleCropEnabled(true)// 裁剪框是否可拖拽 true or false
// .circleDimmedLayer(true)// 是否圓形裁剪 true or false
// .showCropFrame(false)// 是否顯示裁剪矩形邊框 圓形裁剪時(shí)建議設(shè)為false true or false
// .showCropGrid(false)// 是否顯示裁剪矩形網(wǎng)格 圓形裁剪時(shí)建議設(shè)為false true or false
.openClickSound(true)// 是否開(kāi)啟點(diǎn)擊聲音 true or false
// .selectionMedia()// 是否傳入已選圖片 List<LocalMedia> list
// .previewEggs()// 預(yù)覽圖片時(shí) 是否增強(qiáng)左右滑動(dòng)圖片體驗(yàn)(圖片滑動(dòng)一半即可看到上一張是否選中) true or false
// .cropCompressQuality(90)// 裁剪壓縮質(zhì)量 默認(rèn)90 int
.minimumCompressSize(500)// 小于100kb的圖片不壓縮
// .synOrAsy(true)//同步true或異步false 壓縮 默認(rèn)同步
// .cropWH()// 裁剪寬高比洲守,設(shè)置如果大于圖片本身寬高則無(wú)效 int
// .rotateEnabled() // 裁剪是否可旋轉(zhuǎn)圖片 true or false
// .scaleEnabled(true)// 裁剪是否可放大縮小圖片 true or false
// .videoQuality()// 視頻錄制質(zhì)量 0 or 1 int
// .videoMaxSecond(15)// 顯示多少秒以?xún)?nèi)的視頻or音頻也可適用 int
// .videoMinSecond(10)// 顯示多少秒以?xún)?nèi)的視頻or音頻也可適用 int
// .recordVideoSecond()//視頻秒數(shù)錄制 默認(rèn)60s int
// .isDragFrame(false)// 是否可拖動(dòng)裁剪框(固定)
.forResult(requestCode);//結(jié)果回調(diào)onActivityResult code
}
????接著進(jìn)行下一步操作疑务,上代碼:
public static Bitmap createAsciiPic(final String path, Context context) {
final String base = "#8XOHLTI)i=+;:,.";// 字符串由復(fù)雜到簡(jiǎn)單
// final String base = "#,.0123456789:;@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";// 字符串由復(fù)雜到簡(jiǎn)單
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;
}
????我來(lái)說(shuō)下代碼的意義~
????首先會(huì)得到屏幕寬高,接著正規(guī)操作岖沛,對(duì)圖片進(jìn)行縮放暑始,如果圖片大小過(guò)大,就對(duì)圖片進(jìn)行縮放婴削,最大是屏幕的1/7,接著就是for循環(huán)嵌套長(zhǎng)寬,這里為什么y是y+=2呢牙肝?因?yàn)閍scii碼一般都比較長(zhǎng)吧~唉俗,按照android的標(biāo)準(zhǔn)來(lái)看ascii碼繪制出來(lái)的效果比較長(zhǎng)嗤朴。
????我們看for循環(huán)里面做了什么:對(duì)拿到的每個(gè)像素點(diǎn)進(jìn)行灰度轉(zhuǎn)化,這里就用到圖像學(xué)的知識(shí)了虫溜,為什么是0.229:0.578:0.114呢雹姊?因?yàn)閾?jù)研究(不是我研究的~),按照這樣的配比rgb轉(zhuǎn)化以后,人眼看到的是灰度圖像衡楞。吱雏。。瘾境。歧杏。開(kāi)個(gè)玩笑,這就是rgb轉(zhuǎn)灰度的公式之一迷守。然后根據(jù)灰度值犬绒,在0到255之間的位置,來(lái)配對(duì)應(yīng)的ascii碼兑凿,這里 final String base = "#8XOHLTI)i=+;:,.";(字符串由復(fù)雜到簡(jiǎn)單) 所謂的簡(jiǎn)單到復(fù)雜其實(shí)想的不用那么復(fù)雜凯力,就是相同體積內(nèi),繪制出這些字母礼华,哪一個(gè)黑色像素更多咐鹤,僅此而已。直到遍歷所有的像素點(diǎn)以后圣絮,拼成一個(gè)Stringbuffer祈惶,這里每次讀取一個(gè)width的像素以后都要加上一個(gè)換行以區(qū)分一行。接著放到一個(gè)text轉(zhuǎn)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 設(shè)置單間隔的文字,設(shè)置好參數(shù)以后行瑞,在canvas上繪制,通過(guò)bitmap初始化的canvas餐禁,其實(shí)也會(huì)反應(yīng)在bitmap上血久。(我一年前應(yīng)該是沒(méi)設(shè)置好這樣的參數(shù),所以當(dāng)時(shí)畫(huà)出來(lái)的ascii碼圖片帮非,文字間隔比較大氧吐,當(dāng)時(shí)就棄坑了)得到bitmap以后,可以顯示在界面上了末盔,也可以輸出到文字里筑舅,對(duì)于圖片轉(zhuǎn)ascii碼的步驟就到此為止了。
接下來(lái)是視頻轉(zhuǎn)ascii碼的步驟:
其實(shí)視頻可看做是一幀一幀的圖片陨舱,那么接下來(lái)的思路就清晰了吧~
????首先將視頻抓幀翠拣,可以按照你設(shè)定好的每秒抓多少幀,這樣得到一堆圖像序列,而這里得到視頻幀用到了android原生的api游盲,需要android5.0以上:MediaMetadataRetriever 這個(gè)類(lèi)可以得到視頻的時(shí)長(zhǎng)误墓,以及第多少毫秒的圖片預(yù)覽幀蛮粮,于是我先拿到視頻的時(shí)長(zhǎng),比如10000毫秒谜慌,也就是10秒然想,那么接下來(lái)如果我每秒要取15張圖片,那么就每(1000/15)毫秒取一張預(yù)覽幀欣范,直到10000毫秒為止变泄,首先需要強(qiáng)調(diào)下,這個(gè)操作是十分耗時(shí)的恼琼,因此必須將這個(gè)操作放到線程里將這些圖片保存到一個(gè)路徑下妨蛹,具體代碼如下(MediaDecoder是對(duì)于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 + "張解碼開(kāi)始----------------\n");
Bitmap bitmap = mediaDecoder.decodeFrame(i * (1000 / fps));
if (bitmap == null) continue;
Log.i("icv", "第" + i + "張解碼結(jié)束\n");
Log.i("icv", "第" + i + "張轉(zhuǎn)換開(kāi)始\n");
if (weakReference == null || weakReference.get() == null) return;
bitmapTemp = CommonUtil.createAsciiPic(bitmap, weakReference.get());
Log.i("icv", "第" + i + "張轉(zhuǎn)換結(jié)束\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();
}
}
});
}
????這里我直接保存的轉(zhuǎn)換成ascii碼圖片之后的文件了,圖片轉(zhuǎn)ascii碼的步驟見(jiàn)文章上半部分
????接下來(lái)就是最后一步了驳癌,將分割轉(zhuǎn)換的圖片再合成成視頻滑燃,合成視頻的方法我網(wǎng)上也找了很多,不過(guò)基本都是2個(gè)方式:第一個(gè)就是javacodec這個(gè)庫(kù)颓鲜,可是這個(gè)庫(kù)發(fā)現(xiàn)控制不了幀率表窘,也就是說(shuō)一個(gè)視頻如果你轉(zhuǎn)化成圖片設(shè)置的fps比較少的話,比如fps=5甜滨,那么合成視頻的時(shí)候乐严,他會(huì)按照f(shuō)ps = 25默認(rèn)的去合成視頻,那么會(huì)出現(xiàn)的問(wèn)題就是合成的視頻的播放速度會(huì)是原先的5倍- -衣摩,當(dāng)然也可以改這個(gè)庫(kù)的源碼昂验,不過(guò)因?yàn)檫@個(gè)項(xiàng)目以后還有可能加其他的好玩的功能,于是選擇了第二種方案:用ffmpeg進(jìn)行合成艾扮,ffmpeg是一個(gè)用c寫(xiě)的跨平臺(tái)的視頻處理庫(kù)既琴,里面包含了強(qiáng)大的,視頻編解碼泡嘴,推流甫恩,加水印,濾鏡等強(qiáng)大的功能酌予,這也是我選擇它的原因磺箕,由于編譯ffmpeg也是個(gè)大坑,所以直接拿來(lái)了別人編好的移植過(guò)來(lái)了抛虫。
????這里使用了ffmpeg庫(kù)里ffmpeg.c的run方法去跑你拼接的命令松靡,他也是通過(guò)java層傳遞過(guò)來(lái)一個(gè)數(shù)組,這個(gè)數(shù)組裝有ffmpeg的要執(zhí)行的命令建椰,再傳到j(luò)ni里雕欺,在這里面變成一個(gè)char數(shù)組傳遞到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;
}
????簡(jiǎn)略的說(shuō)下各種參數(shù) -f是他規(guī)定的圖片格式,-framerate就是幀率啦阅茶,fps就是一個(gè)int值蛛枚,一般5到25都行谅海,太少會(huì)影響視頻的流暢脸哀,太多會(huì)導(dǎo)致視頻播放過(guò)快,當(dāng)然這個(gè)fps一定要和當(dāng)時(shí)分割成圖片的fps是一模一樣的扭吁,當(dāng)時(shí)分割的如果太細(xì)撞蜂,會(huì)導(dǎo)致后來(lái)合成視頻的文件過(guò)大,因?yàn)榘凑找曈X(jué)殘留原理侥袜,15fps就會(huì)看做是連續(xù)的畫(huà)面了蝌诡,無(wú)停頓感。這里我默認(rèn)選擇5fps是因?yàn)?00毫秒取一幀省時(shí)間,幀數(shù)少枫吧,一會(huì)轉(zhuǎn)化視頻耗時(shí)時(shí)間少啊浦旱。-i表示輸入的媒體文件,一般是avi或mp4的視頻.-b是碼率,這個(gè)可以設(shè)置小一點(diǎn)九杂,就是1秒的媒體所占的大小限制颁湖,-ss是開(kāi)始的時(shí)間,-r是輸出的幀率控制例隆,這里是硬控制甥捺,這里我設(shè)置個(gè)大于framerate的數(shù)就行了,拼好命令以后镀层,就可以傳給ffmpeg進(jìn)行合成了镰禾。合成過(guò)程比較慢,因?yàn)橐簧婕暗揭曨l處理一般都會(huì)慢,靜靜等待執(zhí)行完之后就行了唱逢,到對(duì)應(yīng)目錄上查看合成之后的文件吴侦。
效果圖如下:
這個(gè)demo的不足以及以后將會(huì)改進(jìn)的地方:
- 視頻分割成圖片使用的是系統(tǒng)的api,并沒(méi)有坞古,相當(dāng)于重復(fù)調(diào)用android native的接口备韧,反復(fù)的創(chuàng)建,銷(xiāo)毀資源绸贡,耗時(shí)比較多盯蝴。過(guò)一陣將會(huì)改成使用ffmpeg來(lái)進(jìn)行幀分解,我已經(jīng)跑過(guò)單獨(dú)的測(cè)試demo,效率是目前的10倍 - -听怕。
- 以后會(huì)增加彩色ascii碼的功能捧挺,現(xiàn)在是黑白的ascii碼,其實(shí)在圖片成ascii碼圖片之后尿瞭,再增加一步就行了闽烙,和原先的圖片進(jìn)行相交處理,如果是黑色的,就取原先圖片的彩色rgb黑竞,如果是白色的捕发,就不做處理。
目前支持視頻avi很魂,mp4等常見(jiàn)格式轉(zhuǎn)化成avi扎酷,mp4,gif遏匆。后續(xù)會(huì)支持gif轉(zhuǎn)ascii 的gif或視頻法挨。
項(xiàng)目地址:https://github.com/LineCutFeng/PlayPicdio
歡迎star,你的收藏是我更新的動(dòng)力
系列文章:
Android平臺(tái)下的圖片/視頻轉(zhuǎn)Ascii碼圖片/視頻 (一)
Android平臺(tái)下的圖片/視頻轉(zhuǎn)Ascii碼圖片/視頻 (二)
參考文章:
在Android中使用FFmpeg(android studio環(huán)境)
極樂(lè)凈土----Android實(shí)現(xiàn)圖片轉(zhuǎn)ascii碼字符圖的一些嘗試
android_圖片轉(zhuǎn)視頻_image2video