当前位置: 首页 > news >正文

Android字符串相机

640?wx_fmt=jpeg

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

很早就看到过这种场景,用字符来展示图片甚至播放视频,可以说是黑客炫(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, 如下图:

640?wx_fmt=other

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

一文读懂 YUV 的采样与格式

代码实现

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

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

 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

推荐阅读

  • 跨平台渲染引擎之路:拨云见日

  • Google Jetpack 新组件 CameraX 介绍与实践


关注微信公众号【纸上浅谈】, Android 开发、Camera、OpenGL、FFmpeg 等音视频和图形图像开发文章~~

640?wx_fmt=gif

相关文章:

  • OpenGL 实践之贝塞尔曲线绘制
  • 中国物联网激荡20年
  • OpenGL 实现视频编辑中的转场效果
  • 程序员等级图鉴
  • 快手高性能移动端多媒体引擎架构
  • 如何优雅地实现一个分屏滤镜
  • OpenGLES滤镜开发 — 仿FaceU边框模糊效果
  • Android JNI Crash定位步骤
  • 2019年的第三场LiveVideoStackCon有何不同?
  • 笑死人不偿命的知乎沙雕问题排行榜
  • Android MediaCodec 硬编码 H264 文件
  • 原创|Android Jetpack Compose 最全上手指南
  • 一场微秒级的同步事故
  • 在HTML5上开发音视频应用的五种思路
  • 如何把微信打造成一个学习利器|微信阅读与笔记技巧
  • 收藏网友的 源程序下载网
  • 78. Subsets
  • GDB 调试 Mysql 实战(三)优先队列排序算法中的行记录长度统计是怎么来的(上)...
  • Git同步原始仓库到Fork仓库中
  • IIS 10 PHP CGI 设置 PHP_INI_SCAN_DIR
  • in typeof instanceof ===这些运算符有什么作用
  • JavaScript的使用你知道几种?(上)
  • MySQL QA
  • Netty 4.1 源代码学习:线程模型
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • XML已死 ?
  • 高度不固定时垂直居中
  • 如何编写一个可升级的智能合约
  • 如何设计一个微型分布式架构?
  • 使用common-codec进行md5加密
  • 为物联网而生:高性能时间序列数据库HiTSDB商业化首发!
  • 小程序、APP Store 需要的 SSL 证书是个什么东西?
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 用element的upload组件实现多图片上传和压缩
  • 继 XDL 之后,阿里妈妈开源大规模分布式图表征学习框架 Euler ...
  • ​html.parser --- 简单的 HTML 和 XHTML 解析器​
  • #!/usr/bin/python与#!/usr/bin/env python的区别
  • #DBA杂记1
  • #在 README.md 中生成项目目录结构
  • (9)目标检测_SSD的原理
  • (env: Windows,mp,1.06.2308310; lib: 3.2.4) uniapp微信小程序
  • (笔记)Kotlin——Android封装ViewBinding之二 优化
  • (二)WCF的Binding模型
  • (附源码)spring boot建达集团公司平台 毕业设计 141538
  • (附源码)springboot金融新闻信息服务系统 毕业设计651450
  • (一)python发送HTTP 请求的两种方式(get和post )
  • .NET 3.0 Framework已经被添加到WindowUpdate
  • .NET Core 版本不支持的问题
  • .NET Standard 的管理策略
  • .net 程序 换成 java,NET程序员如何转行为J2EE之java基础上(9)
  • /etc/sudoers (root权限管理)
  • [ C++ ] STL_stack(栈)queue(队列)使用及其重要接口模拟实现
  • [ vulhub漏洞复现篇 ] Apache Flink目录遍历(CVE-2020-17519)
  • []利用定点式具实现:文件读取,完成不同进制之间的
  • [2021]Zookeeper getAcl命令未授权访问漏洞概述与解决