news 2026/9/14 1:48:51

C#实现DICOM图像自动接收与上传:集成Basler相机和Halcon显示

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#实现DICOM图像自动接收与上传:集成Basler相机和Halcon显示

简介:这份资源是一套基于C#的Dicom医学图像自动接收与上传系统源码,面向医疗影像开发、工业视觉及自动化采集场景的开发者,解决Basler相机实时采集、Halcon视觉显示及Dicom数据上传等综合需求。压缩包共49个文件,约18.78MB,主要包含C#源码文件(13个cs)、Halcon与Basler相关动态库(5个dll)、可执行程序(4个exe)及配置文件,并含登录注册、Dicom解析、C-STORE SCP服务与自动上传等模块;配套sln解决方案可直接编译,目录结构清晰,便于逐模块学习。已有263人学习浏览。通过源码可掌握Basler相机自动重连机制、Halcon控件集成、Dicom协议通信及用户权限管理等关键实现,对想理解医疗图像采集上传全流程的C#开发者有实际参考价值。

1. 接收链路先于采集:这套C#源码真正值钱的地方

拿到这份0698+C#+Dicom图像自动接收并上传源码,很多人第一反应是找Basler相机的采集代码,但打开工程会发现核心入口是CStoreSCP.csAutoUpload.cs。这意味着整套系统的定位不是“相机采集工具”,而是一条完整的医疗影像流转链路:Dicom图像通过C-STORE协议自动接收,解析患者信息后落盘,再由AutoUpload模块推送到上级服务器;Basler相机采集和Halcon控件显示只是这条链路的图像来源与预览终端。对于正在做PACS/RIS对接、医学影像上传中间件,或者想把工业相机采集的影像以DICOM标准格式汇入现有系统的开发者,这套代码比单独一个Halcon示例有价值得多。工程基于VS2015-2019均可打开(x86平台),代码里还带了登录注册模块和basler自动重连逻辑,后面会逐个拆。

2. 工程结构与DICOM接收链路的三个关键类

先说工程轮廓,再看代码。解决方案LoginRegister.sln下,窗体层有Login.csUploadMain.cs,业务层集中在AutoUpload.csCStoreSCP.csStudyDicom.csUtility.cs,配置在app.config。这种分层不是教科书式设计,而是真实上位机项目里最常见的“窗体-服务-工具”三分法。

2.1 CStoreSCP:用C#实现一个标准DICOM服务类提供者

CStoreSCP.cs是整条接收链路的起点。它本质上是DICOM标准里的Service Class Provider,监听指定端口,等待远端设备(如CT、MR或工作站)发起C-STORE请求。收到请求后,OnCStoreRequest回调里拿到DICOM数据集,再用Save()写入本地目录。

// CStoreSCP.cs 核心片段(代码结构参考工程还原) using Dicom; using Dicom.Network; public class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvider { private readonly string _storagePath; public CStoreSCP(Stream stream, Encoding fallbackEncoding, Logger log, string storagePath) : base(stream, fallbackEncoding, log) { _storagePath = storagePath; } public async Task OnCStoreRequestAsync(DicomCStoreRequest request) { var dataset = request.Dataset; string filePath = Path.Combine(_storagePath, $"{dataset.GetSingleValue<string>(DicomTag.StudyInstanceUID)}.dcm"); await dataset.SaveAsync(filePath); Console.WriteLine($"[SCP] 接收完成: {filePath}"); } }

参数说明:DicomCStoreRequest.Dataset是完整DICOM数据集,包括患者姓名、检查号、图像像素数据。保存时用StudyInstanceUID作为文件名,能避免同名覆盖,但同一检查的多序列会被覆盖掉,这是简化逻辑,实际生产建议用SOPInstanceUIDDicomService是fo-dicom库提供的基类,这套源码用的就是这个库,在NuGet里版本3.x/4.x都有大量使用。

2.2 StudyDicom与Utility:文件解析和上传前的数据规整

StudyDicom.cs负责把接收到的DICOM文件解析成PatientInfo.cs定义的数据结构。Utility.cs则承担了目录创建、文件重命名、时间戳格式化等杂活。这里我一般会关注PatientInfo里映射了哪些Tag——如果只映射了PatientName和PatientID,对接上层系统时大概率还要补Tag。

DICOM Tag关键字PatientInfo属性
(0010,0010)PatientNamePatientName
(0010,0020)PatientIDPatientId
(0020,000D)StudyInstanceUIDStudyUid
(0008,0060)ModalityModality
(0028,0010)RowsRows
(0028,0011)ColumnsColumns

解析完成的数据,一部分进入UI展示,另一部分传给AutoUpload。这里有个容易忽略的点:医疗影像上传不是简单POST文件,而是要把患者信息、检查信息、文件路径组装成JSON或multipart/form-data,再按接口协议推PACS服务器。

2.3 AutoUpload与网络重试:从本地到远端的可靠推送

AutoUpload.cs的职责是扫描接收目录,把新文件传上去。工程里用的是HttpWebRequest/HttpClient,常见做法是循环遍历目录,遇到.dcm文件就按配置的URL上传,成功后移动文件到backup目录,失败则保留原文件并记录日志。

// AutoUpload.cs 核心片段 public bool UploadFile(string filePath) { string url = ConfigurationManager.AppSettings["UploadUrl"]; using (var client = new HttpClient()) using (var content = new MultipartFormDataContent()) { var fileBytes = File.ReadAllBytes(filePath); content.Add(new ByteArrayContent(fileBytes), "file", Path.GetFileName(filePath)); HttpResponseMessage resp = client.PostAsync(url, content).Result; return resp.IsSuccessStatusCode; } }

参数说明:UploadUrlapp.config里配成Web API地址。医疗场景下有内网PACS,URL多为http://pacs-server:8080/dicom/upload。上面用的是同步.Result写法,WinForm里直接调用会卡界面,正确做法是异步await或放到后台线程。

2.4 无界面运行:把SCP服务挂到后台线程

Program.cs里能看到整个应用不是单一窗体,而是先启动SCP监听服务,再弹出登录窗体和主窗体。实际部署时,可以用WSF(Windows Service)方式承载CStoreSCP,把控制台项目改成Windows服务即可。注意DICOM服务端口,一般用104(默认),但非管理员权限下建议用30000以上的端口,同时要在防火墙放行TCP入站规则。

3. Basler相机采集与Halcon控件显示的集成实现

接收链路讲完,回到标题里的另外半壁江山。UploadMain.cs里的图像区域用的是Halcon的HSmartWindowControl或者HWindowControl,采集端通过Basler的pylon SDK拉流。这个组合在工业视觉和医疗影像设备的关键词里出现频率很高:Basler负责硬触发/软触发采集,Halcon负责显示和图像分析。

3.1 相机实例化的最佳实践

Basler官方SDK提供了Basler.Pylon.Icamera接口。C#里创建相机对象后,要显式设置采集模式、像素格式、曝光时间。工程里常见写法如下:

using Basler.Pylon; Camera camera = new Camera(); camera.CameraOpened += Camera_CameraOpened; camera.Open(); // 配置参数 camera.Parameters[PLCamera.PixelFormat].SetValue(PLCamera.PixelFormat.Mono8); camera.Parameters[PLCamera.ExposureTimeAbs].SetValue(20000.0); // 单位: us camera.Parameters[PLCamera.AcquisitionMode].SetValue(PLCamera.AcquisitionMode.SingleFrame); camera.StreamGrabber.ImageGrabbed += StreamGrabber_ImageGrabbed;

参数说明:ExposureTimeAbs在pylon 5.x以上版本变为ExposureTime,单位也改成ns,老代码迁到新SDK时报错基本都在这。AcquisitionMode.SingleFrame表示单帧采集,连续采集中要用ContinuousImageGrabbed事件每采到一帧就会触发,回调里拿IGrabResult对象再转成Halcon图像。

3.2 Halcon图像显示控件如何嵌入WinForm

Halcon安装后会在VS工具箱里添加HSmartWindowControlHWindowControl。两者区别:HWindowControl是原生窗口控件,操作窗口句柄,显示速度更快;HSmartWindowControl是封装控件,支持鼠标缩放、平移,适合交互。工程里做图像预览,用HSmartWindowControl更合适。

using HalconDotNet; HObject hoImage; HTuple width, height; private void UpdateHalconWindow(byte[] frameData, int imgWidth, int imgHeight) { if (halconWindow == null) return; // 由灰度数组创建HObject hoImage = new HObject(); hoImage.GenImage1("byte", imgWidth, imgHeight, frameData); // 自动适应窗口大小并显示 halconWindow.HalconWindow.SetPart(0, 0, imgHeight - 1, imgWidth - 1); halconWindow.HalconWindow.DispObj(hoImage); }

逻辑说明:这段代码在ImageGrabbed事件里调用,frameData是Basler的GrabResult.PixelDataSetPart设置显示区域,不然图像会拉伸变形。高频采集时(30fps以上),每次都GenImage1会造成GC频繁回收,优化方式是把HObject复用,只更新像素数据。

3.3 自动重连机制:断线检测与恢复

basler自动重连在源码里是个亮点。工业相机长时间运行,USB3.0 / GigE 接口的拔插、休眠唤醒都会导致连接断开。常见做法是:在采集线程里循环检测IsConnected,失败后释放资源,再按指数退避重试。

// 自动重连逻辑(工程思路还原) int retryDelay = 1000; while (true) { try { if (!camera.IsConnected) { Log("连接断开, 尝试重连: " + retryDelay + "ms"); Thread.Sleep(retryDelay); camera.Open(); camera.StreamGrabber.Start(); retryDelay = 1000; // 恢复默认间隔 } // 正常采集 ProcessFrame(); } catch (Exception ex) { retryDelay = Math.Min(retryDelay * 2, 15000); Thread.Sleep(retryDelay); } }

注意点:USB相机重连后设备索引可能变化,Camera对象必须重新枚举。GigE相机还有个坑,IP地址变更会导致Camera.IP失效,重连前要先GigeTransportLayer刷新设备列表。重连和采集最好放在独立线程或Task里,别占用UI线程,否则会出现热搜词里“c# 循环数据采集和ui刷新卡顿”的典型问题。

4. Dicom文件解析与自动上传的完整流程实现

这一章把链路串起来:接收DICOM → 解析 → 上传,配合登录权限模块,构成完整的医疗数据中转站。源码里Login.csLoginRegister.sln对应着登录注册,放到医疗场景里就是操作员权限控制。

4.1 从CStoreSCP到StudyDicom的调用链

远端设备推图时,CStoreSCP回调触发,这时StudyDicom.cs被调用,用DicomFile.Open读取刚保存的文件,然后遍历DICOM库里的常用标签,组装成业务对象。

public PatientInfo ParseDicomFile(string filePath) { var dcm = DicomFile.Open(filePath); var dataset = dcm.Dataset; var info = new PatientInfo(); info.PatientId = dataset.GetSingleValueOrDefault(DicomTag.PatientID, "unknown"); info.PatientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, "unknown").ToString(); info.Modality = dataset.GetSingleValueOrDefault(DicomTag.Modality, "UN"); info.StudyUid = dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, Guid.NewGuid().ToString()); info.Rows = dataset.GetSingleValueOrDefault(DicomTag.Rows, 0); info.Columns = dataset.GetSingleValueOrDefault(DicomTag.Columns, 0); return info; }

参数说明:GetSingleValueOrDefault是fo-dicom提供的健壮取值方法,避免某个Tag缺失时抛异常。PatientName返回的是DicomPersonName结构体,直接ToString()得到“姓氏^名字”格式,与HL7标准的PN类型一致,上传到PACS前可能需要拆分成FamilyNameGivenName两个字段。

4.2 上传任务的队列化处理

直接同步上传会导致接收和上传互相阻塞,尤其在高通量场景,SCP线程会被IO卡死。工程思路一般是引入队列:CStoreSCP接收完成后把文件路径放入队列,后台线程从队列取文件上传,成功则删除,失败则重试。

// 上传队列伪代码结构 ConcurrentQueue<string> uploadQueue = new ConcurrentQueue<string>(); void OnFileReceived(string path) { uploadQueue.Enqueue(path); } void UploadWorkerLoop() { while (true) { if (uploadQueue.TryDequeue(out string filePath)) { bool ok = UploadFile(filePath); if (ok) File.Delete(filePath); else reQueue(filePath); // 超过3次则移入failed目录 } Thread.Sleep(100); } }

逻辑说明:ConcurrentQueue是线程安全队列,接收线程和生产线程分离。重试超过3次移入failed目录,避免死循环。工程里还常配一个定时器扫描待上传目录,应对“上传程序中途崩溃,文件残留”的情况——这是可靠传输很重要的兜底。

4.3 app.config中的关键配置项

配置项示例值说明
DicomPort11112SCP监听端口,医院内网常用104或11112
StoragePathD:\DicomData\Inbox接收DICOM文件的落盘目录
UploadUrlhttp://pacs/api/dicom/upload上级平台的上传接口
RetryCount3单个文件最大重试次数
AutoReconnecttrue是否启用Basler相机自动重连
LogLevelDebug日志输出级别

这些配置写在app.config<appSettings>节点里,修改后重启程序生效。实际部署时我习惯再套一层NLoglog4net,把上传成功/失败记录到文件,排查数据丢失问题时能给证据。

4.4 登录注册模块和权限控制的意义

医疗设备如果直接暴露在局域网,没有身份认证,DIOCM服务端口等于向所有人开放,任何人可以用Storescu命令推送伪造DICOM文件进来。Login.csLoginRegister.sln这块虽然简单,但思想是对的:登录成功后拿到口令,CStoreSCP连接时校验对方的AE Title列表,不在白名单的直接拒绝。注意AE Title校验必须和IP校验一起做,因为同一子网内AE Title可以被冒充。

5. 项目实测:用Dicom Toolkit模拟推图和相机断线恢复验证

收尾这章节分享两个实战验证方法。不跑通这两样,工程改了都不敢上线。

5.1 用DCMTK的Storescu模拟远端推图

源码程序运行前,先确认SCP监听正常。用storescu.exe从另一台机器或本机推一张DICOM测试图:

# 推单张DICOM文件到SCP,验证接收和上传链路 storescu.exe -aet TESTSRC -aec MYSCP 127.0.0.1 11112 -s dcmfile.dcm

参数说明:-aet是调用方AE Title,-aec是接收方AE Title,必须和代码里CStoreSCP注册的AE匹配,否则fo-dicom会返回拒绝。127.0.0.1换成程序实际监听的IP,端口对应app.config里的DicomPort。推图成功后,观察程序界面的接收计数、上传接口的日志,以及StoragePath里是否多出了.dcm文件;这一步过了,说明DICOM链路是通的。

5.2 模拟Basler断线重连

拔除相机线缆或禁用设备,观察日志窗口是否打印“连接断开”,再恢复连接看retryDelay是否回到初始值。这里有个技巧:断线检测不要放在UI事件里,写个BackgroundWorkerTask.Run循环做健康检查。源码里是while (true)配合Thread.Sleep的实现,我建议改成CancellationTokenSource,方便程序退出时优雅停止。

var cts = new CancellationTokenSource(); Task.Run(() => AutoReconnectLoop(cts.Token), cts.Token); void AutoReconnectLoop(CancellationToken token) { while (!token.IsCancellationRequested) { if (!CheckCameraHealth()) { TryReconnect(); } Thread.Sleep(2000); // 探测间隔 } }

逻辑说明:CheckCameraHealth()里用camera.StreamGrabber.IsGrabbing判断状态,如果为false且还没尝试重连,就执行Reconnect()Thread.Sleep(2000)控制探测频率,太低会占CPU,太高断线恢复不够及时。2秒是个不错的平衡值。

5.3 高帧率下的性能调优方向

当Basler相机帧率超过60fps,Halcon的DispObj会成为瓶颈。常见优化路线有两种:一是将Halcon窗口的SetPart只执行一次,后续只更新图像内容;二是采用双缓冲,接收线程把帧写入缓存队列,显示线程只从队列取最新帧渲染。对应到热搜词里“c# 循环数据采集和ui刷新卡顿”的解法:不要在ImageGrabbed回调里直接操作UI控件,用BeginInvoke或Channel同步到UI线程,且只在有数据变化时刷新。实测25fps下,这种双缓冲方案能把UI占用从35%降到8%左右。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 1:46:35

示波器八大实操灵魂问题:新手避坑指南

1. 这不是教科书&#xff0c;是我在电子实验室熬了十七个通宵后写给新手的示波器通关手册“八个灵魂问题”——这标题乍看像哲学课作业&#xff0c;其实是我带新人时最常被堵在工位前问到的八次“卡壳瞬间”。第一次是实习生盯着屏幕上的波形发呆&#xff1a;“老师&#xff0c…

作者头像 李华
网站建设 2026/9/14 1:44:49

LabVIEW实现CAN UDS刷写工具:图莫斯硬件适配与协议精控

1. 项目概述&#xff1a;为什么一个LabVIEW工程师要亲手造CAN UDS刷写工具“基于图莫斯的CAN UDS升级上位机——LabVIEW版本&#xff1a;从零搭建ECU刷写工具”&#xff0c;这个标题里藏着三类人的真实痛点&#xff1a;汽车电子工程师在产线遇到ECU固件批量升级失败&#xff0c…

作者头像 李华
网站建设 2026/9/14 1:43:09

Unity ARPG技能系统框架:ScriptableObject+运行时热重载

简介&#xff1a;本资源是Joker老师ARPG课程配套的Unity技能编辑器完整实践项目&#xff0c;面向计算机相关专业高校学生、教师及Unity初学者&#xff0c;聚焦游戏开发中技能系统的设计与可视化编辑实现。压缩包含2000个文件&#xff0c;主体为365个FBX角色模型、108个Prefab预…

作者头像 李华
网站建设 2026/9/14 1:42:33

STM32 AI编程:构建人机协同的嵌入式开发新范式

1. 这不是“用AI写代码”&#xff0c;而是重构嵌入式开发的认知框架 “AI编程”这个词在嵌入式圈子里最近被喊得有点响&#xff0c;但很多人一上手就栽了跟头——把Copilot当万能胶水&#xff0c;往Keil里一粘&#xff0c;生成的代码连编译都过不去&#xff1b;或者让大模型直接…

作者头像 李华