好好学习,天天向上

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star

前言

我这几天在做一个东西,就是一张像二维码这样的 n*n 的只有两种颜色的点阵图,识别出哪个方块是深色的,哪个方块是浅色的。就像下面这张图

我一开始想的是,既然是图像识别,那不是OpenCV嘛。但是我不会呀,所以就开始研究,发现Android要使用OpenCV还涉及到JNI,NDK。算了算了好复杂,还是直接在网上找现成的轮子吧。找了一圈后,发现没有找到...既然没有轮子,那就自己造

然后想来想去,发现被自己蠢哭了,这个貌似不需要用到OpenCV吧,是我把问题想复杂了。然后开始第二种方案:比如这张图是 24*24 的,那我就把这样图缩小到24px * 24px,那不就正好一个像素对应一个格子嘛,哈哈哈。但最后还是淘汰了这个方案,因为我最终要实现的是拍照再裁切成肉眼的1:1的照片进行识别,因为肉眼裁切或者拍照的时候手机歪了一点点,很难做到正好1:1。这样的话压缩到一个格子对应一个像素这样高的精度很大可能性会造成误差。

然后方案就进化到了第三代,不去进行压缩了,直接计算出每个格子中心像素的坐标,然后判断色值就可以得到这个格子是什么颜色的了。因为不压缩的话每个格子就会占很多个像素,就算图片不是正好的1:1,也不太会出现误差。

正文

废话了一大堆,介绍完方案就来说一下具体实现吧。首先需要明确一个问题:如何去计算某个格子的中心像素的位置,用一个公式就可以得出:

(2 * n+1) * length /(2 * num)

简单解释一下:当知道一条边的长度和格子数后就可以计算出每个格子占多少个像素 length/num,求某一个格子的中心坐标,比如第10个格子,因为从0开始的。所以就是 (2*(10-1)+1) 个半个格子的长度。

先灰度图片,目的是减少误差,然后将Bitmap图片用一个二维数组表示,二维数组的每个元素的值就是对应图片中点的色值。然后通过计算每个方格中心点的坐标,将值从二维数组中取出,放入存放每个方格中心点色值的数组newPx中。最后再遍历一遍newPx,当大于某个值就说明是该方块是白色的,小于某个值则说明该方块是黑色的。那么这个某个值是多少呢,就是所有方格中心像素色值最大值与最小值的平均值。

原理介绍完了,完整代码如下:

/**
* 展示图片
* @param imagePath 图片的路径
*/
private void displayImage(String imagePath) {
if (imagePath != null) {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
Bitmap greyBitmap = convertGreyImg(bitmap);
String pixels = getBitString(greyBitmap, gridNum);
picturePixels.setText(pixels);
} else {
Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
}
} /**
* 获取转化后的表示方格颜色的字符串,黑色用 ● 表示,白色用 ○ 表示
* @param img
* @param num 方格数,总方格数为num * num
* @return
*/
private String getBitString(Bitmap img, int num) {
int width = img.getWidth();//原图像宽度
int height = img.getHeight();//原图像高度
int[] oldPx = new int[width * height];//用来存储原图每个像素点的颜色信息
int[] newPx = new int[num * num]; //用来存放每个方格中心点像素颜色值
int index = 0;
img.getPixels(oldPx, 0, width, 0, 0, width, height);//获取原图中的像素信息
int[][] oldPxTwo = twoArray(oldPx,width,height); //原图每个像素点颜色信息的二维数组
int minValue = Integer.MAX_VALUE;
int maxValue = Integer.MIN_VALUE;
//循环
for (int i = 0; i < num; i++) {
int row = getCoordinate(i,width,num);
for (int j = 0; j < num; j++) {
int col = getCoordinate(j,height,num);
minValue = Math.min(minValue,oldPxTwo[row][col]);
maxValue = Math.max(maxValue,oldPxTwo[row][col]);
newPx[index++] = oldPxTwo[row][col];
}
}
StringBuilder pixels = new StringBuilder();
int middleValue = (minValue + maxValue)/2;
for (int i = 0; i < newPx.length; i++) {
if (i>0 && i%num==0) pixels.append("\n");
if (newPx[i] > middleValue) {
pixels.append("○");
} else {
pixels.append("●");
}
}
return pixels.toString();
} /**
* 获取方块中心点 横/纵 坐标
* @param n 第几个方块,从0开始
* @param length 横向或者纵向的长度
* @param num 横向或者纵向方块数
* @return 坐标值
*/
private int getCoordinate(int n,int length,int num) {
return (2*n+1)*length /(2*num);
} /**
* 一维数组转化为二维数组
* @param arr
* @param width 纵向的方块数,多少行
* @param height
* @return
*/
public int[][] twoArray(int[] arr,int width,int height) {
int[][] result = new int[height][width];
int k = 0;
for (int i = 0;i<height;i++) {
for (int j = 0;j<width;j++) {
result[i][j] = arr[k++];
}
}
return result;
} /**
* 将彩色图转换为灰度图
* @param img 位图
* @return 返回转换好的位图
*/
public Bitmap convertGreyImg(Bitmap img) {
int width = img.getWidth(); //获取位图的宽
int height = img.getHeight(); //获取位图的高 int[] pixels = new int[width * height]; //通过位图的大小创建像素点数组 img.getPixels(pixels, 0, width, 0, 0, width, height);
int alpha = 0xFF << 24;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
int grey = pixels[width * i + j]; int red = ((grey & 0x00FF0000) >> 16);
int green = ((grey & 0x0000FF00) >> 8);
int blue = (grey & 0x000000FF); grey = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11);
grey = alpha | (grey << 16) | (grey << 8) | grey;
pixels[width * i + j] = grey;
}
}
Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
result.setPixels(pixels, 0, width, 0, 0, width, height);
return result;
}

这个转化为灰度图的方法是我在网上找的。

展示一下效果:

总结

好了,到这里就介绍完了,听我介绍完是不是觉得挺简单的。因为本篇文章主要是讲如何讲如何识别点阵图,所以调用相册等内容就没有说,其实我这个Demo调用相册的代码也是直接从《第一行代码》中拿来用的,不是自己写的。代码还可以再优化一下的,比如一位数组转二维数组可以和灰度图片一起实现,不过这些就留给小伙伴们自己去实现啦!

都看完了,来个 "" "" "" 鼓励一下我呗...

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star

随机推荐

  1. 图解JVM执行引擎之方法调用

    一.方法调用 方法调用不同于方法执行,方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程.Class文件的编译过程中不包括传统编译器中的连接步骤,一 ...

  2. LINQ LINQ Operators and Lambda Expression - Syntax &amp; Examples

    LINQ is a cool feature in C# 3.0. Most of the developers are struggling for the syntax and examples. ...

  3. 【Networking】flannel,pipework,weave,udp,vxlan,ovs等资料

    Add Open vSwitch-based multitenant backend for use with OpenShift / Kubernetes:  https://github.com/ ...

  4. R语言多重共现性的检测

    1.kappa值 2. library(car)vif(lm.sol) 得到各个系数的方差膨胀因子,当0<VIF<10的时候,不存在多重共线性,当10<=VIF<100,存在较 ...

  5. springboot 1.5.2 集成kafka 简单例子

    添加依赖 compile("org.springframework.kafka:spring-kafka:1.1.2.RELEASE") 添加application.propert ...

  6. node.js上除了Express还有哪些好用的web开发框架

    老司机都有体会, 开发本身没有多难, 最纠结其实是最初的技术和框架选型, 本没有绝对的好坏之分, 可一旦选择了不适合于自己业务场景的框架, 将来木已成舟后开发和维护成本都很高, 等发现不合适的时候更换 ...

  7. Logback中使用TurboFilter实现日志级别等内容的动态修改

    可能看到这个标题,读者会问:要修改日志的级别,不是直接修改log.xxx就好了吗?为何要搞那么复杂呢?所以,先说一下场景,为什么要通过TurboFilter去动态的修改日志级别.我们在使用Java开发 ...

  8. telegraf、influxDB、Grafana的安装与基本使用

    目的理解influxDB的数据收集原理和方法为使用grafana分析数据及展示结作好准备介绍[收集数据] Telegraf 是一个用 Go 编写的代理程序,可收集系统和服务的统计数据,并写入到 Inf ...

  9. 001.Parted工具使用

    一 Parted简介 1.1 parted和fdisk 通常使用较多的磁盘管理工具为fdisk,但由于磁盘越来越廉价,且磁盘空间越来越大,而fdisk工具分区存在大小限制,只能划分小于2T的磁盘.因此 ...

  10. Swift中的Any 与 AnyObject、AnyClass的区别?

    在 Swift 中能够表示 “任意” 这个概念的除了Any .AnyObject以外,还有一个AnyClass. Any.AnyObject.AnyClass有什么区别: AnyObject是一个成员 ...