Bitmap过滤有效信息

Bitmap过滤有效信息

四月 01, 2020

version 0.2

[TOC]

产生背景

最近在和产品一起讨论我们新设备的亮点功能上尝试探讨,是否可以将导航过程中的路口放大图通过蓝牙传递到我们的hud上。可是由于成本因素考虑,硬件的限制条件比较苛刻。留给我们的显示区域的分辨率只有128*71的大小,而且文件内存只能控制在寥寥2k以内,最初时候甚至不支持加载图片。中间也经历了几次尝试从黑白到彩色,己经波折与尝试终于有了一个不错的效果展示以此记录这次过程。

采用黑白方案过程:

协定的是客户端将图片的特征数据提取并简化成点阵,然后采用压缩的形式再传给客户端。分成数据提取,与信息表示两个部分。

特征提取

首先想到的方案是通过Bitmap来过滤特征的像素值,然后进一步去掉其他无关像素点。

image.png-c

首次过滤效果是这样,上面是原高德地图SDK返回的路口放大图信息,下图是通过Bitmap像素遍历值并替换需要元素拿到的转换图片。

初步过程

初步看来主要信息轮廓都在,看上去只要再压缩完成后去传输就行。
当时想的方案是颜色加坐标,然后传输过去,HUD端那边接收再按行绘制出来。

可在实际压缩至小的分辨率之后发现锯齿比想象中严重虽然分辨率低,但是由于单纯用像素匹配黄色导航箭头可能效果还好,但对于路的边缘有很多误差,甚至像一些建筑物桥梁等根本看不出来是什么,而且还有其他问题。

首先虽然这张图的像素可以替换成简单的四个色值,但是压缩后在所难免产生色彩的偏移:

原始调整颜色-c

原始压缩效果-c

可以看到道路的周围对于这种哪怕非常清晰的原图压缩后也没有很理想的效果。这个对于锯齿的处理,想用简单的连线来代替可处理的判断逻辑比想象中要复杂,如何判断周围的像素要相连,如何判断不相连,压缩之后的图片色彩偏移后 白色与灰色值的判定依旧不是那么清晰。

到这里,似乎问题变得不好处理起来。日常大多处理的东西很少会需要这种精准度的匹配,相关问题组里面也没有人涉及。我的组长只是想到看能不能用OpenCV 来试着处理,可自己从来没有过经验。硬着头皮试试看吧。

OpenCV

终于在一次偶然机会,发现了一种术语叫特征提取。其中的坎尼运算对于描述图像轮廓很有帮助,这是OpenCV中比较常用的特征提取算法。大体是用线条来绘制轮廓而不在描述全部特征的一种方式,也算是边缘检测的一种方法。

在摸索了几天OpenCV的基础使用后,我开始集成进Demo去尝试处理。

坎尼运算-c

坎尼运算2-c

我想或许这是我想要的方案,但这又产生了另一个问题,这只有轮廓却完全是黑白色的体现。但我可以手动再填充一次,同时坎尼运算有两个阈值设置为了简而言之就是这两个阈值算子决定了轮廓绘制的有多详细。

坎尼运算3-c

以上便方便切较为精准的匹配了特征,那么下一步就是希望降低压缩后的颗粒感。(因为分辨率非常小,又为了突出主体在研究的过程中我加上了一部分裁剪的方法来适当去掉无关内容,比如蓝天那些远景图。原理也比较简单,就是从黄色箭头处的位置适当保留高度,而不是全部的高度这样压缩后的图片主要能容就会更大一些。)

压缩与填充

在尝试各种自带方法效果不理想后,奈何对于计算机图像视觉根本于我不着边际,又开始了各种谷歌尝试找到一种方法,于是OpenCV中的一种压缩方式引起了我的注意,这种采用的像素面积关系重采样能较好的拟合这种锯齿行为。同时其中的二值化处理也为压缩后的图片产生色彩偏移带来了可行性,在这过程中也加了一些处理为了使得锯齿更小,同时加入了高斯模糊以及锐化等辅助处理工作。

OpenCv压缩-c

image.png-c

最终处理的图片可以看到在经过这些处理后的图像抗锯齿的效果好了很多,之前左边毛躁的边缘开始类似两条线去收尾。至此在黑白色彩方案里终于可以有一个可预期的效果。

最终展示效果:

最终效果-c

数据表示

处理数据的过程也是一个反复推到重试的过程,刚开始制定的想法就是颜色的二进制值去表示,对方(HUD端)再去绘制。
但很快发现了一个问题,分辨率是固定的也就是 128 * 71的大小,那么我就要有这么多个点,也就是要有这么多的数据。
如果用RGB来表示那么就是三个数 再乘以像素的大小。

(4字节 + 4字节 + 4字节)* 128 * 71 = 109056 字节 ≈ 106k

那么一定是不能按照类似加载到内存大小的形式来表示数据了。

我们初步想的方案准备用一字节来表示颜色,因为颜色种类只有四种以内:
分别是建筑的灰色1,指示的黄色2,道路绘制的白色3,以及无关的黑色内容4。

1字节 * 128 * 71 = 18176 字节 ≈ 8.8K

这样颜色的表示瞬间降低了下来,可这个大小仍旧与实际需要想去甚远。类似jpeg 之类的图片在我保存到硬盘上只有2k到6k大小而且人家的颜色还不止这么点,然后去
翻资料找到一个游程编码(Run-length encoding,简称RLE)压缩的方式。

Wiki-Run-length encoding

image.png-c

简而言之就是把数据按照线性的序列分为连续不连续的两种情况。假设我的图片是10*10 在第一行的像素是这样:

[黑,黑,黑,黑,黑,黄,黄,白,白,白]

那么我可以表示成:

黑[5],黄[2],白[3]

原本一行的内容是 10字节,但现在只需要6字节,这还是因为长度比较少,极端情况一行都是黑色可以用2字节表示。
这个过程也是在和我的组长讨论后看上去比较简单可行的方式。
后来发现传统的方式是偏向横向压缩,对我们的图像更加偏向纵向线条,所以后来换成了纵向的RLE方式,不过那是后来的事情了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* 获取到像素坐标点
*
* @param bitmap
* @return
*/
public static Map<Integer, List<Point>> getCoordinateData(Bitmap bitmap) {
if (bitmap == null) {
Log.e(TAG, "getCoordinateData() called with: bitmap is null");
return null;
}
//获得缩放后的宽高
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Log.d(TAG, "getRegenBitmap after compress: width = " + width + ",height = " + height);

// 2. 判断颜色比较遍历像素 并填充替换=====
// 保存所有的像素的数组,图片宽×高
int[] pixels = new int[width * height];

bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

int length = pixels.length;

Map<Integer, List<Point>> map = new HashMap<>();
List<Point> listY = new ArrayList<>();
List<Point> listW = new ArrayList<>();
List<Point> listG = new ArrayList<>();
Point point;

for (int i = 0; i < length; i++) {
int clr = pixels[i];
int red = Color.red(clr);
int green = Color.green(clr);
int blue = Color.blue(clr);

if (isArrow(red, green, blue)) { //箭头黄色
pixels[i] = Color.YELLOW;
point = new Point();
point.y = i / width;
point.x = i % width;
listY.add(point);
} else if (isWhite(red, green, blue)) {//线条白色
// pixels[i] = Color.WHITE;
pixels[i] = Color.GRAY;
point = new Point();
point.y = i / width;
point.x = i % width;
listG.add(point);
// listW.add(point);
} else if (isBuilding(red, green, blue)) {//建筑灰色
pixels[i] = Color.GRAY;
point = new Point();
point.y = i / width;
point.x = i % width;
listG.add(point);
} else if (isBlue(red, green, blue)) { //道路黑色
pixels[i] = Color.BLACK;
} else {
//其他默认黑色
pixels[i] = Color.BLACK;
}
}

// ImageUtils.save(Bitmap.createBitmap(pixels, width, height, Bitmap.Config.RGB_565),
// (PathUtils.getInternalAppCachePath() + File.separator + "cross1.png")
// , Bitmap.CompressFormat.PNG);
map.put(1, listY);
map.put(2, listW);
map.put(3, listG);
return map;
}

/**
* RLE行程压缩算法
*/
public static class RLEPoint {
private int len = 1; //为了确保只有1字节,点的个数最多127
private Point startPoint;

public int getLen() {
return len;
}

public Point getStartPoint() {
return startPoint;
}

@Override
public String toString() {
return "{len = " + len + ", start:" + startPoint + "}";
}
}

/**
* 使用RLE行程长度编码压缩,(按照纵坐标来压)
*
* @param originPoints
* @return
*/
public static Map<Integer, List<RLEPoint>> covertRLEPoint(Map<Integer, List<Point>> originPoints) {
Map<Integer, List<RLEPoint>> dest = new HashMap<>();

Iterator<Map.Entry<Integer, List<Point>>> iterator = originPoints.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, List<Point>> entry = iterator.next();
Integer key = entry.getKey();
List<Point> points = entry.getValue();

dest.put(key, convertPoint2RLE(points));
}
return dest;
}


/**
* 扩展RLE, 相同长度的再合并, 减少那些散点
*
* @param originRLEPoints
* @return
*/
public static Map<Integer, List<RLEPointsExt>> covertRLEPointExt(Map<Integer, List<RLEPoint>> originRLEPoints) {
Map<Integer, List<RLEPointsExt>> dest = new HashMap<>();

Iterator<Map.Entry<Integer, List<RLEPoint>>> iterator = originRLEPoints.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, List<RLEPoint>> entry = iterator.next();
Integer key = entry.getKey();
List<RLEPoint> points = entry.getValue();

dest.put(key, convertRLEPoint2Ext(points));
}
return dest;
}

在经过整理后再去最终得到压缩后的集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//获取到需要的像素坐标点
Map<Integer, List<Point>> picData = CrossBitmapUtil.getCoordinateData(newBitmap);

/**使用RLE行程长度来对像素坐标点进行编码压缩(按照纵坐标来压)**/
Map<Integer, List<CrossBitmapUtil.RLEPoint>> rlePoints = CrossBitmapUtil.covertRLEPoint(picData);

/**扩展RLE, 相同长度的再合并, 减少那些散点**/
Map<Integer, List<CrossBitmapUtil.RLEPointsExt>> rleDataExt = CrossBitmapUtil.covertRLEPointExt(rlePoints);

Set<Integer> keySet = rleDataExt.keySet();
size += keySet.size(); /** NByte: Color **/

Collection<List<CrossBitmapUtil.RLEPointsExt>> values = rleDataExt.values();
for (List<CrossBitmapUtil.RLEPointsExt> segments : values) {//RLE Ext segments
size += 1;/** 1Byte: segments **/
for (CrossBitmapUtil.RLEPointsExt ext : segments) {
size += 1;/** 1Byte: RLE Len **/
size += 2;/** 2Byte: Point Count **/
size += 2 * ext.getPoints().size(); /** NByte: Points **/
}
}

采用彩色方案的过程:

在几经波折后我们的硬件系统终于升高,可以满足加载jepg图片格式,给了我们直接传输图片的可能。
我们拿到的图片是从高德SDK的路口放大图回调中获取的分辨率是500*320,这个分辨率的图片可以很清晰的展示图像。举例其中一个:

cross_1.png-c

直接调用自带方式压缩至指定分辨率尝试:

image.png-c

可以看到效果是不错的,可是当我转存文件大小时发现文件大小是9.2k大小,可是这个大小对于时效性比较高的信息,我们设备的蓝牙传输过程会变长导致可能这张图片可能要很久才能显示出来,这就需要进一步压缩质量提高传输速度。

image.png-c

之后因为已经因为之前集成了OpenCV所以就尝试用OpenCV的方式对比了一下在质量为15的情况下图片对比:

image.png-c

除了最直观的大小压缩不同外,OpenCV的INTER_AREA方法还是克服了一些干扰的波纹情况,这里为了便捷我还是放到颗粒感比较强的时候。

后记

其实经过整体过程后发现逻辑串通起来没有当初感觉的那么复杂,但是这个其中反反复复验证的过程或许才是最有意义的收获。又尝试着拓宽一些使用方式,对图像处理以及蓝牙之间的数据协议制定都有了新的认识。也为后续去做高德与谷歌的识别奠定了一些图像基础,等有空再整理一篇。

更新日志:

版本 时间 说明
version 0.1 2020年04月01日23:27:47 初版整理过程
version 0.2 2020年07月22日17:31:26 添加彩色图片过程,格式微调
version 0.3 2022年02月08日17:49:01 更换图床为Github