很早就看到過這種場景嗅义,用字符來展示圖片甚至播放視頻族阅,可以說是黑客炫(zhuang)技(b)神器东跪。當(dāng)然有了一定的技術(shù)之后躁染,就明白其實實現(xiàn)挺簡單鸣哀。
相機預(yù)覽
首先是相機預(yù)覽的實現(xiàn),因為不是這里的重點吞彤,所以直接在Github上找到成熟的代碼我衬。Google官方的Demo當(dāng)然是最好的:
https://github.com/googlesamples/android-Camera2Basic
這個項目演示了Camera2 API的基本使用,并在一個TextureView上展示了相機實時畫面饰恕。
轉(zhuǎn)換算法一(RGB轉(zhuǎn)換)
有了TextureView挠羔,就能通過getBitmap()方法拿到bitmap,接下來就是把bitmap轉(zhuǎn)換成字符串懂盐,相關(guān)算法這里有一份:
https://github.com/idevelop/ascii-camera/blob/master/script/ascii.js
雖然是JavaScript的褥赊,但是簡單看一下就知道原理:
- 把bitmap中像素點的RGB值轉(zhuǎn)換成灰度
- 用一個字符數(shù)組表示不同的灰度,如ascii字符串
" .,:;i1tfLCG08@"
莉恼,越往后表示灰度越高拌喉,也就是顏色越深。當(dāng)然也可以中文" 一十大木本米菜數(shù)簇龍龘"
俐银。 - 采樣像素點灰度轉(zhuǎn)換成字符尿背,每行成一個字符串,不同行用換行符連接成一個總的字符串捶惜,展示到TextView上田藐。
算法 Utils.java
public class Utils {
public static void startConvert(final TextureView textureView, final TextView textView) {
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
if(textureView != null) {
Bitmap bitmap = textureView.getBitmap();
if(bitmap != null) {
String s = Utils.bitmap2string(bitmap);
textView.setText(s);
}
}
sendEmptyMessageDelayed(0, 20);
}
};
handler.sendEmptyMessage(0);
}
public static Bitmap imageReader2Bitmap(ImageReader imageReader) {
Image image = null;
ByteBuffer buffer = null;
try {
image = imageReader.acquireNextImage();
buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(buffer != null) {
buffer.clear();
}
if(image != null) {
image.close();
}
}
return null;
}
public static String bitmap2string(Bitmap bitmap) {
StringBuilder sb = new StringBuilder();
int w = bitmap.getWidth();
int h = bitmap.getHeight();
for(int j = 0; j < h; j+=20) {
for(int i = 0; i < w; i+=15) {
int pixel = bitmap.getPixel(i, j);
sb.append(color2char(pixel));
}
sb.append("\r\n");
}
return sb.toString();
}
// private static char[] sChars = " .,:;i1tfLCG08@".toCharArray();
private static char[] sChars = " 一十大木本米菜數(shù)簇龍龘".toCharArray();
public static Character color2char(int color) {
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
int brightness = Math.round(0.299f * red + 0.587f * green + 0.114f * blue);
return sChars[brightness * (sChars.length - 1) / 255];
}
}
在原項目的Camera2BasicFragment的onViewCreated()
方法中添加一行代碼啟動即可
@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
view.findViewById(R.id.picture).setOnClickListener(this);
view.findViewById(R.id.info).setOnClickListener(this);
mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);
Utils.startConvert(mTextureView, (TextView) view.findViewById(R.id.text));
}
轉(zhuǎn)換算法二(YUV轉(zhuǎn)換)
上面雖然實現(xiàn)了圖像到字符串的轉(zhuǎn)換, 但是有一些問題:
- TextureView上面還在顯示視頻畫面, 而我們只需要TextView顯示的字符串, 這是一種浪費, 可是TextureView不顯示就拿不到Bitmap
- 很多視頻播放器是SurfaceView的封裝, 也是沒法直接獲取到Bitmap的
- 從Bitmap中取得像素的RGB值, 轉(zhuǎn)換成灰度, 再轉(zhuǎn)換成字符串, 需要一定的計算量, 是否有更簡單的方式?
使用
ImageReader
可以解決以上問題.ImageReader
是Android API 19后提供的工具類, 它內(nèi)部有一個Surface, 可以加載和讀取圖像, 但是不需要直接顯示在界面上. 就相當(dāng)于一個沒有界面的后臺播放器, 我們需要時可以從里面獲取當(dāng)前"播放"的圖像數(shù)據(jù).
ImageReader
還能設(shè)置圖像的格式, 除了RGB外, 另一種常用的格式是YUV. 它也是用像素點的分量來表示圖像, 不同的是, 它的Y分量代表亮度, U和V兩個分量代表顏色. 這樣表示的好處是彩色與黑白畫面的轉(zhuǎn)換很方便, 去掉UV就是黑白的, 也就是灰度; 并且Y分量可以做一定的壓縮, 比如每兩個或四個像素點取一個Y分量, 以節(jié)省空間, 這就產(chǎn)生了不同格式的YUV, 如下圖
YUV格式的詳細介紹可以看這篇文章
http://www.cnblogs.com/azraelly/archive/2013/01/01/2841269.html
代碼實現(xiàn)
之前初始化相機的時候傳入一個TextureView顯示預(yù)覽, 現(xiàn)在傳入一個ImageReader可以嗎? 其實相機依賴的不是TextureView而是Surface, ImageReader.getSurface()
方法可以獲得它內(nèi)部的Surface.
在ImageReader.OnImageAvailableListener
回調(diào)中可以獲取ImageReader中的圖像.
我這里給ImageReader設(shè)置的格式是ImageFormat.YUV_420_888
, 這種格式可以直接獲得圖像的Y分量也就是灰度.
private ImageReader mImageReader = ImageReader.newInstance(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT,
ImageFormat.YUV_420_888, /*maxImages*/2);
mImageReader.setOnImageAvailableListener(
mOnImageAvailableListener, mBackgroundHandler);
private void createCameraPreviewSession() {
try {
// We set up a CaptureRequest.Builder with the output Surface.
mPreviewRequestBuilder
= mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(mImageReader.getSurface());
// Here, we create a CameraCaptureSession for camera preview.
mCameraDevice.createCaptureSession(Arrays.asList(mImageReader.getSurface()),
轉(zhuǎn)換算法如下, 從ImageReader中取得Image, Image中有幾個平面Image.Plane[]
, 其中第一個平面就是Y分量數(shù)組. 它是一維數(shù)組, 通過逐行掃描將二維圖像保存成一維, 我們獲取圖像寬度后進行相反的操作就能轉(zhuǎn)換成二維. 數(shù)組中保存的灰度值范圍是-128~127
. 轉(zhuǎn)換一下就能映射成字符串了.
public static String yuv2string(ImageReader imageReader) {
Image image = null;
ByteBuffer buffer = null;
try {
image = imageReader.acquireNextImage();
Image.Plane[] planes = image.getPlanes();
buffer = planes[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
int w = image.getWidth();
int h = image.getHeight();
// Log.e("chao", "planes " + planes.length + " " + w + "," + h);
StringBuilder sb = new StringBuilder();
for(int j = 0; j < h; j+=6) {
for (int i = 0; i < w; i+=6) {
// int y = bytes[i * w + j] + 128;
int y = bytes[j * w + i] + 128;
char c = sChars[y * (sChars.length - 1) / 255];
sb.append(c);
}
sb.append("\r\n");
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(buffer != null) {
buffer.clear();
}
if(image != null) {
image.close();
}
}
return null;
}
最終的展示效果與RGB轉(zhuǎn)換后相似, 但是YUV轉(zhuǎn)換通用性更好, 效率更高, 它也是圖像處理中經(jīng)常用到的格式.