41

Android字符串相机

 5 years ago
source link: https://www.tuicool.com/articles/Vj6BFnI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

u6Zv2my.jpg!web

本文已获得作者授权,原文作者:rome753

原文链接:https://www.jianshu.com/p/8fd1f0f8a5f1

很早就看到过这种场景,用字符来展示图片甚至播放视频,可以说是黑客炫(zhuang)技(b)神器。当然有了一定的技术之后,就明白其实实现挺简单。

相机预览

首先是相机预览的实现,因为不是这里的重点,所以直接在Github上找到成熟的代码。

Google官方的Demo当然是最好的:

https://github.com/googlesamples/android-Camera2Basic

这个项目演示了Camera2 API的基本使用,并在一个TextureView上展示了相机实时画面。

转换算法一(RGB转换)

有了TextureView,就能通过getBitmap()方法拿到bitmap,接下来就是把bitmap转换成字符串,相关算法这里有一份:

https://github.com/idevelop/ascii-camera/blob/master/script/ascii.js

虽然是JavaScript的,但是简单看一下就知道原理:

  1. 把bitmap中像素点的RGB值转换成灰度

  2. 用一个字符数组表示不同的灰度,如ascii字符串" .,:;i1tfLCG08@",越往后表示灰度越高,也就是颜色越深。当然也可以中文"一十大木本米菜数簇龍龘"。

  3. 采样像素点灰度转换成字符,每行成一个字符串,不同行用换行符连接成一个总的字符串,展示到TextView上。

算法 Utils.java:

 1public class Utils {
 2
 3    public static void startConvert(final TextureView textureView, final TextView textView) {
 4        Handler handler = new Handler(){
 5            @Override
 6            public void handleMessage(Message msg) {
 7                if(textureView != null) {
 8                    Bitmap bitmap = textureView.getBitmap();
 9                    if(bitmap != null) {
10                        String s = Utils.bitmap2string(bitmap);
11                        textView.setText(s);
12                    }
13                }
14                sendEmptyMessageDelayed(0, 20);
15            }
16        };
17        handler.sendEmptyMessage(0);
18    }
19
20    public static Bitmap imageReader2Bitmap(ImageReader imageReader) {
21        Image image = null;
22        ByteBuffer buffer = null;
23
24        try {
25            image = imageReader.acquireNextImage();
26            buffer = image.getPlanes()[0].getBuffer();
27            byte[] bytes = new byte[buffer.remaining()];
28            buffer.get(bytes);
29            return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
30        } catch (Exception e) {
31            e.printStackTrace();
32        } finally {
33            if(buffer != null) {
34                buffer.clear();
35            }
36            if(image != null) {
37                image.close();
38            }
39        }
40        return null;
41    }
42
43    public static String bitmap2string(Bitmap bitmap) {
44        StringBuilder sb = new StringBuilder();
45        int w = bitmap.getWidth();
46        int h = bitmap.getHeight();
47        for(int j = 0; j < h; j+=20) {
48            for(int i = 0; i < w; i+=15) {
49                int pixel = bitmap.getPixel(i, j);
50                sb.append(color2char(pixel));
51            }
52            sb.append("\r\n");
53        }
54        return sb.toString();
55    }
56
57//    private static char[] sChars = " .,:;i1tfLCG08@".toCharArray();
58    private static char[] sChars = " 一十大木本米菜数簇龍龘".toCharArray();
59
60    public static Character color2char(int color) {
61        int red = Color.red(color);
62        int green = Color.green(color);
63        int blue = Color.blue(color);
64        int brightness = Math.round(0.299f * red + 0.587f * green + 0.114f * blue);
65        return sChars[brightness * (sChars.length - 1) / 255];
66    }
67}

在原项目的 Camera2BasicFragment 的 onViewCreated()方法中添加一行代码启动即可。

1   @Override
2    public void onViewCreated(final View view, Bundle savedInstanceState) {
3        view.findViewById(R.id.picture).setOnClickListener(this);
4        view.findViewById(R.id.info).setOnClickListener(this);
5        mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);
6        Utils.startConvert(mTextureView, (TextView) view.findViewById(R.id.text));
7    }

转换算法二(YUV转换)

上面虽然实现了图像到字符串的转换, 但是有一些问题:

  • TextureView上面还在显示视频画面, 而我们只需要TextView显示的字符串, 这是一种浪费, 可是TextureView不显示就拿不到Bitmap

  • 很多视频播放器是SurfaceView的封装, 也是没法直接获取到Bitmap的

  • 从Bitmap中取得像素的RGB值, 转换成灰度, 再转换成字符串, 需要一定的计算量, 是否有更简单的方式?

使用ImageReader可以解决以上问题. ImageReader是Android API 19后提供的工具类, 它内部有一个Surface, 可以加载和读取图像, 但是不需要直接显示在界面上. 就相当于一个没有界面的后台播放器, 我们需要时可以从里面获取当前"播放"的图像数据.

ImageReader还能设置图像的格式, 除了RGB外, 另一种常用的格式是YUV. 它也是用像素点的分量来表示图像, 不同的是, 它的Y分量代表亮度, U和V两个分量代表颜色. 这样表示的好处是彩色与黑白画面的转换很方便, 去掉UV就是黑白的, 也就是灰度; 并且Y分量可以做一定的压缩, 比如每两个或四个像素点取一个Y分量, 以节省空间, 这就产生了不同格式的YUV, 如下图:

MJzEn2M.jpg!web

YUV格式的详细介绍可以看这篇文章:

一文读懂 YUV 的采样与格式

代码实现

之前初始化相机的时候传入一个TextureView显示预览, 现在传入一个ImageReader可以吗? 其实相机依赖的不是TextureView而是Surface, ImageReader.getSurface()方法可以获得它内部的Surface.

在ImageReader.OnImageAvailableListener回调中可以获取ImageReader中的图像.

我这里给ImageReader设置的格式是ImageFormat.YUV_420_888, 这种格式可以直接获得图像的Y分量也就是灰度.

 1private ImageReader mImageReader = ImageReader.newInstance(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT,
 2                        ImageFormat.YUV_420_888, /*maxImages*/2);
 3    mImageReader.setOnImageAvailableListener(
 4                        mOnImageAvailableListener, mBackgroundHandler);
 5
 6    private void createCameraPreviewSession() {
 7        try {
 8
 9            // We set up a CaptureRequest.Builder with the output Surface.
10            mPreviewRequestBuilder
11                    = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
12            mPreviewRequestBuilder.addTarget(mImageReader.getSurface());
13
14            // Here, we create a CameraCaptureSession for camera preview.
15            mCameraDevice.createCaptureSession(Arrays.asList(mImageReader.getSurface()),

转换算法如下, 从ImageReader中取得Image, Image中有几个平面Image.Plane[], 其中第一个平面就是Y分量数组. 它是一维数组, 通过逐行扫描将二维图像保存成一维, 我们获取图像宽度后进行相反的操作就能转换成二维. 数组中保存的灰度值范围是-128~127. 转换一下就能映射成字符串了.

 1public static String yuv2string(ImageReader imageReader) {
 2        Image image = null;
 3        ByteBuffer buffer = null;
 4
 5        try {
 6            image = imageReader.acquireNextImage();
 7            Image.Plane[] planes = image.getPlanes();
 8            buffer = planes[0].getBuffer();
 9            byte[] bytes = new byte[buffer.remaining()];
10            buffer.get(bytes);
11            int w = image.getWidth();
12            int h = image.getHeight();
13//            Log.e("chao", "planes " + planes.length + " " + w + "," + h);
14            StringBuilder sb = new StringBuilder();
15            for(int j = 0; j < h; j+=6) {
16                for (int i = 0; i < w; i+=6) {
17//                    int y = bytes[i * w + j] + 128;
18                    int y = bytes[j * w + i] + 128;
19                    char c = sChars[y * (sChars.length - 1) / 255];
20                    sb.append(c);
21                }
22                sb.append("\r\n");
23            }
24            return sb.toString();
25        } catch (Exception e) {
26            e.printStackTrace();
27        } finally {
28            if(buffer != null) {
29                buffer.clear();
30            }
31            if(image != null) {
32                image.close();
33            }
34        }
35        return null;
36    }

最终的展示效果与RGB转换后相似, 但是YUV转换通用性更好, 效率更高, 它也是图像处理中经常用到的格式.

Github地址

项目参考地址:

https://github.com/rome753/StringCamera

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK