【零基础入门】Python机器视觉第六阶段:综合项目实战——构建工业缺陷检测原型系统
【零基础入门】Python机器视觉第六阶段:综合项目实战——构建工业缺陷检测原型系统
经过前五个阶段的学习,我们已经掌握了Python基础、OpenCV图像处理、传统算法、深度学习(PyTorch)以及YOLOv8目标检测。现在,是时候将这些知识整合起来,构建一个接近真实工业场景的缺陷检测原型系统了。
本阶段我们将以C# WPF作为上位机界面,Python YOLOv8作为检测服务,并模拟工业相机和运动控制,实现一个完整的“运动→采图→检测→分选”闭环系统。即使没有真实硬件,我们也能通过模拟方式完成全部开发,最终产出一个可以写入简历的项目。
本文所有代码均可直接复制运行,注释非常详细。建议按照步骤逐步实现。
一、本阶段学习目标
- 掌握C#与Python的混合编程(进程调用方式)
- 学会模拟工业相机(
MockCameraService) - 学会模拟运动控制卡(
MotionController) - 掌握WPF MVVM模式的基本使用
- 能够将YOLOv8检测服务集成到C#应用中
- 构建一个完整的演示系统,实现多工位检测流程
- 学会项目结构组织和简历撰写技巧
二、系统整体架构
2.1 架构图
┌─────────────────────────────────────────────────────────────┐
│ C# 上位机 (WPF) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ 主界面 │ │ 运动控制服务 │ │ 相机服务 │ │
│ │ (MVVM) │◄─┤ (Motion) │ │ (MockCamera) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └─────────────────┼────────────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │检测服务调用 │ │
│ │(Python进程) │ │
│ └──────┬──────┘ │
└───────────────────────────┼────────────────────────────────────┘
│
┌───────▼────────┐
│ Python 检测服务 │
│ (YOLOv8模型) │
└────────────────┘
2.2 技术栈
| 模块 | 技术 | 说明 |
|---|---|---|
| 上位机界面 | WPF (C#) | 采用MVVM模式,实时显示图像和检测结果 |
| 运动控制 | C# (模拟) | 用Timer模拟脉冲输出和位置移动 |
| 相机模拟 | C# | 从本地文件夹循环读取图片,模拟视频流 |
| 检测服务 | Python + YOLOv8 | 封装成命令行调用,接收图像路径返回JSON结果 |
| 通信方式 | 进程调用 | C#启动Python进程,传递参数,读取标准输出 |
三、工业相机模拟(MockCameraService)
在没有真实工业相机的情况下,我们需要一个模拟相机,它能从本地文件夹循环读取图片,模拟相机采集过程。
3.1 创建C#模拟相机类
using System;
using System.IO;
using System.Timers;
using OpenCvSharp;
public class MockCameraService : IDisposable
{
private Timer _timer;
private string[] _imageFiles;
private int _currentIndex = 0;
private Action<Mat> _frameCallback;
private bool _isGrabbing = false;
/// <summary>
/// 初始化模拟相机
/// </summary>
/// <param name="imageFolder">包含图片的文件夹路径</param>
/// <param name="fps">模拟帧率</param>
public MockCameraService(string imageFolder, double fps = 30)
{
if (!Directory.Exists(imageFolder))
throw new DirectoryNotFoundException($"文件夹不存在: {imageFolder}");
_imageFiles = Directory.GetFiles(imageFolder, "*.jpg");
if (_imageFiles.Length == 0)
_imageFiles = Directory.GetFiles(imageFolder, "*.png");
if (_imageFiles.Length == 0)
throw new Exception("文件夹中没有jpg或png图片");
Array.Sort(_imageFiles); // 按文件名排序
_timer = new Timer(1000 / fps);
_timer.Elapsed += Timer_Elapsed;
Console.WriteLine($"模拟相机初始化完成,加载 {_imageFiles.Length} 张图片");
}
/// <summary>
/// 连接相机
/// </summary>
public bool Connect()
{
Console.WriteLine("模拟相机连接成功");
return true;
}
/// <summary>
/// 开始采集
/// </summary>
/// <param name="callback">每帧回调函数,接收Mat图像</param>
public void StartGrabbing(Action<Mat> callback)
{
_frameCallback = callback;
_isGrabbing = true;
_timer.Start();
Console.WriteLine("模拟相机开始采集");
}
/// <summary>
/// 停止采集
/// </summary>
public void StopGrabbing()
{
_timer.Stop();
_isGrabbing = false;
Console.WriteLine("模拟相机停止采集");
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
if (!_isGrabbing || _frameCallback == null) return;
// 读取当前图片
string imgPath = _imageFiles[_currentIndex];
Mat frame = Cv2.ImRead(imgPath, ImreadModes.Color);
if (frame.Empty())
{
Console.WriteLine($"读取图片失败: {imgPath}");
return;
}
// 调用回调
_frameCallback(frame);
// 移动到下一张
_currentIndex = (_currentIndex + 1) % _imageFiles.Length;
}
public void Dispose()
{
StopGrabbing();
_timer?.Dispose();
}
}
3.2 使用示例
// 在Main方法中测试
var camera = new MockCameraService(@"D:\test_images", 30);
camera.Connect();
camera.StartGrabbing(frame =>
{
Cv2.ImShow("Mock Camera", frame);
Cv2.WaitKey(1);
});
Console.WriteLine("按任意键停止...");
Console.ReadKey();
camera.StopGrabbing();
camera.Dispose();
Cv2.DestroyAllWindows();
四、运动控制模拟(MotionController)
运动控制是工业设备的核心,我们模拟雷赛运动控制卡的基本功能:移动到指定位置和触发外部信号。
4.1 创建模拟运动控制类
using System;
using System.Threading;
public class MotionController
{
private int _currentPosition = 0;
private readonly object _lock = new object();
/// <summary>
/// 移动到绝对位置(模拟脉冲输出)
/// </summary>
/// <param name="targetPosition">目标位置(单位:脉冲数或mm)</param>
public bool MoveTo(int targetPosition)
{
Console.WriteLine($"[运动] 开始移动: 当前位置 {_currentPosition} -> 目标 {targetPosition}");
// 模拟脉冲输出(这里用延时模拟)
int distance = Math.Abs(targetPosition - _currentPosition);
int steps = distance / 10; // 假设每10个脉冲移动1个单位
for (int i = 0; i < steps; i++)
{
Thread.Sleep(10); // 每个脉冲耗时10ms
lock (_lock)
{
if (_currentPosition < targetPosition)
_currentPosition += 10;
else
_currentPosition -= 10;
}
// 可以在这里触发位置到达事件
}
lock (_lock)
{
_currentPosition = targetPosition;
}
Console.WriteLine($"[运动] 到达目标位置 {targetPosition}");
return true;
}
/// <summary>
/// 触发外部信号(模拟相机拍照触发)
/// </summary>
public void TriggerCamera()
{
Console.WriteLine("[运动] 发送拍照触发信号");
// 实际项目中这里可能会触发一个硬件IO,或调用相机接口
}
/// <summary>
/// 读取当前位置
/// </summary>
public int ReadPosition()
{
lock (_lock)
{
return _currentPosition;
}
}
/// <summary>
/// 归零
/// </summary>
public void Home()
{
Console.WriteLine("[运动] 开始归零");
// 模拟归零过程
Thread.Sleep(500);
_currentPosition = 0;
Console.WriteLine("[运动] 归零完成");
}
}
4.2 使用示例
var motion = new MotionController();
motion.MoveTo(1000);
motion.TriggerCamera();
Console.WriteLine($"当前位置: {motion.ReadPosition()}");
motion.Home();
五、C#调用Python检测服务(封装)
我们采用进程调用方式,将YOLOv8检测服务封装成可重用的类。Python脚本接收图像路径,返回JSON格式的检测结果。
5.1 Python检测脚本(detect_defect.py)
import sys
import json
import cv2
from ultralytics import YOLO
def main():
if len(sys.argv) < 2:
print(json.dumps({"error": "缺少图像路径"}))
return
image_path = sys.argv[1]
try:
# 加载模型(实际项目中可全局加载一次,这里简化)
model = YOLO('best.pt') # 训练好的模型路径
# 推理
results = model(image_path)[0]
detections = []
if len(results.boxes) > 0:
boxes = results.boxes.xyxy.cpu().numpy()
confs = results.boxes.conf.cpu().numpy()
classes = results.boxes.cls.cpu().numpy()
for box, conf, cls in zip(boxes, confs, classes):
detections.append({
"class_name": results.names[int(cls)],
"confidence": float(conf),
"x1": float(box[0]),
"y1": float(box[1]),
"x2": float(box[2]),
"y2": float(box[3])
})
print(json.dumps(detections))
except Exception as e:
print(json.dumps({"error": str(e)}))
if __name__ == "__main__":
main()
5.2 C#封装类(PythonDetector.cs)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
public class DefectResult
{
public string ClassName { get; set; }
public float Confidence { get; set; }
public float X1 { get; set; }
public float Y1 { get; set; }
public float X2 { get; set; }
public float Y2 { get; set; }
public string BboxString => $"({X1:F0},{Y1:F0}) {X2 - X1:F0}x{Y2 - Y1:F0}";
}
public class PythonDetector
{
private string _pythonExe;
private string _scriptPath;
/// <summary>
/// 初始化检测器
/// </summary>
/// <param name="pythonExe">Python解释器路径(如:@"C:\Users\xxx\anaconda3\python.exe")</param>
/// <param name="scriptPath">Python脚本路径</param>
public PythonDetector(string pythonExe, string scriptPath)
{
_pythonExe = pythonExe;
_scriptPath = scriptPath;
}
/// <summary>
/// 检测单张图片
/// </summary>
/// <param name="imagePath">图像路径</param>
/// <returns>检测结果列表</returns>
public List<DefectResult> Detect(string imagePath)
{
var startInfo = new ProcessStartInfo
{
FileName = _pythonExe,
Arguments = $"\"{_scriptPath}\" \"{imagePath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};
using (Process process = Process.Start(startInfo))
{
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrEmpty(error))
throw new Exception($"Python错误: {error}");
try
{
// 尝试解析为错误对象
var errorObj = JsonSerializer.Deserialize<Dictionary<string, string>>(output);
if (errorObj != null && errorObj.ContainsKey("error"))
throw new Exception($"检测错误: {errorObj["error"]}");
}
catch { /* 不是错误对象,继续 */ }
try
{
return JsonSerializer.Deserialize<List<DefectResult>>(output);
}
catch (Exception ex)
{
throw new Exception($"解析结果失败: {ex.Message}\n原始输出: {output}");
}
}
}
}
5.3 使用示例
var detector = new PythonDetector(
@"C:\Users\Administrator\anaconda3\python.exe",
@"D:\PythonProject\detect_defect.py"
);
try
{
var results = detector.Detect(@"D:\test.jpg");
Console.WriteLine($"检测到 {results.Count} 个缺陷");
foreach (var r in results)
{
Console.WriteLine($" {r.ClassName}: {r.Confidence:P2} at {r.BboxString}");
}
}
catch (Exception ex)
{
Console.WriteLine($"检测失败: {ex.Message}");
}
六、WPF主界面开发(MVVM模式)
我们将创建一个简单的WPF窗口,包含开始/停止按钮、图像显示区域、缺陷列表和状态日志。
6.1 新建WPF项目
在Visual Studio中新建一个WPF应用(.NET Core 3.1或.NET 5/6/7/8均可),命名为DefectInspectionSystem。
6.2 安装NuGet包
OpenCvSharp4(用于图像处理)OpenCvSharp4.WpfExtensions(用于将Mat转为BitmapSource)
在包管理器控制台中执行:
Install-Package OpenCvSharp4
Install-Package OpenCvSharp4.WpfExtensions
6.3 主界面XAML(MainWindow.xaml)
<Window x:Class="DefectInspectionSystem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="工业缺陷检测原型系统" Height="600" Width="900"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 顶部控制栏 -->
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Orientation="Horizontal" Margin="0,0,0,10">
<Button x:Name="BtnStart" Content="开始检测" Width="80" Height="30" Margin="5" Click="BtnStart_Click"/>
<Button x:Name="BtnStop" Content="停止检测" Width="80" Height="30" Margin="5" Click="BtnStop_Click"/>
<TextBlock Text="状态:" VerticalAlignment="Center" Margin="20,0,0,0"/>
<TextBlock x:Name="TxtStatus" Text="就绪" VerticalAlignment="Center" Margin="5,0,0,0" Foreground="Green"/>
</StackPanel>
<!-- 左侧信息面板 -->
<StackPanel Grid.Row="1" Grid.Column="0" Margin="5">
<GroupBox Header="检测统计" Height="120">
<StackPanel Margin="5">
<TextBlock x:Name="TxtTotal" Text="检测总数: 0"/>
<TextBlock x:Name="TxtDefect" Text="缺陷总数: 0" Margin="0,5,0,0"/>
<TextBlock x:Name="TxtPosition" Text="当前位置: 0" Margin="0,5,0,0"/>
</StackPanel>
</GroupBox>
<GroupBox Header="缺陷列表" Height="300" Margin="0,10,0,0">
<ListView x:Name="DefectListView">
<ListView.View>
<GridView>
<GridViewColumn Header="类型" DisplayMemberBinding="{Binding ClassName}" Width="60"/>
<GridViewColumn Header="置信度" DisplayMemberBinding="{Binding Confidence, StringFormat={}{0:P2}}" Width="80"/>
<GridViewColumn Header="位置" DisplayMemberBinding="{Binding BboxString}" Width="100"/>
</GridView>
</ListView.View>
</ListView>
</GroupBox>
</StackPanel>
<!-- 右侧图像显示 -->
<Border Grid.Row="1" Grid.Column="1" BorderBrush="Gray" BorderThickness="1" Margin="5">
<Image x:Name="ImgDisplay" Stretch="Uniform"/>
</Border>
<!-- 底部日志 -->
<TextBox Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
x:Name="TxtLog" Height="80" Margin="5"
IsReadOnly="True" VerticalScrollBarVisibility="Auto"
TextWrapping="Wrap"/>
</Grid>
</Window>
6.4 后台代码(MainWindow.xaml.cs)
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
namespace DefectInspectionSystem
{
public partial class MainWindow : Window
{
private MockCameraService _camera;
private MotionController _motion;
private PythonDetector _detector;
private CancellationTokenSource _cts;
private ObservableCollection<DefectResult> _defects = new ObservableCollection<DefectResult>();
private int _totalDetected = 0;
private int _defectCount = 0;
public MainWindow()
{
InitializeComponent();
// 初始化服务
string pythonExe = @"C:\Users\Administrator\anaconda3\python.exe"; // 修改为你的路径
string scriptPath = @"D:\PythonProject\detect_defect.py"; // 修改为你的脚本路径
string imageFolder = @"D:\test_images"; // 模拟相机图片文件夹
_detector = new PythonDetector(pythonExe, scriptPath);
_camera = new MockCameraService(imageFolder, 10); // 10fps,放慢速度便于观察
_camera.Connect();
_motion = new MotionController();
DefectListView.ItemsSource = _defects;
}
private async void BtnStart_Click(object sender, RoutedEventArgs e)
{
_cts = new CancellationTokenSource();
SetStatus("检测中...", Colors.Orange);
Log("开始检测流程");
try
{
await Task.Run(() => InspectionLoop(_cts.Token));
}
catch (OperationCanceledException)
{
Log("检测被用户取消");
}
catch (Exception ex)
{
Log($"检测异常: {ex.Message}");
}
finally
{
SetStatus("就绪", Colors.Green);
}
}
private void BtnStop_Click(object sender, RoutedEventArgs e)
{
_cts?.Cancel();
_camera.StopGrabbing();
}
/// <summary>
/// 检测主循环(模拟多工位)
/// </summary>
private void InspectionLoop(CancellationToken token)
{
// 模拟多个检测位置
int[] positions = { 100, 200, 300, 400, 500 };
_camera.StartGrabbing(frame =>
{
if (token.IsCancellationRequested)
{
_camera.StopGrabbing();
return;
}
try
{
// 显示当前帧
Dispatcher.Invoke(() =>
{
var bitmap = WriteableBitmapConverter.ToWriteableBitmap(frame);
ImgDisplay.Source = bitmap;
});
// 移动到一个位置(这里简化,实际应关联位置)
// 注意:不能在回调中执行耗时操作,但为了简单起见,我们在这里做检测
// 更好的做法是:检测用另一个线程
int currentPos = positions[new Random().Next(positions.Length)];
Dispatcher.Invoke(() => TxtPosition.Text = $"当前位置: {currentPos}");
// 模拟运动完成
_motion.MoveTo(currentPos);
_motion.TriggerCamera();
// 临时保存图像到文件(因为Python检测需要文件路径)
string tempFile = System.IO.Path.GetTempFileName() + ".jpg";
frame.SaveImage(tempFile);
// 调用检测
var results = _detector.Detect(tempFile);
// 更新UI
Dispatcher.Invoke(() =>
{
_totalDetected++;
_defects.Clear();
foreach (var r in results)
{
_defects.Add(r);
}
_defectCount += results.Count;
TxtTotal.Text = $"检测总数: {_totalDetected}";
TxtDefect.Text = $"缺陷总数: {_defectCount}";
Log($"位置 {currentPos}: 检测到 {results.Count} 个缺陷");
});
// 模拟分选动作
if (results.Count > 0)
{
// 触发分选(此处可调用运动控制)
Console.WriteLine("触发分选");
}
// 清理临时文件
try { System.IO.File.Delete(tempFile); } catch { }
}
catch (Exception ex)
{
Dispatcher.Invoke(() => Log($"处理帧异常: {ex.Message}"));
}
});
// 模拟运行一段时间(实际应等待用户停止)
while (!token.IsCancellationRequested)
{
Thread.Sleep(100);
}
}
private void SetStatus(string status, Color color)
{
Dispatcher.Invoke(() =>
{
TxtStatus.Text = status;
TxtStatus.Foreground = new SolidColorBrush(color);
});
}
private void Log(string message)
{
Dispatcher.Invoke(() =>
{
TxtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\n");
TxtLog.ScrollToEnd();
});
}
protected override void OnClosed(EventArgs e)
{
_camera?.StopGrabbing();
_camera?.Dispose();
_cts?.Cancel();
base.OnClosed(e);
}
}
}
6.5 解决跨线程UI更新问题
WPF中非UI线程不能直接更新UI控件,我们使用Dispatcher.Invoke来安全更新。
七、完整项目结构
DefectInspectionSystem/
├── DefectInspectionSystem.csproj
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── Services/
│ ├── MockCameraService.cs
│ ├── MotionController.cs
│ └── PythonDetector.cs
├── Models/
│ └── DefectResult.cs
├── PythonScripts/
│ └── detect_defect.py
└── test_images/ # 模拟图片文件夹
├── img1.jpg
├── img2.jpg
└── ...
7.1 在Visual Studio中组织
- 创建
Services文件夹,添加上述三个服务类。 - 创建
Models文件夹,添加DefectResult.cs。 - 创建
PythonScripts文件夹,将detect_defect.py和训练好的best.pt放入其中。 - 在
bin/Debug/netX.X/下创建test_images文件夹,放入一些测试图片。
八、运行与测试
- 确保Python环境可用,且已安装
ultralytics、opencv-python。 - 将训练好的YOLOv8模型
best.pt放在Python脚本同目录。 - 修改代码中的Python解释器路径和脚本路径为实际路径。
- 按F5运行,点击“开始检测”,观察界面实时显示图像和检测结果。
九、项目亮点与简历撰写
9.1 项目亮点
- 全流程闭环:从运动控制、图像采集、算法检测到分选逻辑,模拟真实工业场景。
- 跨语言协作:C#负责界面和运动控制,Python负责深度学习检测,充分发挥各自优势。
- 模块化设计:相机、运动、检测均为独立服务,易于替换真实硬件。
- 模拟硬件:无真实相机/板卡仍可开发调试,节约成本。
9.2 简历描述模板
工业缺陷检测原型系统(2025.03 - 至今)
技术栈:C# · WPF · Python · PyTorch · YOLOv8 · OpenCV
- 项目概述:设计并实现一套模拟工业环境的硅片表面缺陷检测系统,包含运动控制、图像采集、深度学习检测、结果展示四大模块。
- 核心贡献:
- 基于YOLOv8训练表面缺陷检测模型,在NEU数据集上达到mAP50 0.95,支持划痕、脏污、裂纹等4类缺陷识别。
- 使用C# WPF开发上位机界面,采用MVVM模式,实时显示检测图像和缺陷列表。
- 通过进程调用实现C#与Python混合编程,Python负责模型推理,C#负责流程控制。
- 模拟运动控制卡,实现多工位自动检测与分选逻辑,单次检测周期控制在2秒内。
- 设计MockCameraService模拟工业相机采图,无需硬件即可完成全流程开发测试。
9.3 GitHub上传建议
- 创建仓库
DefectInspectionSystem - 编写README.md,包含项目简介、技术栈、运行截图、如何运行
- 忽略不需要的文件(如
.vs、bin、obj) - 附上一张界面截图
十、总结与下一步
通过本阶段的学习,你已经成功构建了一个工业缺陷检测原型系统,它集成了:
- ✅ Python深度学习模型(YOLOv8)
- ✅ C# WPF上位机界面
- ✅ 模拟相机和运动控制
- ✅ 完整的检测流程
下一步:进入第七阶段——AI模型部署,学习如何将模型转换为ONNX、使用TensorRT/OpenVINO加速,并在边缘设备上运行。
📚 参考文档链接
- WPF MVVM模式教程 —— 微软官方教程
- C#调用Python进程 —— Process类详解
- OpenCvSharp文档 —— C#版OpenCV
- YOLOv8官方文档 —— 模型训练与导出
- .NET 6+WPF+MVVM调用摄像头进行识别 —— 实战案例
- C#+WPF+Opencv模块化开发视觉对位系统 —— 工业视觉参考
如果在实现过程中遇到任何问题,欢迎随时交流!下一阶段我们将进入更深入的AI模型部署,敬请期待。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)