效果展示
实现功能
- connection
- 膨胀
- 腐蚀
- 开运算
- 闭运算
- 特征计算
核心代码
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
namespace view3d
{
// 基础对象类,类似于 Halcon 的 HObject
public abstract class HObject
{
public abstract HObjectType Type { get; }
public enum HObjectType
{
Region,
XLD,
Image
}
}
// 使用 RLE (Run-Length Encoding) 编码的 HRegion 实现
public class HRegion : HObject
{
public override HObjectType Type => HObjectType.Region;
// RLE 编码数据结构: 每行存储起始列和结束列的列表
private Dictionary<int, List<Interval>> _rleData = new Dictionary<int, List<Interval>>();
// 边界框缓存
private Rectangle _boundingBox;
private bool _boundingBoxValid = false;
// 用于表示一行中的连续区间
private struct Interval
{
public int Start;
public int End;
public Interval(int start, int end)
{
Start = start;
End = end;
}
public int Length => End - Start + 1;
}
// 构造函数
public HRegion() { }
// 从位图创建区域
public HRegion(Bitmap bitmap, byte threshold = 128)
{
FromBitmap(bitmap, threshold);
}
// 从位图二值化并创建区域
public void FromBitmap(Bitmap bitmap, byte threshold = 128,byte maxThreshold=255)
{
_rleData.Clear();
_boundingBoxValid = false;
for (int y = 0; y < bitmap.Height; y++)
{
List<Interval> intervals = new List<Interval>();
bool inRegion = false;
int startX = 0;
for (int x = 0; x < bitmap.Width; x++)
{
Color pixel = bitmap.GetPixel(x, y);
byte intensity = (byte)((pixel.R + pixel.G + pixel.B) / 3);
bool isForeground = intensity >= threshold && intensity<=maxThreshold;
if (isForeground && !inRegion)
{
// 进入区域
inRegion = true;
startX = x;
}
else if (!isForeground && inRegion)
{
// 离开区域
intervals.Add(new Interval(startX, x - 1));
inRegion = false;
}
}
// 处理行末尾仍在区域中的情况
if (inRegion)
{
intervals.Add(new Interval(startX, bitmap.Width - 1));
}
if (intervals.Count > 0)
{
_rleData[y] = intervals;
}
}
UpdateBoundingBox();
}
// 更新边界框
private void UpdateBoundingBox()
{
if (_rleData.Count == 0)
{
_boundingBox = Rectangle.Empty;
_boundingBoxValid = true;
return;
}
int minX = int.MaxValue;
int maxX = int.MinValue;
int minY = _rleData.Keys.Min();
int maxY = _rleData.Keys.Max();
foreach (var kvp in _rleData)
{
foreach (var interval in kvp.Value)
{
if (interval.Start < minX) minX = interval.Start;
if (interval.End > maxX) maxX = interval.End;
}
}
_boundingBox = new Rectangle(minX, minY, maxX - minX + 1, maxY - minY + 1);
_boundingBoxValid = true;
}
// 获取边界框
public Rectangle BoundingBox
{
get
{
if (!_boundingBoxValid) UpdateBoundingBox();
return _boundingBox;
}
}
// 计算区域面积
public double Area()
{
double area = 0;
foreach (var intervals in _rleData.Values)
{
foreach (var interval in intervals)
{
area += interval.Length;
}
}
return area;
}
// 计算重心
public PointF CenterOfGravity()
{
if (_rleData.Count == 0) return PointF.Empty;
double sumX = 0, sumY = 0;
double totalPixels = 0;
foreach (var kvp in _rleData)
{
int y = kvp.Key;
foreach (var interval in kvp.Value)
{
int length = interval.Length;
double centerX = (interval.Start + interval.End) / 2.0;
sumX += centerX * length;
sumY += y * length;
totalPixels += length;
}
}
if (totalPixels == 0) return PointF.Empty;
return new PointF((float)(sumX / totalPixels), (float)(sumY / totalPixels));
}
// 计算圆度
public double Circularity()
{
double area = Area();
if (area == 0) return 0;
// 计算周长 (简化版,实际需要更精确的计算)
double perimeter = 0;
foreach (var kvp in _rleData)
{
int y = kvp.Key;
foreach (var interval in kvp.Value)
{
// 上边界
if (!_rleData.ContainsKey(y - 1) || !HasPixel(y - 1, interval.Start, interval.End))
perimeter += interval.Length;
// 下边界
if (!_rleData.ContainsKey(y + 1) || !HasPixel(y + 1, interval.Start, interval.End))
perimeter += interval.Length;
// 左边界
perimeter += CountLeftEdges(interval.Start, y);
// 右边界
perimeter += CountRightEdges(interval.End, y);
}
}
return (4 * Math.PI * area) / (perimeter * perimeter);
}
private bool HasPixel(int y, int startX, int endX)
{
if (!_rleData.TryGetValue(y, out var intervals))
return false;
foreach (var interval in intervals)
{
if (interval.Start <= endX && interval.End >= startX)
return true;
}
return false;
}
private int CountLeftEdges(int x, int y)
{
int count = 0;
if (!HasPixel(y, x - 1, x - 1)) count++;
return count;
}
private int CountRightEdges(int x, int y)
{
int count = 0;
if (!HasPixel(y, x + 1, x + 1)) count++;
return count;
}
// 转换为位图
public Bitmap ToBitmap(Bitmap bitmap, Color color ,bool clear=true)
{
if (_rleData.Count == 0) return new Bitmap(1, 1);
Rectangle bb = BoundingBox;
//Bitmap bitmap = new Bitmap(bb.Width, bb.Height);
if (clear)
{
using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Black);
}
}
foreach (var kvp in _rleData)
{
int y = kvp.Key ;
foreach (var interval in kvp.Value)
{
for (int x = interval.Start ; x <= interval.End ; x++)
{
if (x >= 0 && x < bitmap.Width && y >= 0 && y < bitmap.Height)
{
bitmap.SetPixel(x, y,color);
}
}
}
}
return bitmap;
}
// 形态学操作 - 膨胀
public HRegion Dilation(int radius)
{
HRegion result = new HRegion();
foreach (var kvp in _rleData)
{
int y = kvp.Key;
foreach (var interval in kvp.Value)
{
for (int dy = -radius; dy <= radius; dy++)
{
int newY = y + dy;
int dx = (int)Math.Sqrt(radius * radius - dy * dy);
int newStart = interval.Start - dx;
int newEnd = interval.End + dx;
if (!result._rleData.TryGetValue(newY, out var intervals))
{
intervals = new List<Interval>();
result._rleData[newY] = intervals;
}
// 合并重叠或相邻的区间
bool merged = false;
for (int i = 0; i < intervals.Count; i++)
{
if (newEnd + 1 >= intervals[i].Start && newStart - 1 <= intervals[i].End)
{
// 合并区间
int start = Math.Min(newStart, intervals[i].Start);
int end = Math.Max(newEnd, intervals[i].End);
intervals[i] = new Interval(start, end);
merged = true;
// 检查是否需要与后面的区间合并
while (i + 1 < intervals.Count && intervals[i].End + 1 >= intervals[i + 1].Start)
{
intervals[i] = new Interval(intervals[i].Start, Math.Max(intervals[i].End, intervals[i + 1].End));
intervals.RemoveAt(i + 1);
}
break;
}
}
if (!merged)
{
intervals.Add(new Interval(newStart, newEnd));
// 保持区间有序
intervals.Sort((a, b) => a.Start.CompareTo(b.Start));
}
}
}
}
result._boundingBoxValid = false;
return result;
}
// 腐蚀操作入口(自动选择最优策略)
public HRegion ErosionOptimized(int radius, bool useCircle = true)
{
if (radius <= 0) return new HRegion();
if (_rleData.Count == 0) return new HRegion();
// 超大半径直接返回空(优化极端情况)
Rectangle bb = BoundingBox;
if (radius >= Math.Max(bb.Width, bb.Height) * 2)
return new HRegion();
// 分阶段腐蚀(自动分解大半径)
HRegion result = this;
int remainingRadius = radius;
while (remainingRadius > 0)
{
int currentRadius = Math.Min(5, remainingRadius); // 单次最大半径=5
result = useCircle ?
ErosionCircleSingleStep(result, currentRadius) :
ErosionRectSingleStep(result, currentRadius);
remainingRadius -= currentRadius;
}
return result;
}
// 单步圆形腐蚀(并行优化)
private HRegion ErosionCircleSingleStep(HRegion region, int radius)
{
var result = new ConcurrentDictionary<int, List<Interval>>();
var offsets = PrecomputeCircleOffsets(radius); // 预计算圆形偏移量
Parallel.ForEach(region._rleData.Keys, y =>
{
var erodedIntervals = new List<Interval>();
foreach (var interval in region._rleData[y])
{
int currentStart = -1;
for (int x = interval.Start; x <= interval.End; x++)
{
bool isValid = true;
foreach (var (dx, dy) in offsets)
{
int checkX = x + dx;
int checkY = y + dy;
if (!region.IsPixelCovered(checkY, checkX))
{
isValid = false;
break;
}
}
// 合并连续区间
if (isValid)
{
if (currentStart == -1)
currentStart = x;
}
else if (currentStart != -1)
{
erodedIntervals.Add(new Interval(currentStart, x - 1));
currentStart = -1;
}
}
if (currentStart != -1)
erodedIntervals.Add(new Interval(currentStart, interval.End));
}
if (erodedIntervals.Count > 0)
result[y] = MergeIntervals(erodedIntervals);
});
return new HRegion { _rleData = new Dictionary<int, List<Interval>>(result) };
}
// 单步矩形腐蚀(分离为水平+垂直)
private HRegion ErosionRectSingleStep(HRegion region, int radius)
{
// 水平腐蚀(1x(2r+1)矩形)
var horiEroded = ErosionRect1D(region, radius, 0);
// 垂直腐蚀((2r+1)x1矩形)
var vertEroded = ErosionRect1D(horiEroded, 0, radius);
return vertEroded;
}
// 一维矩形腐蚀(高效实现)
private HRegion ErosionRect1D(HRegion region, int radiusX, int radiusY)
{
var result = new ConcurrentDictionary<int, List<Interval>>();
int totalRadius = radiusX + radiusY;
Parallel.ForEach(region._rleData.Keys, y =>
{
var erodedIntervals = new List<Interval>();
foreach (var interval in region._rleData[y])
{
int start = interval.Start + radiusX;
int end = interval.End - radiusX;
if (start > end) continue;
// 检查垂直方向
bool isValid = true;
for (int dy = -radiusY; dy <= radiusY; dy++)
{
int checkY = y + dy;
if (!region.IsIntervalCovered(checkY, start, end))
{
isValid = false;
break;
}
}
if (isValid)
erodedIntervals.Add(new Interval(start, end));
}
if (erodedIntervals.Count > 0)
result[y] = MergeIntervals(erodedIntervals);
});
return new HRegion { _rleData = new Dictionary<int, List<Interval>>(result) };
}
// 预计算圆形结构元素偏移量
private List<(int dx, int dy)> PrecomputeCircleOffsets(int radius)
{
var offsets = new List<(int, int)>();
int r2 = radius * radius;
for (int dy = -radius; dy <= radius; dy++)
for (int dx = -radius; dx <= radius; dx++)
if (dx * dx + dy * dy <= r2)
offsets.Add((dx, dy));
return offsets;
}
// 合并重叠/相邻区间
private List<Interval> MergeIntervals(List<Interval> intervals)
{
if (intervals.Count == 0) return intervals;
var merged = new List<Interval> { intervals[0] };
for (int i = 1; i < intervals.Count; i++)
{
var last = merged[merged.Count - 1];
if (intervals[i].Start <= last.End + 1)
merged[merged.Count - 1] = new Interval(last.Start, Math.Max(last.End, intervals[i].End));
else
merged.Add(intervals[i]);
}
return merged;
}
// 形态学操作 - 腐蚀
// 形态学操作 - 腐蚀(支持圆形和矩形结构元素)
public HRegion Erosion(int radius, bool useCircle = true)
{
HRegion result = new HRegion();
foreach (var kvp in _rleData)
{
int y = kvp.Key;
foreach (var interval in kvp.Value)
{
// 初始候选区间(先应用半径的简单收缩)
int candidateStart = interval.Start + radius;
int candidateEnd = interval.End - radius;
if (candidateStart > candidateEnd) continue;
// 检查候选区间内的每个点
List<Interval> validIntervals = new List<Interval>();
int currentStart = -1;
int currentEnd = -1;
for (int x = candidateStart; x <= candidateEnd; x++)
{
bool isValid = true;
if (useCircle)
{
// 圆形结构元素检查
for (int dy = -radius; dy <= radius; dy++)
{
for (int dx = -radius; dx <= radius; dx++)
{
// 检查是否在圆形内
if (dx * dx + dy * dy <= radius * radius)
{
int checkX = x + dx;
int checkY = y + dy;
if (!HasPixel(checkY, checkX, checkX))
{
isValid = false;
break;
}
}
}
if (!isValid) break;
}
}
else
{
// 矩形结构元素检查
for (int dy = -radius; dy <= radius; dy++)
{
for (int dx = -radius; dx <= radius; dx++)
{
int checkX = x + dx;
int checkY = y + dy;
if (!HasPixel(checkY, checkX, checkX))
{
isValid = false;
break;
}
}
if (!isValid) break;
}
}
// 处理连续的有效区间
if (isValid)
{
if (currentStart == -1)
{
currentStart = x;
currentEnd = x;
}
else if (x == currentEnd + 1)
{
currentEnd = x;
}
else
{
validIntervals.Add(new Interval(currentStart, currentEnd));
currentStart = x;
currentEnd = x;
}
}
}
// 添加最后一个区间
if (currentStart != -1)
{
validIntervals.Add(new Interval(currentStart, currentEnd));
}
// 将有效区间添加到结果中
if (validIntervals.Count > 0)
{
if (!result._rleData.TryGetValue(y, out var resultIntervals))
{
resultIntervals = new List<Interval>();
result._rleData[y] = resultIntervals;
}
resultIntervals.AddRange(validIntervals);
}
}
}
// 对每行的区间进行合并和排序
foreach (var intervals in result._rleData.Values)
{
intervals.Sort((a, b) => a.Start.CompareTo(b.Start));
// 合并重叠或相邻的区间
for (int i = 0; i < intervals.Count - 1;)
{
if (intervals[i].End + 1 >= intervals[i + 1].Start)
{
intervals[i] = new Interval(
Math.Min(intervals[i].Start, intervals[i + 1].Start),
Math.Max(intervals[i].End, intervals[i + 1].End));
intervals.RemoveAt(i + 1);
}
else
{
i++;
}
}
}
result._boundingBoxValid = false;
return result;
}
/// <summary>
/// 检查指定像素 (x,y) 是否被当前区域覆盖
/// </summary>
/// <param name="y">行坐标</param>
/// <param name="x">列坐标</param>
/// <returns>true如果像素在区域内</returns>
public bool IsPixelCovered(int y, int x)
{
// 1. 检查该行是否存在数据
if (!_rleData.TryGetValue(y, out var intervals))
return false;
// 2. 二分查找优化:快速定位x可能所在的区间
int left = 0;
int right = intervals.Count - 1;
while (left <= right)
{
int mid = (left + right) / 2;
var interval = intervals[mid];
if (x < interval.Start)
{
right = mid - 1;
}
else if (x > interval.End)
{
left = mid + 1;
}
else
{
return true; // x在区间[Start, End]内
}
}
return false;
}
/// <summary>
/// 检查指定行y上的区间[start, end]是否完全被覆盖
/// </summary>
public bool IsIntervalCovered(int y, int start, int end)
{
if (!_rleData.TryGetValue(y, out var intervals))
return false;
// 遍历所有区间,检查是否有一个区间完全包含[start, end]
foreach (var interval in intervals)
{
if (interval.Start <= start && interval.End >= end)
return true;
// 区间已排序,如果当前区间.Start > end,后续不可能有覆盖
if (interval.Start > end)
break;
}
return false;
}
// 开运算 = 先腐蚀后膨胀
public HRegion Opening(int radius)
{
return Erosion(radius).Dilation(radius);
}
// 闭运算 = 先膨胀后腐蚀
public HRegion Closing(int radius)
{
return Dilation(radius).Erosion(radius);
}
// 连通域分析
public static List<HRegion> Connection(HRegion region)
{
List<HRegion> connectedRegions = new List<HRegion>();
if (region._rleData.Count == 0) return connectedRegions;
// 使用并查集算法标记连通区域
Dictionary<Point, int> labels = new Dictionary<Point, int>();
int currentLabel = 1;
UnionFind uf = new UnionFind();
// 第一遍扫描: 分配临时标签
foreach (var kvp in region._rleData)
{
int y = kvp.Key;
foreach (var interval in kvp.Value)
{
for (int x = interval.Start; x <= interval.End; x++)
{
Point p = new Point(x, y);
// 检查相邻像素的标签
List<int> neighborLabels = new List<int>();
// 左邻
if (labels.TryGetValue(new Point(x - 1, y), out int leftLabel))
neighborLabels.Add(leftLabel);
// 上邻
if (labels.TryGetValue(new Point(x, y - 1), out int topLabel))
neighborLabels.Add(topLabel);
if (neighborLabels.Count == 0)
{
// 新标签
int newLabel = currentLabel++;
labels[p] = newLabel;
uf.MakeSet(newLabel);
}
else
{
// 使用最小的邻居标签
int minLabel = neighborLabels.Min();
labels[p] = minLabel;
// 合并等价类
foreach (var label in neighborLabels)
{
if (label != minLabel)
uf.Union(minLabel, label);
}
}
}
}
}
// 第二遍扫描: 分配最终标签并收集区域
Dictionary<int, HRegion> labelToRegion = new Dictionary<int, HRegion>();
foreach (var kvp in labels)
{
Point p = kvp.Key;
int label = uf.Find(kvp.Value);
if (!labelToRegion.TryGetValue(label, out HRegion connectedRegion))
{
connectedRegion = new HRegion();
labelToRegion[label] = connectedRegion;
}
if (!connectedRegion._rleData.TryGetValue(p.Y, out var intervals))
{
intervals = new List<Interval>();
connectedRegion._rleData[p.Y] = intervals;
}
// 尝试合并相邻的像素
bool merged = false;
for (int i = 0; i < intervals.Count; i++)
{
if (intervals[i].End + 1 == p.X)
{
intervals[i] = new Interval(intervals[i].Start, p.X);
merged = true;
break;
}
else if (intervals[i].Start - 1 == p.X)
{
intervals[i] = new Interval(p.X, intervals[i].End);
merged = true;
break;
}
}
if (!merged)
{
intervals.Add(new Interval(p.X, p.X));
// 保持区间有序
intervals.Sort((a, b) => a.Start.CompareTo(b.Start));
}
}
// 更新每个连通区域的边界框
foreach (var r in labelToRegion.Values)
{
r.UpdateBoundingBox();
connectedRegions.Add(r);
}
return connectedRegions;
}
// 并查集数据结构用于连通域分析
private class UnionFind
{
private Dictionary<int, int> parent = new Dictionary<int, int>();
private Dictionary<int, int> rank = new Dictionary<int, int>();
public void MakeSet(int x)
{
parent[x] = x;
rank[x] = 0;
}
public int Find(int x)
{
if (parent[x] != x)
parent[x] = Find(parent[x]);
return parent[x];
}
public void Union(int x, int y)
{
int xRoot = Find(x);
int yRoot = Find(y);
if (xRoot == yRoot) return;
if (rank[xRoot] < rank[yRoot])
parent[xRoot] = yRoot;
else if (rank[xRoot] > rank[yRoot])
parent[yRoot] = xRoot;
else
{
parent[yRoot] = xRoot;
rank[xRoot]++;
}
}
}
}
}
测试代码
运行速度在300ms左右,有点儿慢
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using static System.Net.Mime.MediaTypeNames;
using Image = System.Drawing.Image;
using System.Diagnostics; // 添加Stopwatch命名空间
namespace view3d
{
public partial class MainForm : Form
{
HRegion region = new HRegion();
Bitmap image;
public MainForm()
{
InitializeComponent();
}
private void load_Click(object sender, EventArgs e)
{
var stopwatch = Stopwatch.StartNew(); // 开始计时
// 创建打开文件对话框
OpenFileDialog openFileDialog = new OpenFileDialog();
// 设置文件过滤器(只显示图片文件)
openFileDialog.Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif";
// 设置对话框标题
openFileDialog.Title = "选择要加载的图片";
// 显示对话框并检查用户是否点击了确定
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
try
{
Image loadedImage = Image.FromFile(openFileDialog.FileName);
// 从文件路径创建Image对象
image = new Bitmap(loadedImage);
string v1 = textBox1.Text;
string v2 = textBox2.Text;
byte i1 = byte.Parse(v1);
byte i2 = byte.Parse(v2);
region.FromBitmap((Bitmap)image, i1, i2);
// 将图片显示到pictureBox1
pictureBox1.Image = region.ToBitmap((Bitmap)image, Color.White);
// 设置PictureBox的SizeMode为Zoom,使图片适应控件大小
pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
stopwatch.Stop(); // 停止计时
MessageBox.Show($"图片加载和处理耗时: {stopwatch.ElapsedMilliseconds} 毫秒", "耗时统计");
}
catch (Exception ex)
{
// 如果加载失败,显示错误信息
MessageBox.Show("加载图片时出错: " + ex.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void pictureBox1_Click(object sender, EventArgs e)
{
}
private void connection_Click(object sender, EventArgs e)
{
var stopwatch = Stopwatch.StartNew(); // 开始计时
List<HRegion> conns = HRegion.Connection(region);
using (Graphics g = Graphics.FromImage(image))
{
g.Clear(Color.Black);
}
// 预定义一组颜色,或者随机生成颜色
Color[] colors = new Color[]
{
Color.Red,
Color.Green,
Color.Blue,
Color.Yellow,
Color.Magenta,
Color.Cyan,
Color.Orange,
Color.Purple,
Color.Lime,
Color.Pink
};
for (int i = 0; i < conns.Count; i++)
{
HRegion region = conns[i];
if (region != null)
{
// 使用循环索引选择颜色,如果超过预定义颜色数量则随机生成
Color regionColor = i < colors.Length ? colors[i] : GetRandomColor();
region.ToBitmap(image, regionColor, false);
}
}
stopwatch.Stop(); // 停止计时
MessageBox.Show($"连通域分析耗时: {stopwatch.ElapsedMilliseconds} 毫秒", "耗时统计");
// 刷新显示
pictureBox1.Invalidate();
}
// 辅助方法:生成随机颜色
private Color GetRandomColor()
{
Random rand = new Random();
return Color.FromArgb(rand.Next(256), rand.Next(256), rand.Next(256));
}
private void Dilation_Click(object sender, EventArgs e)
{
var stopwatch = Stopwatch.StartNew(); // 开始计时
string v1 = textBox1.Text;
string v2 = textBox2.Text;
byte i1 = byte.Parse(v1);
byte i2 = byte.Parse(v2);
region = region.Dilation(i2);
region.ToBitmap(image, Color.Yellow);
stopwatch.Stop(); // 停止计时
MessageBox.Show($"膨胀操作耗时: {stopwatch.ElapsedMilliseconds} 毫秒", "耗时统计");
// 刷新显示
pictureBox1.Invalidate();
}
private void button2_Click(object sender, EventArgs e)
{
var stopwatch = Stopwatch.StartNew(); // 开始计时
string v1 = textBox1.Text;
string v2 = textBox2.Text;
byte i1 = byte.Parse(v1);
byte i2 = byte.Parse(v2);
region = region.ErosionOptimized(i2);
region.ToBitmap(image, Color.Green);
stopwatch.Stop(); // 停止计时
MessageBox.Show($"腐蚀操作耗时: {stopwatch.ElapsedMilliseconds} 毫秒", "耗时统计");
// 刷新显示
pictureBox1.Invalidate();
}
}
}
测试demo下载
见资源标定