public static WriteableBitmap BuildColorBitmap(CTSliceInfo ct, byte[,] normalizedPixelBuffer) { byte[] imageDataArray = new byte[ct.RowCount * ct.ColumnCount * 4]; int i = 0; for (int r = 0; r != ct.RowCount; ++r) { for (int c = 0; c != ct.ColumnCount; ++c) { byte aGrayValue = normalizedPixelBuffer[r, c]; // Black/White image: all RGB values are set to same value // Alpha value is set to 255 imageDataArray[i * 4] = aGrayValue; imageDataArray[i * 4 + 1] = aGrayValue; imageDataArray[i * 4 + 2] = aGrayValue; imageDataArray[i * 4 + 3] = 255; ++i; } } // Allocate the Bitmap WriteableBitmap bitmap = new WriteableBitmap(ct.ColumnCount, ct.RowCount, 96, 96, PixelFormats.Pbgra32, null); // Write the Pixels Int32Rect imageRectangle = new Int32Rect(0, 0, ct.ColumnCount, ct.RowCount); int imageStride = ct.ColumnCount * bitmap.Format.BitsPerPixel / 8; bitmap.WritePixels(imageRectangle, imageDataArray, imageStride, 0); return bitmap; }
// Creates a new GridCell for adjacent CT Slices. // For the index convention, please refer to the article 'Polygonising a scalar field' written by Paul Bourke. // http://paulbourke.net/geometry/polygonise/ public GridCell(int theSliceIndex, int theRowIndex, int theColumnIndex, CTSliceInfo CTSliceFront, CTSliceInfo CTSliceBack) { double X_Left_Front = CTSliceFront.UpperLeft_X + (theColumnIndex * CTSliceFront.PixelSpacing_X); double X_Right_Front = X_Left_Front + CTSliceFront.PixelSpacing_X; double X_Left_Back = CTSliceBack.UpperLeft_X + (theColumnIndex * CTSliceBack.PixelSpacing_X); double X_Right_Back = X_Left_Back + CTSliceBack.PixelSpacing_X; double Y_Top_Front = CTSliceFront.UpperLeft_Y + (theRowIndex * CTSliceFront.PixelSpacing_Y); double Y_Botton_Front = Y_Top_Front + CTSliceFront.PixelSpacing_Y; double Y_Top_Back = CTSliceBack.UpperLeft_Y + (theRowIndex * CTSliceBack.PixelSpacing_Y); double Y_Botton_Back = Y_Top_Back + CTSliceBack.PixelSpacing_Y; double Z_Front = CTSliceFront.UpperLeft_Z; double Z_Back = CTSliceBack.UpperLeft_Z; p[0] = new Point3D(X_Left_Back, Y_Botton_Back, Z_Back); p[1] = new Point3D(X_Right_Back, Y_Botton_Back, Z_Back); p[2] = new Point3D(X_Right_Front, Y_Botton_Front, Z_Front); p[3] = new Point3D(X_Left_Front, Y_Botton_Front, Z_Front); p[4] = new Point3D(X_Left_Back, Y_Top_Back, Z_Back); p[5] = new Point3D(X_Right_Back, Y_Top_Back, Z_Back); p[6] = new Point3D(X_Right_Front, Y_Top_Front, Z_Front); p[7] = new Point3D(X_Left_Front, Y_Top_Front, Z_Front); val[0] = CTSliceBack[theRowIndex + 1, theColumnIndex]; val[1] = CTSliceBack[theRowIndex + 1, theColumnIndex + 1]; val[2] = CTSliceFront[theRowIndex + 1, theColumnIndex + 1]; val[3] = CTSliceFront[theRowIndex + 1, theColumnIndex]; val[4] = CTSliceBack[theRowIndex, theColumnIndex]; val[5] = CTSliceBack[theRowIndex, theColumnIndex + 1]; val[6] = CTSliceFront[theRowIndex, theColumnIndex + 1]; val[7] = CTSliceFront[theRowIndex, theColumnIndex]; }
// Gray Level Thresholding public static short GLThresholding(CTSliceInfo ct, int sr, short threshold, short tissc, short lungc) { int nr = ct.RowCount; int nc = ct.ColumnCount; short[,] bm = ct.HounsfieldPixelBuffer; // iterative Tuple <float, float> q = AverageThreshold(ct, sr, threshold, -900); short newthr = Round(0.5f * (q.Item1 + q.Item2)); q = AverageThreshold(ct, sr, newthr, -900); threshold = Round(0.5f * (q.Item1 + q.Item2)); for (int r = 0; r != nr; ++r) { for (int c = 0; c != nc; ++c) { short v = bm[r, c]; bm[r, c] = tissc; if (v < threshold) { bm[r, c] = lungc; } } } return(threshold); }
// Creates a new GridCell for adjacent CT Slices. // For the index convention, please refer to the article 'Polygonising a scalar field' written by Paul Bourke. // http://paulbourke.net/geometry/polygonise/ public GridCell(int theSliceIndex, int theRowIndex, int theColumnIndex, CTSliceInfo CTSliceFront, CTSliceInfo CTSliceBack) { double X_Left_Front = CTSliceFront.UpperLeft_X + (theColumnIndex * CTSliceFront.PixelSpacing_X); double X_Right_Front = X_Left_Front + CTSliceFront.PixelSpacing_X; double X_Left_Back = CTSliceBack.UpperLeft_X + (theColumnIndex * CTSliceBack.PixelSpacing_X); double X_Right_Back = X_Left_Back + CTSliceBack.PixelSpacing_X; double Y_Top_Front = CTSliceFront.UpperLeft_Y + (theRowIndex * CTSliceFront.PixelSpacing_Y); double Y_Botton_Front = Y_Top_Front + CTSliceFront.PixelSpacing_Y; double Y_Top_Back = CTSliceBack.UpperLeft_Y + (theRowIndex * CTSliceBack.PixelSpacing_Y); double Y_Botton_Back = Y_Top_Back + CTSliceBack.PixelSpacing_Y; double Z_Front = CTSliceFront.UpperLeft_Z; double Z_Back = CTSliceBack.UpperLeft_Z; p[0] = new Point3D( X_Left_Back, Y_Botton_Back , Z_Back); p[1] = new Point3D( X_Right_Back, Y_Botton_Back , Z_Back); p[2] = new Point3D( X_Right_Front, Y_Botton_Front , Z_Front); p[3] = new Point3D( X_Left_Front, Y_Botton_Front , Z_Front); p[4] = new Point3D( X_Left_Back, Y_Top_Back , Z_Back); p[5] = new Point3D( X_Right_Back, Y_Top_Back , Z_Back); p[6] = new Point3D( X_Right_Front, Y_Top_Front , Z_Front); p[7] = new Point3D( X_Left_Front, Y_Top_Front , Z_Front); val[0] = CTSliceBack[theRowIndex + 1, theColumnIndex]; val[1] = CTSliceBack[theRowIndex + 1, theColumnIndex + 1]; val[2] = CTSliceFront[theRowIndex + 1, theColumnIndex + 1]; val[3] = CTSliceFront[theRowIndex + 1, theColumnIndex]; val[4] = CTSliceBack[theRowIndex, theColumnIndex]; val[5] = CTSliceBack[theRowIndex, theColumnIndex + 1]; val[6] = CTSliceFront[theRowIndex, theColumnIndex + 1]; val[7] = CTSliceFront[theRowIndex, theColumnIndex]; }
public void AddImageSlice(CTSliceInfo ct) { ImageSlice newImageSlice = new ImageSlice(ct, the3DModel); // Insert new Image Slice at the end _ImageSliceList.Add(_ImageSliceList.Count, newImageSlice); }
// Create new ImageSlice public ImageSlice(CTSliceInfo ct, ModelVisual3D the3DModel) { _ct = ct; _FileName = ct.FileName; _ZValue = ct.UpperLeft_Z.ToString(); _3DModel = the3DModel; }
// true if value is replaced public bool Add(CTSliceInfo ct) { string fname = ct.FileName; int sliceloc = ct.SliceLoc; bool rc = _slices_by_locn.ContainsKey(sliceloc); _slices_by_name[fname] = ct; _slices_by_locn[sliceloc] = ct; return true; }
private static List <Triangle> ComputeTriangles(CTSliceInfo[] slices, int theIsoValue) { // 1. Calculate the Center Point // ============================= // For moving the 3D model with the mouse, the implementation is taken from Code Project 'WPF 3D Primer'. // See also: 'http://www.codeproject.com/Articles/23332/WPF-3D-Primer#' // However, this implementation needs the 3D model to be centered in the origin of the coordinate system. // As a consequence, all CT Slices have to be shifted by the Center Point (method 'AdjustPatientPositionToCenterPoint()') CTSliceInfo firstCT = slices[0]; CTSliceInfo lastCT = slices[slices.Length - 1]; double Center_X = firstCT.UpperLeft_X + (firstCT.PixelSpacing_X * firstCT.ColumnCount / 2); double Center_Y = firstCT.UpperLeft_Y + (firstCT.PixelSpacing_Y * firstCT.RowCount / 2); // CT Slices are already sorted ascending in Z direction double Center_Z = firstCT.UpperLeft_Z + ((lastCT.UpperLeft_Z - firstCT.UpperLeft_Z) / 2); // Create the Center Point Point3D aCenterPoint = new Point3D(Center_X, Center_Y, Center_Z); // 2. The Marching Cubes algorithm // =============================== // For each Voxel of the CT Slice, an own GridCell is created. // The IsoValue and the x/y/z information for each corner of the GridCell is taken from the direct neighbor voxels (of the same or adjacant CT Slice). // Looping has to be done over all CT Slices. List <Triangle> triangles = new List <Triangle>(); CTSliceInfo slice1 = null; CTSliceInfo slice2 = slices[0]; slice2.AdjustPatientPositionToCenterPoint(aCenterPoint); for (int sliceIdx = 1; sliceIdx != slices.Length; ++sliceIdx) { slice1 = slice2; slice2 = slices[sliceIdx]; slice2.AdjustPatientPositionToCenterPoint(aCenterPoint); for (int r = 0; r != slice1.RowCount - 1; ++r) { for (int c = 0; c != slice1.ColumnCount - 1; ++c) { GridCell aGridCell = new GridCell(sliceIdx, r, c, slice1, slice2); MarchingCubes.Polygonise(aGridCell, theIsoValue, ref triangles); } } } return(triangles); }
// Creates the Volume View out of a given list of CT Slices for the specified Iso Value. public void CreateVolume(CTSliceInfo[] slices, int theIsoValue) { CTSliceInfo firstCT = slices[0]; CTSliceInfo lastCT = slices[slices.Length - 1]; List <Triangle> triangles = ComputeTriangles(slices, theIsoValue); MeshGeometry3D mesh = ComputeMesh(triangles); // Last step is to give the mesh to the WPF viewport in order to render it. mGeometryModel = new GeometryModel3D(mesh, new DiffuseMaterial(new SolidColorBrush(Colors.Red))); mGeometryModel.Transform = new Transform3DGroup(); // GeometryModel3D qqq = new GeometryModel3D(ComputeMesh(ComputeTriangles(slices, theIsoValue-100)), // new DiffuseMaterial(new SolidColorBrush(Colors.Blue))); // qqq.Transform = mGeometryModel.Transform; Model3DGroup a3DGroup = new Model3DGroup(); a3DGroup.Children.Add(mGeometryModel); // a3DGroup.Children.Add(qqq); m3DModel.Content = a3DGroup; // Dump some useful size information. mInfoLabel.Text = string.Format("CT Pixel Data\n"); mInfoLabel.Text += string.Format("{0} x {1} x {2} = {3:### ### ### ###}\n\n", firstCT.RowCount, firstCT.ColumnCount, slices.Length, firstCT.RowCount * firstCT.ColumnCount * slices.Length); mInfoLabel.Text += string.Format("Marching Cubes\n"); mInfoLabel.Text += string.Format("IsoValue: {0}, Triangle Count: {1:### ### ### ###}\n\n", theIsoValue, triangles.Count); mInfoLabel.Text += string.Format("Mesh Size\n"); mInfoLabel.Text += string.Format("Points: {0:### ### ### ###}", triangles.Count * 3); // 4. Camera Setup // =============== // We assume the maximum size of the model in Z direction double aEstimatedModelSize = lastCT.UpperLeft_Z - firstCT.UpperLeft_Z; // Setup the camera position. A reasonable value for the model/camera distance is choosen. // In order to rotate the 3D model via the mouse, the implementation from the Code Project 'WPF 3D Primer' is taken. // However, in order to use this proposal out of the box, the camera must be positioned on the Z axis only. mViewPortCamera.Position = new Point3D(0, 0, -(aEstimatedModelSize * 3)); // The 3D model is centered in the origin of the coordinate system. // Hence, camera and light direction should point to the origin. mViewPortCamera.LookDirection = new Point3D(0, 0, 0) - mViewPortCamera.Position; mViewPortLight.Direction = mViewPortCamera.LookDirection; }
private void CreateMaskedVolumeView(short theIsoValueInHounsfield, byte[][,] mask, CTSliceInfo[] slices) { // shall be used only once, because original slice data will be altered // require data reloading for (int k = 0; k != slices.Length; ++k) { CTSliceInfo ct = slices[k]; short[,] buffer = ct.HounsfieldPixelBuffer; byte[,] maska = mask[k]; for (int r = 0; r != ct.RowCount; ++r) { for (int c = 0; c != ct.ColumnCount; ++c) { if (maska[r, c] > 0) { buffer[r, c] = theIsoValueInHounsfield; } } } } if (slices.Length > 2) { VolumeView aVolumeViewWindow = new VolumeView(); Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait; // Create the Volume for the specified IOD list / IsoValue. aVolumeViewWindow.CreateVolume(slices, theIsoValueInHounsfield); aVolumeViewWindow.Title = string.Format("DICOM Viewer - Volume View (IsoValue = {0} in Hounsfield Units)", theIsoValueInHounsfield.ToString()); Mouse.OverrideCursor = null; aVolumeViewWindow.ShowDialog(); } else { System.Windows.MessageBox.Show("The series does not have suffcient CT Slices in order to generate a Volume View!"); } }
public static Tuple <float, float> AverageThreshold(CTSliceInfo ct, int sr, short threshold, short skipthr) { int nr = ct.RowCount; int nc = ct.ColumnCount; short[,] bm = ct.HounsfieldPixelBuffer; int av_high = 0; int av_low = 0; int nof_high = 0; int nof_low = 0; for (int r = 0; r != sr; ++r) { for (int c = 0; c != nc; ++c) { short v = bm[r, c]; if (v < skipthr) { continue; } if (v > threshold) { av_high += v; ++nof_high; continue; } av_low += v; ++nof_low; } } return(new Tuple <float, float>((float)av_low / (float)nof_low, (float)av_high / (float)nof_high)); }
public static short[,] Apply(CTSliceInfo ct, GaussBlur gb) { short[,] hmap = ct.HounsfieldPixelBuffer; int nr = ct.RowCount; int nc = ct.ColumnCount; int br = gb.br; int bc = gb.bc; float[,] blur = gb.blur; short[,] bm = new short[nr, nc]; int l = bm.Length; Array.Clear(bm, 0, bm.Length); for (int r = br; r != nr-br; ++r) { for (int c = bc; c != nc-bc; ++c) { float s = 0.0f; for (int ir = -br; ir <= br; ++ir) { int sr = r + ir; for (int ic = -bc; ic <= bc; ++ic) { int sc = c + ic; s += (float)hmap[sr, sc] * blur[ir + br, ic + bc]; } } bm[r, c] = (short)(s + 0.5f); } } return bm; }
public static WriteableBitmap BuildGrey8Bitmap(CTSliceInfo ct, byte[,] normalizedPixelBuffer) { byte[] imageDataArray = new byte[ct.RowCount * ct.ColumnCount * 1]; int i = 0; for (int r = 0; r != ct.RowCount; ++r) { for (int c = 0; c != ct.ColumnCount; ++c) { byte grayValue = normalizedPixelBuffer[r, c]; imageDataArray[i] = grayValue; ++i; } } // Allocate the Bitmap WriteableBitmap bitmap = new WriteableBitmap(ct.ColumnCount, ct.RowCount, 96, 96, PixelFormats.Gray8, null); // Write the Pixels Int32Rect imageRectangle = new Int32Rect(0, 0, ct.ColumnCount, ct.RowCount); int imageStride = ct.ColumnCount * bitmap.Format.BitsPerPixel / 8; bitmap.WritePixels(imageRectangle, imageDataArray, imageStride, 0); return bitmap; }
private void ButtonLungs_Click(object sender, RoutedEventArgs e) { bool rc = _scol.BuildSortedSlicesArray(); if (!rc) { System.Windows.MessageBox.Show("There are skips in CTs!"); return; } CTSliceInfo[] slices = _scol.Slices; // Step 1: find the couch int sr = SelectStartSlice(slices); CTSliceInfo ct = slices[sr]; int sc = Couch.DetectCouchInOneSlice(ct.HounsfieldPixelBuffer, ct.RowCount, ct.ColumnCount); int scBefore = Couch.DetectCouchInOneSlice(slices[sr + 10].HounsfieldPixelBuffer, slices[sr + 10].RowCount, slices[sr + 10].ColumnCount); int scAfter = Couch.DetectCouchInOneSlice(slices[sr - 10].HounsfieldPixelBuffer, slices[sr - 10].RowCount, slices[sr - 10].ColumnCount); sc = Math.Max(Math.Max(sc, scBefore), scAfter); // Step 2: Gaussian blur GaussBlur gb = new GaussBlur((float)ct.PixelSpacing_X, (float)ct.PixelSpacing_X, 5); for (int k = 0; k != slices.Length; ++k) { ct = slices[k]; short[,] bm = CTSliceHelpers.Apply(ct, gb); ct.HounsfieldPixelBuffer = bm; } // Step 3: clear below the couch for (int k = 0; k != slices.Length; ++k) { ct = slices[k]; short[,] hb = ct.HounsfieldPixelBuffer; for (int r = ct.RowCount - 1; r > sc; --r) { for (int c = 0; c != ct.ColumnCount; ++c) { hb[r, c] = -1024; } } } // Step 4: gray level thresholding for (int k = 0; k != slices.Length; ++k) { ct = slices[k]; Couch.GLThresholding(ct, sc, -499, 0, -499); } // Step 5: Flool fill for (int k = 0; k != slices.Length; ++k) { ct = slices[k]; short[,] ret = Couch.FloodFill(ct.HounsfieldPixelBuffer, ct.RowCount, ct.ColumnCount, 3, 3, -499, 0); ct.HounsfieldPixelBuffer = ret; } // Step 6: Contours via Moore Neighbour for (int k = slices.Length - 1; k >= slices.Length - 2; --k) { ct = slices[k]; int nr = ct.RowCount; int nc = ct.ColumnCount; int z = ct.SliceLoc; double zz = ct.UpperLeft_Z; short[,] bm = ct.HounsfieldPixelBuffer; bool[,] image = new bool[nr, nc]; for (int r = 0; r != nr; ++r) { for (int c = 0; c != nc; ++c) { image[r, c] = false; if (bm[r, c] < 0) { image[r, c] = true; } } } System.Drawing.Point[] contour = MooreContour.Trace(image, nr, nc); foreach (var pt in contour) { int r = pt.Y; int c = pt.X; bm[r, c] = 500; } } }
// Helper method to handle the selection change event of the IOD Tree. // a) In case the selected tree node represents only group information (Patient, SOPClass, Study, Series), the detailed view is cleared. // b) In case the selected tree node represents an IOD, the DICOM Metainformation is displayed in the DICOM Tag Tree. // c) In case the selected tree node represents a CT Slice, in addition to the DICOM Metainformation, // the ImageFlow button, the volume buttons and the bitmap is shown. private void mIODTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs <object> e) { TreeViewItem aSelectedNode = _IODTree.SelectedItem as TreeViewItem; if (aSelectedNode == null) { return; } // Clear old content _DICOMTagTree.Items.Clear(); _Grid.RowDefinitions.First().Height = new GridLength(0); _Grid.RowDefinitions.Last().Height = new GridLength(0); IOD anIOD = aSelectedNode.Tag as IOD; if (anIOD == null) { return; } // Set the FileName as root node string aFileName = Path.GetFileName(anIOD.FileName); TreeViewItem rootNode = new TreeViewItem() { Header = string.Format("File: {0}", aFileName) }; _DICOMTagTree.Items.Add(rootNode); // Expand the root node rootNode.IsExpanded = true; // Add all DICOM attributes to the tree foreach (XElement xe in anIOD.XDocument.Descendants("DataSet").First().Elements("DataElement")) { AddDICOMAttributeToTree(rootNode, xe); } // In case the IOD does have a processable pixel data, the ImageFlow button, the volume buttons and the bitmap is shown. // Otherwise, only the DICOM attributes are shown and the first and last grid row is hided. if (anIOD.IsPixelDataProcessable()) { CTSliceInfo ct = _scol.Retrieve(anIOD.FileName); if (ct == null) { ct = new Helper.CTSliceInfo(anIOD.XDocument, anIOD.FileName); _scol.Add(ct); } _Grid.RowDefinitions.First().Height = new GridLength(30); _Grid.RowDefinitions.Last().Height = new GridLength(ct.RowCount + 16); _Image.Source = CTSliceHelpers.GetPixelBufferAsBitmap(ct); _curCT = ct; } else { _Grid.RowDefinitions.First().Height = new GridLength(0); _Grid.RowDefinitions.Last().Height = new GridLength(0); _curCT = null; } }
private static List<Triangle> ComputeTriangles(CTSliceInfo[] slices, int theIsoValue) { // 1. Calculate the Center Point // ============================= // For moving the 3D model with the mouse, the implementation is taken from Code Project 'WPF 3D Primer'. // See also: 'http://www.codeproject.com/Articles/23332/WPF-3D-Primer#' // However, this implementation needs the 3D model to be centered in the origin of the coordinate system. // As a consequence, all CT Slices have to be shifted by the Center Point (method 'AdjustPatientPositionToCenterPoint()') CTSliceInfo firstCT = slices[0]; CTSliceInfo lastCT = slices[slices.Length - 1]; double Center_X = firstCT.UpperLeft_X + (firstCT.PixelSpacing_X * firstCT.ColumnCount / 2); double Center_Y = firstCT.UpperLeft_Y + (firstCT.PixelSpacing_Y * firstCT.RowCount / 2); // CT Slices are already sorted ascending in Z direction double Center_Z = firstCT.UpperLeft_Z + ((lastCT.UpperLeft_Z - firstCT.UpperLeft_Z) / 2); // Create the Center Point Point3D aCenterPoint = new Point3D(Center_X, Center_Y, Center_Z); // 2. The Marching Cubes algorithm // =============================== // For each Voxel of the CT Slice, an own GridCell is created. // The IsoValue and the x/y/z information for each corner of the GridCell is taken from the direct neighbor voxels (of the same or adjacant CT Slice). // Looping has to be done over all CT Slices. List<Triangle> triangles = new List<Triangle>(); CTSliceInfo slice1 = null; CTSliceInfo slice2 = slices[0]; slice2.AdjustPatientPositionToCenterPoint(aCenterPoint); for (int sliceIdx = 1; sliceIdx != slices.Length; ++sliceIdx) { slice1 = slice2; slice2 = slices[sliceIdx]; slice2.AdjustPatientPositionToCenterPoint(aCenterPoint); for (int r = 0; r != slice1.RowCount - 1; ++r) { for (int c = 0; c != slice1.ColumnCount - 1; ++c) { GridCell aGridCell = new GridCell(sliceIdx, r, c, slice1, slice2); MarchingCubes.Polygonise(aGridCell, theIsoValue, ref triangles); } } } return triangles; }
// Creates the Volume View out of a given list of CT Slices for the specified Iso Value. public void CreateVolume(CTSliceInfo[] slices, int theIsoValue) { CTSliceInfo firstCT = slices[0]; CTSliceInfo lastCT = slices[slices.Length - 1]; List<Triangle> triangles = ComputeTriangles(slices, theIsoValue); MeshGeometry3D mesh = ComputeMesh(triangles); // Last step is to give the mesh to the WPF viewport in order to render it. mGeometryModel = new GeometryModel3D(mesh, new DiffuseMaterial(new SolidColorBrush(Colors.Red))); mGeometryModel.Transform = new Transform3DGroup(); // GeometryModel3D qqq = new GeometryModel3D(ComputeMesh(ComputeTriangles(slices, theIsoValue-100)), // new DiffuseMaterial(new SolidColorBrush(Colors.Blue))); // qqq.Transform = mGeometryModel.Transform; Model3DGroup a3DGroup = new Model3DGroup(); a3DGroup.Children.Add(mGeometryModel); // a3DGroup.Children.Add(qqq); m3DModel.Content = a3DGroup; // Dump some useful size information. mInfoLabel.Text = string.Format("CT Pixel Data\n"); mInfoLabel.Text += string.Format("{0} x {1} x {2} = {3:### ### ### ###}\n\n", firstCT.RowCount, firstCT.ColumnCount, slices.Length, firstCT.RowCount * firstCT.ColumnCount * slices.Length); mInfoLabel.Text += string.Format("Marching Cubes\n"); mInfoLabel.Text += string.Format("IsoValue: {0}, Triangle Count: {1:### ### ### ###}\n\n", theIsoValue, triangles.Count); mInfoLabel.Text += string.Format("Mesh Size\n"); mInfoLabel.Text += string.Format("Points: {0:### ### ### ###}", triangles.Count * 3); // 4. Camera Setup // =============== // We assume the maximum size of the model in Z direction double aEstimatedModelSize = lastCT.UpperLeft_Z - firstCT.UpperLeft_Z; // Setup the camera position. A reasonable value for the model/camera distance is choosen. // In order to rotate the 3D model via the mouse, the implementation from the Code Project 'WPF 3D Primer' is taken. // However, in order to use this proposal out of the box, the camera must be positioned on the Z axis only. mViewPortCamera.Position = new Point3D(0, 0, -(aEstimatedModelSize * 3)); // The 3D model is centered in the origin of the coordinate system. // Hence, camera and light direction should point to the origin. mViewPortCamera.LookDirection = new Point3D(0, 0, 0) - mViewPortCamera.Position; mViewPortLight.Direction = mViewPortCamera.LookDirection; }
// build bitmap directly from Hounsfield map, shifted by 1024 public static WriteableBitmap BuildGrey16Bitmap(CTSliceInfo ct, byte[,] normalizedPixelBuffer) { byte[] imageDataArray = new byte[ct.RowCount * ct.ColumnCount * 2]; int i = 0; for (int r = 0; r != ct.RowCount; ++r) { for (int c = 0; c != ct.ColumnCount; ++c) { ushort grayValue = (ushort)(ct[r, c] + 1024); imageDataArray[i * 2 + 0] = (byte)((grayValue >> 8) & 0x00FF); imageDataArray[i * 2 + 1] = (byte)(grayValue & 0x00FF); ++i; } } // Allocate the Bitmap WriteableBitmap bitmap = new WriteableBitmap(ct.ColumnCount, ct.RowCount, 96, 96, PixelFormats.Gray16, null); // Write the Pixels Int32Rect imageRectangle = new Int32Rect(0, 0, ct.ColumnCount, ct.RowCount); int imageStride = ct.ColumnCount * bitmap.Format.BitsPerPixel / 8; bitmap.WritePixels(imageRectangle, imageDataArray, imageStride, 0); return bitmap; }
// Helper method, which returns the pixel data of a CT slice as gray-scale bitmap. public static WriteableBitmap GetPixelBufferAsBitmap(CTSliceInfo ct) { int windowLeftBorder = ct.WindowCenter - (ct.WindowWidth / 2); /* GaussBlur gb = new GaussBlur(1.0f, (float)ct.PixelSpacing_X, 5); short[,] bm = Apply(ct, gb); ct.HounsfieldPixelBuffer = bm; */ byte[,] normalizedPixelBuffer = BuildNormalizedPixelBuffer(ct, ct.WindowCenter, ct.WindowWidth, windowLeftBorder); WriteableBitmap bitmap = BuildGrey16Bitmap(ct, normalizedPixelBuffer); return bitmap; }
public static byte[,] BuildNormalizedPixelBuffer(CTSliceInfo ct, int windowCenter, int windowWidth, int windowLeftBorder) { Debug.Assert(windowWidth > 0); byte[,] normalizedPixelBuffer = new byte[ct.RowCount, ct.ColumnCount]; // Normalize the Pixel value to [0,255] for (int r = 0; r != ct.RowCount; ++r) { for (int c = 0; c != ct.ColumnCount; ++c) { short aPixelValue = ct[r, c]; int aPixelValueNormalized = (255 * (aPixelValue - windowLeftBorder)) / windowWidth; if (aPixelValueNormalized <= 0) normalizedPixelBuffer[r, c] = 0; else if (aPixelValueNormalized >= 255) normalizedPixelBuffer[r, c] = 255; else normalizedPixelBuffer[r, c] = Convert.ToByte(aPixelValueNormalized); } } return normalizedPixelBuffer; }