最近呢,在做一个小小的项目,大体呢就是要识别一张试纸条上的条纹灰度值。
分析灰度值在图像处理领域属于一个常规的算法,用Opencv+Python的话实现起来应该非常非常的轻松,但这个项目的难点在于如何使用手机端APP来实现灰度值计算这个效果,因此需要我们来手搓算法。
本期介绍使用.NET MAUI和SkiaSharp实现二值化、灰度化、连通区域检测和灰度值计算等实现。#NETMAUI #SkiaSharp #Androdi
1 项目环境

项目采用.NET 8.0框架,开发软件使用的Visual Studio 2022,支持IOS、Android和PC三端代码通用。
由于博主手机是Android系统,因此仅在Android和PC进行了测试,Android系统要求最低5.0.
2 UI界面

在xaml文件中我们编写UI界面代码,其中包含两个skia画板分别用于显示原始图片和灰度值趋势变化图。
Button用于图像处理开始代码测试还有一个Label标签用于显示我们调试过程中显示的信息。
当然这个界面还需要做很多优化,这个是一个Demo版本。
3 基本图像处理
本系统的基本图像处理包括对图像做高斯滤波/中值滤波、灰度化、二值化等操作,由于没有合适的库,我们需要手写这些算法。
internal staticclassImageProcess
我们将这些算法统一封装到ImageProcess类中并在类中实现这些函数,由于个别原因,仅展示部分代码和简单解释。
publicstatic SKBitmap Binarize(SKBitmap source, byte threshold = 128)
{
if (source == null)
thrownew ArgumentNullException(nameof(source));
int width = source.Width;
int height = source.Height;
var result = new SKBitmap(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
var srcPixmap = source.PeekPixels();
var dstPixmap = result.PeekPixels();
IntPtr srcPtr = srcPixmap.GetPixels();
IntPtr dstPtr = dstPixmap.GetPixels();
int bytesPerPixel = 4;
int stride = srcPixmap.RowBytes;
unsafe
{
byte* src = (byte*)srcPtr;
byte* dst = (byte*)dstPtr;
for (int y = 0; y < height; y++)
{
byte* srcRow = src + y * stride;
byte* dstRow = dst + y * stride;
for (int x = 0; x < width; x++)
{
byte b = srcRow[x * 4 + 0];
byte g = srcRow[x * 4 + 1];
byte r = srcRow[x * 4 + 2];
// 灰度计算
byte gray = (byte)(0.299 * r + 0.587 * g + 0.114 * b);
byte bw = (gray >= threshold) ? (byte)255 : (byte)0;
dstRow[x * 4 + 0] = bw; // B
dstRow[x * 4 + 1] = bw; // G
dstRow[x * 4 + 2] = bw; // R
dstRow[x * 4 + 3] = 255; // A
}
}
}
return result;
}
灰度化和二值化算法
灰度化算法比较简单,只要将各个像素点值的RGBA四个通道值按照灰度算法比例进行叠加即可计算出他们的灰度值。
计算出灰度值之后,将灰度值和我们设置的阈值进行对比即可对图片进行二值化处理。
其实这里就能体现C/C++/C# 的好处了,能够直接进行底层指针操作,效率高了几十倍。
由于图片的目标区域只有一个而且目标巨大,因此使用广度优先算法,对联通内容最大的区域进行记录并标记位置和宽高。
publicstatic SKRectI FindLargestWhiteRegion(SKBitmap binary)
{
int width = binary.Width;
int height = binary.Height;
bool[,] visited = newbool[width, height];
List<(int x, int y)> largestRegion = null;
int largestSize = 0;
SKPixmap pixmap = binary.PeekPixels();
IntPtr ptr = pixmap.GetPixels();
int stride = pixmap.RowBytes;
unsafe
{
byte* basePtr = (byte*)ptr;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
if (visited[x, y]) continue;
byte* pixel = basePtr + y * stride + x * 4;
byte r = pixel[2];
if (r < 128) continue;
var region = new List<(int x, int y)>();
Queue<(int x, int y)> queue = new();
queue.Enqueue((x, y));
visited[x, y] = true;
while (queue.Count > 0)
{
var (cx, cy) = queue.Dequeue();
region.Add((cx, cy));
foreach (var (nx, ny) in Neighbors(cx, cy, width, height))
{
if (!visited[nx, ny])
{
byte* p = basePtr + ny * stride + nx * 4;
byte nr = p[2];
if (nr >= 128)
{
visited[nx, ny] = true;
queue.Enqueue((nx, ny));
}
}
}
}
if (region.Count > largestSize)
{
largestRegion = region;
largestSize = region.Count;
}
}
}
}
if (largestRegion == null || largestRegion.Count == 0)
return SKRectI.Empty;
int minX = largestRegion.Min(p => p.x);
int maxX = largestRegion.Max(p => p.x);
int minY = largestRegion.Min(p => p.y);
int maxY = largestRegion.Max(p => p.y);
returnnew SKRectI(minX, minY, maxX + 1, maxY + 1);
}
4 灰度波形
最后当我们找到了目标区域之后就很简单了,我们可以直接对目标区域的每行/列区域进行灰度平均值分析,即可获得如果所示效果。

554