用c#实现:
1 ct 文件说明:
说明数据文件
说明图像文件(2进制 8位)
一张CT图像有 512x512 个像素点,在dicom文件中每个像素由2字节表示,所以每张图片约512KB大小。图像中每个像素都是整数,专业名称为 Hounsfield scale 或 CT Number,是描述物质的放射密度的量化值(参考Wikipedia)。上表为常见物质的HU值。
2、读取数据
原始数据:
Data.des中是xml格式数据:<whole>
<Patient>
<PatientName>zhang san</PatientName>
<PatientSex>m</PatientSex>
<PatientAge>60</PatientAge>
</Patient>
<Scan>
<Hospital>ShenYang Hospital</Hospital>
<WindowWidth>600</WindowWidth>
<WindowCenter>400</WindowCenter>
<ScanTime>2010/06/20 12:30:20</ScanTime>
<ImageWidth>512</ImageWidth>
<ImageHeight>512</ImageHeight>
<XPixelSpacing>0.05</XPixelSpacing>
<YPixelSpacing>0.05</YPixelSpacing>
<Interval>0.1</Interval>
</Scan>
<Image>
<Image1>D001_001.0.raw</Image1>
<Image2>D002_001.0.raw</Image2>
<Image3>D002_002.0.raw</Image3>
<Image4>D002_003.0.raw</Image4>
<Image5>D002_004.0.raw</Image5>
<Image6>D002_005.0.raw</Image6>
<Image7>D002_006.0.raw</Image7>
</Image>
</whole>
患者信息等数据是xml格式通过XmlDocument读取
用记事本打开 des 文件
通过标签描述数据本身
患者姓名 性别 年龄 扫描(scan)等信息
1 我们要把这个文件内容读取到我们程序中
首先我们构建一个对象imageInfo 来描述这些信息
1、目标 把xml 信息读取到对象中来
2、借用一个xmldocument 对象进行处理
3 xmldocument 完整描述一个xml文件!
xml 文件又着严格的层级结构!
建议大家利用第三方AI 工具生成这些代码,再阅读!
读取图像数据 raw,要把它转为bitmap
ps:https://learn.microsoft.com/zh-CN/dotnet/api/system.drawing.bitmap?view=dotnet-plat-ext-8.0
打开文件:
准备:
private string fileName= "d:/temp/D001_001.0.raw"; //文件名
private short[] imageData; //原始数据
Bitmap bmpImage; //显示位图
private int windowWidth = 300; //窗宽
private int windowCenter = 100; //窗位
private int imgWidth = 512; //图像宽度
private int imgHeight = 512; //图像高度
1) File.Open
2)BinaryReader
读取的文件放到一个临时数组当中
3)byte[] tempData = new byte[imgWidth * imgHeight * 2];
4)把tempData 数据读取到imageData 数组当中,这时候要位移运算 imageData[j] = (short)((short)tempData[j * 2 + 1] << 8 | tempData[j*2]);
5)灰度转换
windowCenter、windowCenter和医学有关,自己去查。
public void Rendering()
{
//使用窗宽窗位,创建灰度查找表。图像存储的数据都是正值,而CT值的最小值是1024,查表时下标必须为正,
//所以此处调整了窗宽窗位
int offset = 1024;
Byte[] lookUpTable = new Byte[4096]; //数组元素默认值为0
int low = windowCenter - windowWidth / 2 + offset;
int high = windowCenter + windowWidth / 2 + offset;
for (int i = low; i <= high; i++)
{
lookUpTable[i] = (Byte)((i - (windowCenter - windowWidth / 2 + offset)) / (double)windowWidth * 255);
}
for (int i = high + 1; i < 4096; i++)
{
lookUpTable[i] = 255;
}
grayBmp = new byte[imgWidth * imgHeight];
for (int i = 0; i < imgHeight; i++)
{
for (int j = 0; j < imgWidth; j++)
{
short data = (short)(imageData[i * imgWidth + j]);
grayBmp[i * imgWidth + j] = lookUpTable[data];
int temp = lookUpTable[data];
Color pixel = Color.FromArgb(temp, temp, temp);
bmpImage.SetPixel(j, i, pixel); //速度很慢,使用下面的方法把数据一次性传过去
}
}
}
把图像文件信息封装到类当中:
{
[System.Serializable]
class MImage
{
private string fileName; //文件名
private short[] imageData; //原始数据
Bitmap bmpImage; //显示位图
private int windowWidth = 300; //窗宽
private int windowCenter = 100; //窗位
private int imgWidth = 512; //图像宽度
private int imgHeight = 512; //图像高度
public int ImgWidth
{
get { return imgWidth; }
}
public int ImgHeight
{
get { return imgHeight; }
}
public int WindowWidth
{
get { return windowWidth; }
set { windowWidth = value; }
}
public int WindowCenter
{
get { return windowCenter; }
set { windowCenter = value; }
}
public Bitmap BmpImage
{
get { return bmpImage; }
}
public MImage(string fileName)
{
this.fileName = fileName;
this.imgWidth = ImageInfo.ImageWidth;
this.imgHeight = ImageInfo.ImageHeight;
this.windowWidth = ImageInfo.WindowWidth;
this.windowCenter = ImageInfo.WindowCenter;
}
public Bitmap LoadImage()
{
}
}
}
6) 前端渲染 通过graphics 把bitmap呈现在界面。
Rectangle showRect = new Rectangle();
1 我们通过OnPaint 绘制图像 ,搞个继承panel的自定义组件实现。
通过OnPaint 绘制图像
界面设计布局:
增加子面板的的例子代码:
for (int i = 1; i < 4; i++)
{
for (int x = 1; x < 4; x++)
{
Panel panel1 = new Panel();
// Initialize the Panel control.
panel1.Location = new Point(56 + i * 264, 72 + x * 152);
panel1.Size = new Size(264, 152);
// Set the Borderstyle for the Panel to three-dimensional.
panel1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
// Initialize the Label and TextBox controls.
// Add the Panel control to the form.
this.Controls.Add(panel1);
// Add the Label and TextBox controls to the Panel.
}
}
Winform menu+status+SplitContainer
splicePage :作为左半边的面板 这个是自定义的 继承了Panel
Child panel: MImageWnd
创建panel
1 MImageWnd mw= new MImageWnd()
2 追加到父容器splicePage 当中 this.Controls.add(mw)
定位: 左 上位置
设置childPanel 的 height、width:平分splicePage 的长度 宽度
显示 与显示数据分离
List<MImageWnd >
我们要干三件工作
1 改变colNum .rowNum
//清理现场的工作
2 清空wndList wndList 是我们记录子面板的集合 可以看成是前端的数据模型
3、Controls remove MImageWnd
InitializeComponent();
Rectangle rect = Screen.GetWorkingArea(this); //获得屏幕分辨率
this.WindowState = FormWindowState.Maximized;
this.mainSplit.SplitterDistance = (int)(this.mainSplit.Width * 4f / 5f);
this.slicePage = new SlicePage();
slicePage.Dock = DockStyle.Fill;
this.mainSplit.Panel1.Controls.Add(slicePage);
slicePage.CreateControls();
slicePage: 起到父容器及控制器作用:
{
public partial class SlicePage : Panel
{
List<MImageWnd> wndList = new List<MImageWnd>(); //窗口数组
List<MImage> imgList = new List<MImage>(); //图像数组
List<AnnoTool> annoList = new List<AnnoTool>(); //标注数组,和图像一一对应
int rowNum = 3; //子窗口行数
int columnNum = 3; //子窗口列数
VScrollBar sb = new VScrollBar();
int scrollBarWidth = 20;
public List<MImage> ImgList
{
get { return imgList; }
set { imgList = value; }
}
public List<AnnoTool> AnnoList
{
get { return annoList; }
set { annoList = value; }
}
public SlicePage()
{
sb.Minimum = 0;
sb.Maximum = 0;
sb.Scroll += new ScrollEventHandler(sb_Scroll);
}
public void LoadImage()
{
if (imgList == null)
{
return;
}
int num = imgList.Count < wndList.Count ? imgList.Count : wndList.Count;
for (int i = 0; i < num; i++)
{
wndList[i].SetImage(imgList[i], annoList[i]);
wndList[i].Invalidate();
wndList[i].Update();
// ((MainForm)(this.Parent.Parent.Parent)).ProcessProgressBar();
}
sb.Minimum = 0;
if (imgList.Count % columnNum != 0)
{
sb.Maximum = imgList.Count / columnNum;
}
else
sb.Maximum = imgList.Count / columnNum - 1;
sb.SmallChange = 1;
sb.LargeChange = rowNum;
sb.Value = 0;
}
public void Rotate()
{
foreach (MImageWnd wnd in wndList)
{
wnd.Rotate();
}
}
private void InitializeComponent()
{
this.SuspendLayout();
//
// SlicePage
//
this.Resize += new System.EventHandler(this.SlicePage_Resize);
this.ResumeLayout(false);
}
void sb_Scroll(object sender, ScrollEventArgs e)
{
if (imgList == null)
{
return;
}
int start = e.NewValue * columnNum; //点击滚动条后,从此幅图像开始显示
int num = (imgList.Count - start) < wndList.Count ? (imgList.Count - start) : wndList.Count;
for (int i = 0; i < num; i++)
{
wndList[i].SetImage(imgList[i + start], annoList[i + start]);
wndList[i].Invalidate();
wndList[i].Update();
}
for (int i = num; i < wndList.Count; i++) //多余的窗口,清除它的图像
{
wndList[i].ClearImage();
}
}
public void Relayout(int row, int col)
{
rowNum = row;
columnNum = col;
foreach (MImageWnd wnd in wndList)
{
this.Controls.Remove(wnd);
}
this.wndList.Clear();
CreateControls();
ArrangeControls();
LoadImage(); //改变了窗口个数以后,重新加载图像
}
public void CreateControls()
{
for (int i = 0; i < rowNum * columnNum; i++)
{
MImageWnd wnd = new MImageWnd();
wnd.BorderStyle = BorderStyle.FixedSingle;
this.Controls.Add(wnd);
wndList.Add(wnd);
}
this.Controls.Add(sb);
}
private void ArrangeControls()
{
if (wndList.Count == 0)
return;
int width = (this.Width - scrollBarWidth) / columnNum;
int height = this.Height / rowNum;
for (int i = 0; i < rowNum * columnNum; i++)
{
wndList[i].Location = new Point(i % columnNum * width, i / columnNum * height);
wndList[i].ClientSize = new Size(width, height);
}
sb.Location = new Point(this.Width - scrollBarWidth, 0);
sb.Size = new Size(scrollBarWidth, this.Height);
}
//此两种写法的详细区别。
protected override void OnResize(EventArgs eventargs)
{
ArrangeControls();
base.OnResize(eventargs);
}
private void SlicePage_Resize(object sender, EventArgs e)
{
}
//public const int USER = 0x0400;
//public const int WM_TEST = USER + 101;
//protected override void DefWndProc(ref System.Windows.Forms.Message m)
//{
// switch (m.Msg)
// {
// case WM_TEST: //处理消息
// MessageBox.Show("haha");
// break;
// default:
// base.DefWndProc(ref m);//调用基类函数处理非自定义消息。
// break;
// }
//}
}
}
其中,重要的一点是把数据源设置到子面板当中
wndList[i].SetImage(imgList[i]);
wndList[i].Invalidate();
wndList[i].Update();
为scrollBar留空间 int scrollBarWidth = 20;
查官网 scrollBar 用法
1 、创建 :VScrollBar sb = new VScrollBar();
2 、设置 scrollBarWidth height
3 、add 到父容器
scrollBar5个属性:
value=0 : 滚动的刻度
Maximum= this.imglist.count%columnNum*rowNum!=0
this.imglist.count/columnNum*rowNum
this.imglist.count/columnNum*rowNum-1
Minimum=0
SmallChange=1
LargeChange=rowNum
设置滚动事件
3*3
改参数 2*2 、4*4
清除 数据模型、实际展示
ps:Delegate Class
类比c函数指针:
https://learn.microsoft.com/en-us/dotnet/csharp/delegates-overview
通过委托来实现事件处理的过程,通常需要以下4个步骤:
• 定义委托类型,并在发布者类中定义一个该类型的公有成员。
• 在订阅者类中定义委托处理方法。
• 订阅者对象将其事件处理方法链接到发布者对象的委托成员(一个委托类型的引用)上。
• 发布者对象在特定的情况下“激发”委托操作,从而自动调用订阅者对象的委托处理方法。
1 打开文件
根据dilalog 中的选择 动态读取xml 文件
xml文件当中的信息 转换为ImageInfo对象、设置 MIMage
我们已经能读取数据,也能形成九宫格,现在要组装到项目中:
form :读取文件 ——》
xml----->imageInfo
数据文件---》 MIMage
ImageInfo info = new ImageInfo();
XmlDocument myxml = new XmlDocument();
ps: OpenFileDialog
来自 <https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.openfiledialog?view=windowsdesktop-7.0>
更改openfileDialog过滤条件
dlg.Filter = "图像描述文件 (*.des)|*.des|生数据 (*.raw)|*.raw";
调用
if (dlg.ShowDialog() == DialogResult.OK)
{
slicePage.ImgList.Clear();
if (dlg.FilterIndex == 1)
{
ParseDesFile(dlg.FileName);
//slicePage.LoadImage(); //重绘界面
}
}
private void ParseDesFile(string fileName)
{
ImageInfo info = new ImageInfo();
XmlDocument myxml = new XmlDocument();
myxml.Load(fileName);
XmlNode root = myxml.DocumentElement;
XmlNode node = root["Patient"];
ImageInfo.PatientName = node["PatientName"].InnerText;
ImageInfo.PatientSex = Convert.ToChar(node["PatientSex"].InnerText);
ImageInfo.PatientAge = Convert.ToInt32(node["PatientAge"].InnerText);
node = root["Scan"];
ImageInfo.HospitalName = node["Hospital"].InnerText;
ImageInfo.WindowWidth = Convert.ToInt32(node["WindowWidth"].InnerText);
ImageInfo.WindowCenter = Convert.ToInt32(node["WindowCenter"].InnerText);
ImageInfo.ScanTime = node["ScanTime"].InnerText;
ImageInfo.ImageWidth = Convert.ToInt32(node["ImageWidth"].InnerText);
ImageInfo.ImageHeight = Convert.ToInt32(node["ImageHeight"].InnerText);
ImageInfo.XPixelSpacing = Convert.ToDouble(node["XPixelSpacing"].InnerText);
ImageInfo.YPixelSpacing = Convert.ToDouble(node["YPixelSpacing"].InnerText);
ImageInfo.Interval = Convert.ToDouble(node["Interval"].InnerText);
node = root["Image"];
int pos = fileName.LastIndexOf('\\');
string path = fileName.Substring(0, pos + 1);
foreach (XmlNode var in node.ChildNodes)
{
MImage img = new MImage(path + var.InnerText);
slicePage.ImgList.Add(img);
}
}
Draw object:
1. Rectangle
2. Line
3. Ellipse
4. Text
1 创建MImage、 imageInfo 2个文件, 修改namespace MImage中增加loadImage方法
2 slicePage 起到连接数据与显示模型的桥梁的作用,我们在它当中增加字段与属性,让它认识MImage等
private List<MImage> imgList = new List<MImage>(); //图像数组
internal List<MImage> ImgList { get => imgList; set => imgList = value; }
控制层:
/**
* 把数据模型的数据塞给显示模型 这里不负责渲染 但调用invalate() update() 方法 引起渲染
*
*/
public void LoadImage() {
for (int i = 0; i < 7; i++)
{
wndList[i].SetImage(imgList[i]);
wndList[i].Invalidate();
wndList[i].Update();
}
}
Mimage: 准备数据,把原始raw数据转换为bitmap数据:
public Bitmap LoadImage()
{
imageData = new short[imgWidth * imgHeight];
bmpImage = new System.Drawing.Bitmap(imgWidth, imgHeight);
byte[] tempData = new byte[imgWidth * imgHeight * 2];
BinaryReader sr = new BinaryReader(File.Open(fileName, FileMode.Open));
//没有办法直接把数据读到short数组里面,所以借助byte数组转化一下。
sr.Read(tempData, 0, imgWidth * imgHeight * 2);
sr.Close();
for (int i = 0; i < imgWidth * imgHeight; i++)
{
imageData[i] = (short)((short)tempData[2 * i + 1] << 8 | tempData[2 * i]);
}
Rendering();
return bmpImage;
}
public void Rendering()
{
//使用窗宽窗位,创建灰度查找表。图像存储的数据都是正值,而CT值的最小值是1024,查表时下标必须为正,
//所以此处调整了窗宽窗位
int offset = 1024;
Byte[] lookUpTable = new Byte[4096]; //数组元素默认值为0
int low = windowCenter - windowWidth / 2 + offset;
int high = windowCenter + windowWidth / 2 + offset;
for (int i = low; i <= high; i++)
{
lookUpTable[i] = (Byte)((i - (windowCenter - windowWidth / 2 + offset)) / (double)windowWidth * 255);
}
for (int i = high + 1; i < 4096; i++)
{
lookUpTable[i] = 255;
}
//grayBmp = new byte[imgWidth * imgHeight];
for (int i = 0; i < imgHeight; i++)
{
for (int j = 0; j < imgWidth; j++)
{
short data = (short)(imageData[i * imgWidth + j]);
//grayBmp[i * imgWidth + j] = lookUpTable[data];
int temp = lookUpTable[data];
Color pixel = Color.FromArgb(temp, temp, temp);
bmpImage.SetPixel(j, i, pixel); //速度很慢,使用下面的方法把数据一次性传过去
}
}
}
//绘制数据
if (null != mImage)
{
double tempFactor;
if (this.Width / (double)this.Height > mImage.ImgWidth / (double)mImage.ImgHeight) //如果窗口较宽
{
tempFactor = this.Height / (double)mImage.ImgHeight * factor;
}
else
{
tempFactor = this.Width / (double)mImage.ImgWidth * factor;
}
showRect.X = (int)((this.Width - mImage.ImgWidth * tempFactor) / 2) + offset.X;
showRect.Y = (int)((this.Height - mImage.ImgHeight * tempFactor) / 2) + offset.Y;
showRect.Width = (int)(mImage.ImgWidth * tempFactor);
showRect.Height = (int)(mImage.ImgHeight * tempFactor);
e.Graphics.DrawImage(mImage.BmpImage, showRect);
}
滚动事件:
动态计算偏移量
重新设置Imgwnd 的图像数据源
加载的时候,loadimg
sb.Minimum = 0;
if (imgList.Count % columnNum != 0)
{
sb.Maximum = imgList.Count / columnNum;
}
else
sb.Maximum = imgList.Count / columnNum - 1;
sb.SmallChange = 1;
sb.LargeChange = rowNum;
sb.Value = 0;
鼠标状态
enum MOUSESTATE
{
NONE,
WL,
ZOOM,
MOVE,
LINE,
RECT,
ELLIPSE,
TEXT
}
图像旋转:
这个基于矩阵变化
Matrix rotateMatrix = new Matrix();
绘制的时候设置gr转换
Wnd:
public void Rotate()
{
Point center = new Point();
center.X = (showRect.Left + showRect.Right) / 2;
center.Y = (showRect.Top + showRect.Bottom) / 2;
rotateMatrix.RotateAt(90, center);
Invalidate();
Update();
}
slicePage 循环调用它
委托:
using System;
using System.IO;
namespace DelegateAppl
{
class PrintString
{
static FileStream fs;
static StreamWriter sw;
// 委托声明
public delegate void printString(string s);
// 该方法打印到控制台
public static void WriteToScreen(string str)
{
Console.WriteLine("The String is: {0}", str);
}
// 该方法打印到文件
public static void WriteToFile(string s)
{
fs = new FileStream("c:\\message.txt", FileMode.Append, FileAccess.Write);
sw = new StreamWriter(fs);
sw.WriteLine(s);
sw.Flush();
sw.Close();
fs.Close();
}
// 该方法把委托作为参数,并使用它调用方法
public static void sendString(printString ps)
{
ps("Hello World");
}
static void Main(string[] args)
{
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
sendString(ps1);
sendString(ps2);
Console.ReadKey();
}
}
}
绘制线段:
1、MImageWnd中 规定位置字段,记录点击位置
private int start_x = -1;
private int start_y = -1;
private int end_x = -1;
private int end_y = -1;
2、构建 MLine 类
internal class MLine
{
private int start_x = -1;
private int start_y = -1;
private int end_x = -1;
private int end_y = -1;
public int Start_x { get=> start_x; set=> start_x=value; }
public int Start_y { get => start_y; set => start_y = value; }
public int End_x { get => end_x; set => end_x = value; }
public int End_Y { get => end_y; set => end_y = value; }
}
3、MImageWnd中 添加集合 List<MLine> lines
4、 划线方法 public void DrawLineInt(int x0, int y0, int x1, int y1, Graphics g)
{
......
}
事件定义、订阅、发布处理 的例子代码:
using System;
namespace SimpleEvent
{
using System;
/***********发布器类***********/
public class EventTest
{
private int value;
public delegate void NumManipulationHandler();
public event NumManipulationHandler ChangeNum;
protected virtual void OnNumChanged()
{
if ( ChangeNum != null )
{
ChangeNum(); /* 事件被触发 */
}else {
Console.WriteLine( "event not fire" );
Console.ReadKey(); /* 回车继续 */
}
}
public EventTest()
{
int n = 5;
SetValue( n );
}
public void SetValue( int n )
{
if ( value != n )
{
value = n;
OnNumChanged();
}
}
}
/***********订阅器类***********/
public class subscribEvent
{
public void printf()
{
Console.WriteLine( "event fire" );
Console.ReadKey(); /* 回车继续 */
}
}
/***********触发***********/
public class MainClass
{
public static void Main()
{
EventTest e = new EventTest(); /* 实例化对象,第一次没有触发事件 */
subscribEvent v = new subscribEvent(); /* 实例化对象 */
e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 注册 */
e.SetValue( 7 );
e.SetValue( 11 );
}
}
}
e.KeyCode == Keys.Enter