前言
图片算法有很多种,其中opencv可以处理很多图像,但是在一些简单的场景中,其实不需要open cv这么强大的东西,我们完全可以自行实现一些效果。
我们以常用的三个场景展开
- 灰度化:彩色图片转灰白图片
- 亮度调节:通过一些手段调高图片亮度
- 二值化:一般用于目标检测和目标追踪、汉明距离计算,当然,还可以用于图像反选等场景。
下面,是本篇实现的效果。
亮度调节
亮度调节其实有两个思路,一个是将图片的颜色格式转为其他格式,如hsl、hsv,其中HSL颜色格式在之前的点阵体文章中谈及过,我们只需要将L分量设置为50%,就能实现最高亮度。
不过,这种转换相对来说还是过于复杂,因为调整之后,还需要转为rgb格式。
其实,我们知道,如果red-green-blue 同步递增,其最终颜色是偏向白色的,白色自然是亮色,因此我们同步调整red-green-blue色值,理论上图片亮度就会显著提高。
这里我们可以使用Color Matrix来实现,我们可以看到 4x5的矩阵,色值增量是最后一列 。
注意:4x5 的矩阵是提供更多的一列,方便矩阵加法运算
下面是ColorMatrix 单一像素转换矩阵计算,当然,实际绘制时所有的像素都会参与运算。
然而,ColorMatrix 并没有提供修改最后一列数据的方法,不过没关系,我们自定义矩阵,设置进去即可,当然,我们可以不用关注alpha,只需要给red\green\blue三个通道增加50即可。
final float[] a = new float[20];
a[0] = a[6] = a[12] = a[18] = 1;
a[4] = a[9] = a[14] = 50; // red \ green \ blue 色值增加 50
ColorMatrix cm = new ColorMatrix(a);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
注意:另一种方式是利用LightingColorFilter也可以实现亮度增加,不过话说回来,LightingColorFilter屏蔽了一些矩阵操作,实际上我觉得你应该按照本篇的方法,这样你就能明明白白知道为什么是第5列是增量了。
展示效果如下:
从下面右侧的一列我们可以看到,这种亮度调节是有效的,那能不能单独调节每个颜色通道呢,答案是可以的,但是,这种情况只会增强其中某一种颜色的亮度。
灰度化
实际上,灰度化应用也很广泛,实现灰度效果的方式很多,比如HSL、HSV、YUV颜色格式,其中,HSV和HSL只需要将色彩饱和度设置为0即可实现灰度化。
另一种可行的方案是,利用YUV转换时的30-59-11公式,当然下面是0.299-0.587-0.114,我们转换图像计算出Y分量之后,不用关注U、V分量,或者U=0,V=0时就是灰度图,让RED=GREEN=BLUE=Y 分量即可实现灰度化。
Y = 0.299*R + 0.587*G + 0.114*B;
U = -0.169*R - 0.331*G + 0.5 *B;
V = 0.5 *R - 0.419*G - 0.081*B;
YUV转RGB:
R = Y + 1.4075 * V;
G = Y - 0.3455 * U - 0.7169*V;
B = Y + 1.779 * U;
不过,如果能使用矩阵那自然还得使用矩阵,使用30-59-11公式修改单个像素实在是太慢了,这里我们选择Color Matrix实现,将饱和度设置为0。
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
不过,话说回来,30-59-11公式也可以用,但是矩阵肯定性能更好一点,下面,我们把30-59-11公式使用的机会留给二值化。
二值化
二值化是一种非常重要的图像,比如服装染色、目标检测和目标跟踪、人物环境替换、动作捕捉等,因为只有两种颜色,相比来说可以剔除很多干扰因素。
亮度分界二值化算法
下面,我们利用颜色亮度为128分界实现二值化,小于128的强制设置为黑色,大于128的设置为白色,简单粗暴,但是受限于明暗度的问题,一些重要的细节可能被忽视掉。
另外,我们使用 (red * 38 + green * 75 + blue * 15) >> 7 来减少浮点运算。
public static Bitmap grayBitmap2BinaryBitmap(Bitmap graymap, boolean isReverse) {
//得到图形的宽度和长度
int width = graymap.getWidth();
int height = graymap.getHeight();
//创建二值化图像
Bitmap binarymap = graymap.copy(Bitmap.Config.ARGB_8888, true);
//依次循环,对图像的像素进行处理
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
//得到当前像素的值
int col = binarymap.getPixel(i, j);
//得到alpha通道的值
int alpha = Color.alpha(col);
//得到图像的像素RGB的值
int red = Color.red(col);
int green = Color.green(col);
int blue = Color.blue(col);
// 用公式X = 0.3×R+0.59×G+0.11×B计算出X代替原来的RGB
//int gray = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11);
int gray = (red * 38 + green * 75 + blue * 15) >> 7; //降低浮点运算
//对图像进行二值化处理
if (gray > 128) {
gray = isReverse ? 0xFF000000 : 0xFFFFFFFF;
} else {
gray = isReverse ? 0xFFFFFFFF : 0xFF000000;
}
//设置新图像的当前像素值
binarymap.setPixel(i, j, gray);
}
}
return binarymap;
}
效果
平均值二值化算法
但是128是经验值,理论上还可以选择平均值,这样可能选择出整体图片的亮度平均,不过,有一定的不确定性就是,色彩占比越小,丢失的细节可能越多。
/**
* 平均灰度算法获取二值图
*
* @param srcBitmap 图像像素数组地址( ARGB 格式)
* @return Bitmap
*/
public static Bitmap grayAverageBitmap2BinaryBitmap(Bitmap srcBitmap) {
int width = srcBitmap.getWidth();
int height = srcBitmap.getHeight();
double pixel_total = width * height; // 像素总数
if (pixel_total == 0) return null;
Bitmap bitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
long sum = 0; // 总灰度
int threshold = 0; // 阈值
for (int i = 0; i < pixel_total; i++) {
int x = i % width;
int y = i / width;
int pixel = bitmap.getPixel(x, y);
// 分离三原色及透明度
int alpha = Color.alpha(pixel);
int red = Color.red(pixel);
int green = Color.green(pixel);
int blue = Color.blue(pixel);
int gray = (red * 38 + green * 75 + blue * 15) >> 7;
if (alpha == 0 && gray == 0) {
gray = 0xFF;
}
if(gray > 0xFF){
gray = 0xFF;
}
bitmap.setPixel(x, y, gray | 0xFFFFFF00);
sum += gray;
}
// 计算平均灰度
threshold = (int) (sum / pixel_total);
for (int i = 0; i < pixel_total; i++) {
int x = i % width;
int y = i / width;
int pixel = bitmap.getPixel(x, y) & 0x000000FF;
int color = pixel <= threshold ? 0xFF000000 : 0xFFFFFFFF;
bitmap.setPixel(x, y, color);
}
return bitmap;
}
不过,上述代码中我们setPixel进行了与0xFFFFFF00的位或运算,主要原因是Bitmap是ARGB_8888,不满足的话存储数据可能异常,因此有必要转换一下,不过读取的时候需要0x000000FF位与运算。
为了优化细节丢失的问题,日本学者提出了OTSU算法,具体原理是统计0-255每种阶梯的亮度,然后通过方差计算出相关权重。
OTSU 二值化算法
/**
* OTSU 算法获取二值图
*
* @param srcBitmap 图像像素数组地址( ARGB 格式)
* @return 二值图像素数组地址
*/
public static Bitmap bitmap2OTSUBitmap(Bitmap srcBitmap) {
int width = srcBitmap.getWidth();
int height = srcBitmap.getHeight();
double pixel_total = width * height; // 像素总数
if (pixel_total == 0) return null;
Bitmap bitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
long sum1 = 0; // 总灰度值
long sumB = 0; // 背景总灰度值
double wB = 0.0; // 背景像素点比例
double wF = 0.0; // 前景像素点比例
double mB = 0.0; // 背景平均灰度值
double mF = 0.0; // 前景平均灰度值
double max_g = 0.0; // 最大类间方差
double g = 0.0; // 类间方差
int threshold = 0; // 阈值
double[] histogram = new double[256];// 灰度直方图,下标是灰度值,保存内容是灰度值对应的像素点总数
// 获取灰度直方图和总灰度
for (int i = 0; i < pixel_total; i++) {
int x = i % width;
int y = i / width;
int pixel = bitmap.getPixel(x, y);
// 分离三原色及透明度
int alpha = Color.alpha(pixel);
int red = Color.red(pixel);
int green = Color.green(pixel);
int blue = Color.blue(pixel);
int gray = (red * 38 + green * 75 + blue * 15) >> 7;
if (alpha == 0 && gray == 0) {
gray = 0xFF;
}
if(gray > 0xFF){
gray = 0xFF;
}
bitmap.setPixel(x, y, gray | 0xFFFFFF00);
// 计算灰度直方图分布,Histogram 数组下标是灰度值,保存内容是灰度值对应像素点数
histogram[gray]++;
sum1 += gray;
}
// OTSU 算法
for (int i = 0; i < 256; i++) {
wB = wB + histogram[i]; // 这里不算比例,减少运算,不会影响求 T
wF = pixel_total - wB;
if (wB == 0 || wF == 0) {
continue;
}
sumB = (long) (sumB + i * histogram[i]);
mB = sumB / wB;
mF = (sum1 - sumB) / wF;
g = wB * wF * (mB - mF) * (mB - mF);
if (g >= max_g) {
threshold = i;
max_g = g;
}
}
for (int i = 0; i < pixel_total; i++) {
int x = i % width;
int y = i / width;
int pixel = bitmap.getPixel(x, y) & 0x000000FF;
int color = pixel <= threshold ? 0xFF000000 : 0xFFFFFFFF;
bitmap.setPixel(x, y, color);
}
return bitmap;
}
效果如下
从上图我们看到OTSU算法保留的细节还是比较多的。
反向二值化
反向二值化其实只是很好将黑白颜色调换即可,上面的逻辑中我们其实已经实现了,通过参数控制就能实现不同的反向二值化图片。
if (gray > 128) {
gray = isReverse ? 0xFF000000 : 0xFFFFFFFF;
} else {
gray = isReverse ? 0xFFFFFFFF : 0xFF000000;
}
算法评价
在二值化图片中,细节其实很重要,如何尽可能保留细节,其实也有难度,因此常规的方式就是调整算法,然后对比选出最佳的图片。
那么如何判定细节的充足程度呢,有这样一套算法叫做“汉明距离”,很多系统工具类产品中都有这种算法,用于高级别的相似图片检索,我们可以利用这种算法,对比灰度图和生成的二值化图片,如果距离越短,意味着相似度越高。
这里我们可以参考 《pHash》的实现,通过汉明距离比较图片。
总结
到这里本篇就结束了,通过本篇我们可以了解到一些比较有价值的图片效果实现,分别是亮度调节、灰度化、二值化。
在后续的内容中,我们也会继续关注Canvas相关的绘制,如AGSL、RenderNode,我们文章的路线会向Jet Compose转移,希望大家继续关注。
本篇代码
下面是完整的本篇代码:
public class BitmapUtil {
public static Bitmap bitmap2GrayBitmap(Bitmap bmSrc) {
// 得到图片的长和宽
int width = bmSrc.getWidth();
int height = bmSrc.getHeight();
// 创建目标灰度图像
Bitmap bmpGray = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
// 创建画布
Canvas c = new Canvas(bmpGray);
Paint paint = new Paint();
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
paint.setColorFilter(f);
c.drawBitmap(bmSrc, 0, 0, paint);
return bmpGray;
}
/**
* 提高图片亮度
*
* @param bitmap
* @return
*/
public static Bitmap bitmap2LightBitmap(Bitmap bitmap) {
//得到图像的宽度和长度
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Bitmap outputBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
//依次循环对图像的像素进行处理
final float[] a = new float[20];
a[0] = a[6] = a[12] = a[18] = 1;
a[4] = a[9] = a[14] = 50;
ColorMatrix cm = new ColorMatrix(a);
Paint paint = new Paint();
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
paint.setColorFilter(f);
Canvas c = new Canvas(outputBitmap);
c.drawBitmap(bitmap, 0, 0, paint);
return outputBitmap;
}
public static Bitmap grayBitmap2BinaryBitmap(Bitmap graymap, boolean isReverse) {
//得到图形的宽度和长度
int width = graymap.getWidth();
int height = graymap.getHeight();
//创建二值化图像
Bitmap binarymap = graymap.copy(Bitmap.Config.ARGB_8888, true);
//依次循环,对图像的像素进行处理
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
//得到当前像素的值
int col = binarymap.getPixel(i, j);
//得到alpha通道的值
int alpha = Color.alpha(col);
//得到图像的像素RGB的值
int red = Color.red(col);
int green = Color.green(col);
int blue = Color.blue(col);
// 用公式X = 0.3×R+0.59×G+0.11×B计算出X代替原来的RGB
//int gray = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11);
int gray = (red * 38 + green * 75 + blue * 15) >> 7; //降低浮点运算
//对图像进行二值化处理
if (gray > 128) {
gray = isReverse ? 0xFF000000 : 0xFFFFFFFF;
} else {
gray = isReverse ? 0xFFFFFFFF : 0xFF000000;
}
//设置新图像的当前像素值
binarymap.setPixel(i, j, gray);
}
}
return binarymap;
}
/**
* 平均灰度算法获取二值图
*
* @param srcBitmap 图像像素数组地址( ARGB 格式)
* @return Bitmap
*/
public static Bitmap grayAverageBitmap2BinaryBitmap(Bitmap srcBitmap) {
int width = srcBitmap.getWidth();
int height = srcBitmap.getHeight();
double pixel_total = width * height; // 像素总数
if (pixel_total == 0) return null;
Bitmap bitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
long sum = 0; // 总灰度
int threshold = 0; // 阈值
for (int i = 0; i < pixel_total; i++) {
int x = i % width;
int y = i / width;
int pixel = bitmap.getPixel(x, y);
// 分离三原色及透明度
int alpha = Color.alpha(pixel);
int red = Color.red(pixel);
int green = Color.green(pixel);
int blue = Color.blue(pixel);
int gray = (red * 38 + green * 75 + blue * 15) >> 7;
if (alpha == 0 && gray == 0) {
gray = 0xFF;
}
if(gray > 0xFF){
gray = 0xFF;
}
bitmap.setPixel(x, y, gray | 0xFFFFFF00);
sum += gray;
}
// 计算平均灰度
threshold = (int) (sum / pixel_total);
for (int i = 0; i < pixel_total; i++) {
int x = i % width;
int y = i / width;
int pixel = bitmap.getPixel(x, y) & 0x000000FF;
int color = pixel <= threshold ? 0xFF000000 : 0xFFFFFFFF;
bitmap.setPixel(x, y, color);
}
return bitmap;
}
/**
* OTSU 算法获取二值图
*
* @param srcBitmap 图像像素数组地址( ARGB 格式)
* @return 二值图像素数组地址
*/
public static Bitmap bitmap2OTSUBitmap(Bitmap srcBitmap) {
int width = srcBitmap.getWidth();
int height = srcBitmap.getHeight();
double pixel_total = width * height; // 像素总数
if (pixel_total == 0) return null;
Bitmap bitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
long sum1 = 0; // 总灰度值
long sumB = 0; // 背景总灰度值
double wB = 0.0; // 背景像素点比例
double wF = 0.0; // 前景像素点比例
double mB = 0.0; // 背景平均灰度值
double mF = 0.0; // 前景平均灰度值
double max_g = 0.0; // 最大类间方差
double g = 0.0; // 类间方差
int threshold = 0; // 阈值
double[] histogram = new double[256];// 灰度直方图,下标是灰度值,保存内容是灰度值对应的像素点总数
// 获取灰度直方图和总灰度
for (int i = 0; i < pixel_total; i++) {
int x = i % width;
int y = i / width;
int pixel = bitmap.getPixel(x, y);
// 分离三原色及透明度
int alpha = Color.alpha(pixel);
int red = Color.red(pixel);
int green = Color.green(pixel);
int blue = Color.blue(pixel);
int gray = (red * 38 + green * 75 + blue * 15) >> 7;
if (alpha == 0 && gray == 0) {
gray = 0xFF;
}
if(gray > 0xFF){
gray = 0xFF;
}
bitmap.setPixel(x, y, gray | 0xFFFFFF00);
// 计算灰度直方图分布,Histogram 数组下标是灰度值,保存内容是灰度值对应像素点数
histogram[gray]++;
sum1 += gray;
}
// OTSU 算法
for (int i = 0; i < 256; i++) {
wB = wB + histogram[i]; // 这里不算比例,减少运算,不会影响求 T
wF = pixel_total - wB;
if (wB == 0 || wF == 0) {
continue;
}
sumB = (long) (sumB + i * histogram[i]);
mB = sumB / wB;
mF = (sum1 - sumB) / wF;
g = wB * wF * (mB - mF) * (mB - mF);
if (g >= max_g) {
threshold = i;
max_g = g;
}
}
for (int i = 0; i < pixel_total; i++) {
int x = i % width;
int y = i / width;
int pixel = bitmap.getPixel(x, y) & 0x000000FF;
int color = pixel <= threshold ? 0xFF000000 : 0xFFFFFFFF;
bitmap.setPixel(x, y, color);
}
return bitmap;
}
}
作者:时光少年
链接:https://juejin.cn/post/7352075697094180901
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。