# MyPacs **Repository Path**: gmt001/my-pacs ## Basic Information - **Project Name**: MyPacs - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-01 - **Last Updated**: 2026-03-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 三方库: dcmtk:读取dicom信息 vtk-dicom:读取图像为图片并渲染(vtkDICOMReader,继承自vtkimagereader2),单帧时非必要,vtk本身也提供了单帧图片的读取方法(vtkimagereader2) vtk:三维建模 开发步骤: 1、ui实现,包括基本布局及窗口激活切换 2、给图片展示组件添加renderwindow 3、实现dicomreader,定义patient-study-series-image(定义model及操作),扫描文件并建立dicom数据层级结构 4、捕捉文件扫描时发出的渲染信号,渲染图片到renderwindow,需要先设置renderwindow的图片层、文本展示层(两个renderer,一个绑定image_actor,一个绑定text_actor)、窗位窗宽,并添加interactor 5、实现interactor交互操作,主要包括鼠标滚动上下切换图片并更新文本信息,鼠标左键按下并移动调整窗位窗宽,鼠标右键缩放等 6、实现预览组件,加载缩略图 7、各按钮对应操作实现,包括原始图像变换、切片、3d重建等 8、实现dicom文件上传下载页面,可暂时使用现成服务器软件测试 9、实现简易dicom服务器 10、看情况添加数据标注功能 细节: 1、线程FilesImporter,负责打开文件或文件夹时调用reader解析dicom,解析完毕后发送更新主界面信号,在首次添加文件/文件夹时启动 2、每次以一个series为单元进行刷新,读取到新series时,FilesImporter发送添加、展示缩略图、更新当前2D主图像信号 3、FilesImporter采用生产者-消费者模型,使用文件队列存储文件路径,importFiles不断从队列取文件,调用DicomReader读取 4、加载dicom,dataSet是键值对结构,使用tag从中获取对应信息,主要的信息分为:patient\study\series\image,层层包含(分别为这几类信息建立model,封装对应tag信息和子项crud操作) filePath = t_filePath; file = std::unique_ptr(new DcmFileFormat()); if (file->loadFile(t_filePath.c_str()).bad()) { file.release(); dataSet = nullptr; throw std::runtime_error("Cannot open file!"); } dataSet = file->getDataset(); 5、每次加载dicom时,设置patient/study/series为当前dicom的对应id 6、主界面ViewerContent,负责接收importer信号、界面互动、图像渲染,主要实现的操作包括:布局切换、缩略图添加&展示&隐藏、图像操作(3d建模、mpr切片、像素取反、水平&垂直翻转、左右旋转)。缩略图操作由Thumbnail模块实现,图像操作由uiWidget模块实现 7、thumbnail为tabwidget,每个patient一个patienttab,每个patienttab的每个study对应一个子studytab,每个studytab内部是一个listwidget实现的缩略图列表,对series取首张图像(按instancenumer+slicelocation排序,对多帧dicom取首帧)做缩略图(生成bmp文件)加到列表中,点击缩略图时,将最后一个窗口替换为选中series 8、uiWidget中,widget2D实现2D图像相关操作,widget3D实现3D操作,widgetMpr实现切片,共同继承于widgetbase,widgetwrapper是对三者的包装,widgetContainer负责存放这些窗口对象,widgetManager操作这些窗口对象,具体的渲染操作在对应的helper中。renderwindow设置为两层,分别放renderer,一个对应image_actor,用于展示图片,一个对应text_actor,用于展示说明文字(overlay)。 9、widget2D采用: 1)QVTKOpenGLNativeWidget+vtkGenericOpenGLRenderWindow做渲染输出窗口,放在widget2D内 2)在widget2D内设置helper的renderwindow,再在helper内获取renderwindow的interactor,设置interactor的style为自定义的interactorstyle以实现事件处理。事件传递顺序:操作系统产生滚轮信号 -> Qt 生成 QWheelEvent -> 投递给 QVTKOpenGLNativeWidget::wheelEvent() -> QVTKOpenGLNativeWidget 调用其内部持有的 vtkRenderWindowInteractor的FindPokedRenderer(x, y) 确定鼠标位置对应的渲染器,然后调用InvokeEvent触发 MouseWheelForwardEvent 或 MouseWheelBackwardEvent -> vtkRenderWindowInteractor 接收到事件后,查找当前绑定的 vtkInteractorStyle,将事件分发给vtkInteractorStyle 的对应回调函数(如 OnMouseWheelForward())。 3)VtkWidgetEventFilter在QVTKOpenGLNativeWidget鼠标按压、滚动、双击事件发生时触发widget2D的焦点事件(只要eventFilter调用基类的eventFilter,仍然会触发vtkRenderWindowInteractor的响应),传递给widgetwrapper,更新窗口样式 4)widget2D内虽然添加了scrollbar,但并没有处理相应的wheel事件(在widget2D的wheel事件处理中对scrollbar进行setvalue),scrollbar只是作为一个有多张图片的标志,位置始终为0,并且在图片数增加时更新maxinum 5)初始化imageviwer,绑定renderwindow,用于设置inputdata和控制图像渲染(分单帧多帧),注意imageviwer会重置缩放比例,若需要手动设置缩放比例,应该放在imageviwer的render调用后 6)设置窗位窗宽(识别图像类型,不同类型设置方式不同,有的是设置查找表,有的是设置范围) 7)设置overlay图层,用于在渲染图层上加文字(vtkOpenGLTextActor),包括series信息,动态信息(首张图片渲染时更新zoomfactor,鼠标滚轮滚动时切换当前索引,鼠标左键按下并移动时修改床位窗宽)。注意需要将overlay层的intercative设置为false,以便鼠标操作作用到图片层 8)renderer->GetActiveCamera()获取camera,用于图像翻转及旋转 10、overlay 1)包装vtkOpenGLTextActor,提供按key内容设置,拼接后显示 2)创建管理类,读取模板文件,使用dicom meta data信息替换内容并设置到textactor,放置到对应四个边角 3)设置callback,在窗口大小变化时重新放置文字(是否需要待验证) 4)给overlay绑定renderer,可以直接和图片共用一个renderer,或者新建一个图层,使用单独的renderer,前者和图片共用坐标系,深度绑定不够灵活 11、3d重建(widget类负责创建窗口,helper负责3d重建、环境参数设置及裁剪) 1)初始化mapper,设置混合模式及inputdata 2)初始化volume(actor),添加mapper到volume 3)添加volume到renderer 4)添加renderer到renderwindow(vtkGenericOpenGLRenderWindow在渲染时使用了QOpenGLContext,QOpenGLContext是非线程安全的,跨线程使用需要movetothread绑定到目标线程且目标线程需要已运行,故通常只在创建该对象的线程中使用,应该改用另外建立的vtkrenderwindow,在该window中渲染,再将其renderer传回给vtkGenericOpenGLRenderWindow) 5)设置interactor(vtkInteractorStyleTrackballCamera,实现空间变换,注意应是vtkGenericOpenGLRenderWindow的interactor) 6)设置默认环境参数,可以在实现模式选择后设置默认模式 7)渲染 8)添加boxwidget(绘制参考立方体,实现裁剪,设置callback observer以实现在每次立方体改变时自动修改裁剪平面) 9)提供模式选择,不同模式下加载对应环境参数(如:光照等,MIP代码设置,其余来自json文件) 12、mpr切片 1)初始化三个widget,添加三个renderwindow,用于放置三个切面 2)初始化三个vtkImageResliceToColors对面用于获取切片,需要设置横断面、矢状面、冠状面矩阵 3)平移横断面、矢状面、冠状面平面到中心(计算中心坐标,赋给矩阵第4列),确保切面处于中间 4)初始化slicer,绑定输入数据、设置输出参数、颜色映射 5)初始化mapper,添加reslicer输出数据到mapper 6)初始化actor,添加mapper到actor 7)初始化renderer,添加actor到renderer 8)添加renderer到renderwindow,渲染 9)切面添加坐标轴(vtkAxisActor2D),坐标系为显示坐标 —— 暂不考虑实现 10)interactor,鼠标滚轮实现切面沿法向量方向平移,平移单位为像素;缩放,对应屏幕显示像素范围变化,若添加了坐标轴且不覆盖窗口全范围需要更新坐标轴可见刻度;平移;调整窗位窗宽 11)实现旋转切面:先实现绘制,再考虑interactor 其他: vtk图形渲染一般步骤 1、选用目标图形的对应类,创建对象,将对象outputport绑定到vtkPolyData 2、创建mapper,对vtkpolydata,主要是vtkpolymapper对象,将polydat的output绑定到mapper,并设置属性 3、多个图形vtkpolydata可以先加到vtkAppendPolyData集合,再一起绑定到mapper 4、创建actor,将mapper设置为2中创建的mapper 5、创建renderer,将actor添加到renderer 6、将renderer添加到renderwindow 7、进行渲染 vtk事件响应: 1)不存在冒泡,不同组件可以监听同个事件,通常需要设置优先级,否则按照添加的顺序响应 2)grabfocus(针对后续事件):用于将输入焦点转移到当前组件,此后输入操作只会触发当前组件的事件响应,除非手动调用releasefocus,或者对象被销毁 3)setabortflag(正对当前事件):用于阻止事件传播,当多个组件同时监听同个事件时,不同优先级存在先后顺序,先响应的调用了此函数,则后面组件的响应不会被触发 vtkWidgetRepresentation中不通过将actor添加到renderer进行关联,而是重写RenderOpaqueGeometry、RenderOverlay等方法(参数Viewport一般是renderer对象),在方法中调用actor进行渲染,renderer调用render本质上也是调用这些函数 自定义vtkwidget小部件 1、继承vtkAbstractWidget,实现createdefaultrepresentation 2、定义actor,主要实现绘图 3、定义representation,需要继承vtkWidgetRepresentation,实现 BuildRepresentation、RenderOverlay、RenderOpaqueGeometry、ReleaseGraphicsResources、ComputeInteractionState等方法,在这些方法中调用actor 4、实现必要的callback,如鼠标移动时按位置变换光标,鼠标按下移动时图像进行对应变换等 5、创建小部件对象,调用setInteractor、setRenderer关联renderer,调用setEnable使小部件生效,重新render以展示小部件 6、复合小部件主要是在类中定义子部件对象,设置对应的representation,并setInteractor 等值面设置为0作用?CT值为0处绘制平面,没有CT值为0的区域是作用不能体现 boxwidget作用是绘制一个参考立方体,对该立方体旋转,并利用边界作为裁剪平面对建模结果进行裁剪 不同模式对应显示不同器官,有必要实现,作用是设置CT值到RGB颜色等环境参数映射,通过rgbpoint可控制显示不同部位 updatewindowlevel含义,dicom半窗宽相对最大值是1000,变化量是相对1000的变化量,转化为相对标准映射点的变化量,不一定要按照这种算法调整 vtktypemarco宏,自动定义一些类方法,如NewInstance vtkwidgetbase,辅助类,用于读取dicom并设置vtkwidgetdicom的renderwindow vtkwidgetdicom,继承自vtkimageview2(2d),用于控制渲染 toolbarwidget3d,控制启用空间变换,切换模式(设置环境参数) transferfunction,提供映射点以设置颜色及透明度 boxwidget创建时renderer已转移给vtkGenericOpenGLRenderWindow(addrenderer中调用了setrenderwindow),其绑定的interactor和qtvtkwidget取到的interactor实际上是同一个 vtkhandlewidget是一个手柄组件,继承自vtkAbstractWidget,其内有一个vtkPointHandleRepresentation3D成员,用于控制可视化,默认以一个“+”的方式展示,绘制的图形在setEnable时被关联到renderer(从interactor的FindPokedRenderer获取)上,在对应renderwindow调用render时显示到界面。其余函数用于实现交互逻辑。通过sethandlesize调整可视化点的大小,settolerance设置鼠标响应的球形区域范围 vtkresliceplanecursorwidget定义可一个复合部件,用于旋转、移动平面,获取不同角度的切面,包含了handlewidget和自定义的两条正交线,在鼠标靠近两条线且不在手柄部件范围内时显示一个手形图标,鼠标左键按下时切换为旋转状态并允许旋转;在鼠标进入手柄范围时,切换为平移状态允许平移中心。此类只是触发光标旋转、平移事件,具体的响应变换由vtkresliceewidget实现 vtkreslicewidgetrepresentation重新定义actor实现自定义绘图,主要控制handlewidget外观及添加两条正交线 vtkreslicewidget只是用于管理添加交互组件,从vtkabstractwidget继承非必要,只是方便利用vtk智能指针等特性 回调:用于监控对象的某个事件,在事件发生后执行特定动作;renderwindow可以使用interactor实现交互,其他如小部件则需要自定义事件的callback来实现交互。实现步骤:1、实现一个回调类——继承自vtkCommand&实现Execute方法;2、被监控对象使用AddObserver添加监控。SetCallbackMethod指定回调时,需要同时满足vtkCommnad和vtkWidgetEvent才会触发,单独一者不会触发(vtkWidgetEvent::AddPoint用鼠标点击时触发),且源是调用此函数的对象。3、项目中vtkresliceplanecursorwidget主要实现的回调是movement时变换光标、对vtkhandlewidget的拖动发送平移信号、对正交线附近的拖动发送旋转信号,后两者需要在vtkreslicewidget进行信号响应(一个切面发信号,需要对另外两个切面做变换,因此信号的发送者不是信号的响应者)。4、添加的回调需要在析构时remove,回调是全局的,对象被析构后callback里的manager仍然存在,但manager的指针成员所指对象已被析构(浅拷贝) dicomreader提供底层读取dicom信息接口,包装在一个wrapper类中供FileImporter调用,wrapper类主要实现建立patient-study-series-image层次结构 读取dicom信息后,发送信号,触发对应widget类读取图像信息并展示,在图片上加覆盖层展示dicom信息 同series放一个子窗口,使用scrollbar切换,每扫描到一个图片更新scrollbar深度,只有出现新series时才切换渲染窗口并进行渲染 更新scrollbar采用信号&槽机制,信号源对象相同,切换窗口时目标对象不同,需要释放当前正在添加图片的widget的槽函数连接(需保证按series有序),或者在槽函数中判断当前series是否与widget的series一致 使用set集合存储image,保证遍历时有序,需要自定义比较规则(按sopinstancenumber&slicelocation) vtk绘图imageviewer2,默认图像平面与z轴正交,可通过SetSliceOrientationToXY设置 vtk智能指针(vtk内置对象的构造函数非公有,不能通过普通指针构造对象,vtkStandardNewMacro宏定义了公有New方法,使用vtk智能指针时会调用该方法创建对象) vtkNew:作用基本同常规指针,复制构造函数和赋值操作符被禁用,提供跳出作用域时自动销毁功能,构造函数中自动调用模板参数类型的New方法建对象 vtkSmartPointer:类似shareptr,通过引用计数管理,初始化为nullptr VtkUniquePtr:基于uniqueptr reslice矩阵 前三行:前3列表示旋转后新坐标系坐标轴在原坐标系的单位向量(应用到平面上,前两列表示平面左右、上下轴向,第三列表示平面的法向量),最后一列表示平移后新坐标系原点后在原坐标系的坐标 最后一行:表示缩放及透视,通常设置为(0,0,0,1) 窗位窗宽获取两种方式,一种来自dcmtk的dataset,扫描时赋值给image(dicom文件没有tag时根据图片CT值范围自动计算),一种来自vtkdicom的reader(从reader元数据获取),具体用哪种看有哪种对象 坐标系 显示坐标:屏幕像素坐标,以渲染窗口左上角为原点 视口坐标:范围0-1,需要通过乘以窗口实际像素尺寸转换为显示坐标 世界坐标:渲染场景所在坐标,是现实世界坐标的抽象,以设置的物理长度单位(m,cm,mm等)为轴刻度基本单位 世界坐标中的物体投影到投影平面,由视口裁剪后显示,投影平面到视口的比例不会自动改变,缩放时,投影相应变大变小,若不进行特殊处理,则物体在视口中的大小也会变化 bug & qs 1、onRefreshScrollValue 代码多余: // 当前查看的不是第一张,插入的图片在当前图片之前,value+1,确保查看的图片是同一张, 加载过程中 scrollBar->value 正常总是0,不可能出现此种情况 // const auto value = (image->getIndex() <= scrollBar->value() && size > 1 && image->getIndex() > 0) ? scrollBar->value() + 1 : scrollBar->value(); 2、更新scrollbar时updateoverlay可能先于render,需要跳过 3、点击缩略图将最后一个窗口切换为所选series待实现 4、zoom计算方式,分子考虑了space,分母没有,需要确认是否存在问题:spacing默认为1,缩放影响spacing1,分母不考虑才是原始的大小 5、移动手柄时,重新setposition,只是移动了两条线,图片本身移动,线段长度也没变,会不会在界面上出现线段一部分被遮挡,一部分缺失?设置了scale,一拉长线段,确保不会出现此问题 6、vtkrelicewidgetrepresentation考虑不实现renderoverlay,actor并没有文字层,可以 7、原项目的frameless有问题,导致获取鼠标位置不准确 8、正交线setscale功能待验证,widgetinteraction为何取actor的position:手柄组件移动会导致整个物体移动,actor发生变化,可以获取移动量,另外需要将图片减去移动量以确保图片仍在窗口中间 9、widget小部件显示后,interactor是否还会触发原来的函数,尝试注释掉GrabFocus 10、setabortflag作用 11、事件本身会自动传给所有监听的对象,单独invokeevent作用,interactor作为事件源,应该会自动传给widget和interactor的响应 12、手柄氛围内鼠标按下是否同时触发手柄startinteraction及父部件leftbuttonpressevent 13、修改图片和小部件中心平移位置可能有问题待验证 14、平移时actor移动距离是切面所在平面上的距离,不能直接当成x,y移动距离,需要考虑旋转角度 15、mprinteractor 在down时grabfocus,没有在up是releasefocus 16、文件读取时可以加上对采用压缩转移语法数据集的解压缩 pacs交互页面:提供pacs查询、dicom文件上传pacs、从本地文件或远程文件解析dicom并展示报告,导入到viewer页面进行查看 1、查询窗口提供pacs设置及查询确认 2、上传窗口提供导入文件/目录按钮及文件列表(暂时简单列出,后续再整成目录树),上传pacs按钮 3、提供报告展示窗口及导入到viewer页面按钮 *:只负责展示文件信息,以确认上传、下载内容,文件不用于3d建模,无需排序,只需要解析到series信息,并展示dicom文件列表即可 实现过程: 1、实现scu类,用于数据上传、查询、下载等:初始化服务端实体(名称、ip、port等)、初始化查询数据集、初始化presentationcontext;实现echo、store、move、find操作 2、实现scp类,用于本地查询管理、下载,需要包装在子线程中运行以实现后台一直监听目标AE 3、全局配置类,从文件读取或写pacs信息,使用单例模式 ——本地配置:ae title, ae port,用于scu设置title,下载时充当scp,需要port ——pacs server配置:ae titlem, ae ip, ae port, ar desciption 4、查询实现:调用scu类进行查询,查询结果由dataManager类处理后传给ui类,ui类更新到页面; 5、数据上传:打开本地文件/目录,将文件列表更新到ui, 6、数据下载:调用move服务 7、scu放在主线程会堵塞gui线程,使得无法在传输数据时及时更新progress bar,可以考虑使用movetothread放到子线程 8、改造为子线程执行后,数据传输完成、传输出错、传输进度等需要主动发送信号到主线程进行处理,确保数据同步 Viewer的patient\study\series\image匹配关系通过指针建立,每次解析文件前需要手动扫描已添加patient\study\series元素列表以确认是否遇到新元素 scu的匹配关系通过Qmap建立,通过key值保证唯一性,不用手动判断是否遇到新元素 服务端:webapi,包含query/store服务,vue前端提供服务状态查询、启动、停止,实现cmove时需要创建scu,给cmove请求端发送cstore,目前ip写死在代码中,后续可以考虑放在配置文件,通过sopinstanceid进行查询 服务端增加支持压缩转移语法