Exporting excel chart as image - c#

I have an Excel file which has a charts, these charts represent data in columns, in my program I change these data in columns and chart changes also, after that I export these charts in .png files, but there I met an exception HRESULT: 0x80030020 (STG_E_SHAREVIOLATION)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Excel = Microsoft.Office.Interop.Excel;
using RTO.Models;
using Novacode;
using System.Drawing;
using Word = Microsoft.Office.Interop.Word;
using System.Reflection;
using CommonLib.SharedModels;
namespace RTO
{
class Program
{
public static void ReportRTO(RtoCommonData cmnData, List<Antenna> antennas)
{
Novacode.Image imageh, imagev, image1, image2;
Picture pictureh, picturev, picture1, picture2;
Paragraph pimg;
var exApp = new Excel.Application();
exApp.ScreenUpdating = false;
var exBook = exApp.Workbooks.Open(fileLeaf);
var exSheet = exBook.Worksheets[1] as Excel.Worksheet;
Excel.Range r1 = exSheet.get_Range("A1", "A360");
Excel.Range r2 = exSheet.get_Range("B1", "B360");
double[,] d1 = new double[360, 1];
double[,] d2 = new double[360, 1];
int w = 1;
var application = new Excel.Application();
application.ScreenUpdating = false;
var workbook = application.Workbooks.Open(fileExcel);
var worksheet = workbook.Worksheets[1] as Excel.Worksheet;
Excel.Range rng1 = worksheet.get_Range("A1", "A361");
Excel.Range rng2 = worksheet.get_Range("B1", "B361");
Excel.Range rng3 = worksheet.get_Range("C1", "C361");
Excel.Range rng4 = worksheet.get_Range("D1", "D361");
double[,] data1 = new double[361, 1];
double[,] data2 = new double[361, 1];
double[,] data3 = new double[361, 1];
double[,] data4 = new double[361, 1];
int flnmadd = 1;
for (int i = 0; i < antennas.Count; i++)
{
//Save chart as image
w = 1;
foreach (Excel.Worksheet ws in exBook.Worksheets)
{
Excel.ChartObjects chartObjects = (Excel.ChartObjects)(ws.ChartObjects(Type.Missing));
foreach (Excel.ChartObject co in chartObjects)
{
co.Select();
Excel.Chart chart = co.Chart;
chart.Export(exportPath + #"\leaf" + w + ".png", "PNG", false);
w++;
}
}
//Insert image to doc
image1 = doc.AddImage(leafimg1);
picture1 = image1.CreatePicture();
picture1.Width = 310;
picture1.Height = 310;
image2 = doc.AddImage(leafimg2);
picture2 = image2.CreatePicture();
picture2.Width = 310;
picture2.Height = 310;
pimg = doc.InsertParagraph();
pimg.AppendPicture(picture1);
pimg.AppendPicture(picture2);
for (int j = 0; j < boztrows; j++)
{
data1[j, 0] = sumbozres[i].Rxhor[j];
data2[j, 0] = sumbozres[i].Rzhor[j];
data3[j, 0] = sumbozres[i].Rxver[j];
data4[j, 0] = sumbozres[i].Rzver[j];
}
data1[boztrows, 0] = data1[0, 0];
data2[boztrows, 0] = data2[0, 0];
data3[boztrows, 0] = data3[0, 0];
data4[boztrows, 0] = data4[0, 0];
rng1.Value = data1;
rng2.Value = data2;
rng3.Value = data3;
rng4.Value = data4;
//Save chart as image
flnmadd = 1;
foreach (Excel.Worksheet ws in workbook.Worksheets)
{
Excel.ChartObjects chartObjects = (Excel.ChartObjects)(ws.ChartObjects(Type.Missing));
foreach (Excel.ChartObject co in chartObjects)
{
co.Select();
Excel.Chart chart = co.Chart;
chart.Export(exportPath + #"\charthv" + flnmadd + ".png", "PNG", false);
flnmadd++;
}
}
//Insert image to doc
if (antennas[i].Type == "БС")
{
imageh = doc.AddImage(charthimg);
pictureh = imageh.CreatePicture();
pictureh.Width = 624;
pictureh.Height = 357;
imagev = doc.AddImage(chartvimg);
picturev = imagev.CreatePicture();
picturev.Width = 624;
picturev.Height = 156;
pimg = doc.InsertParagraph();
pimg.AppendPicture(pictureh);
pimg = doc.InsertParagraph();
pimg.AppendPicture(picturev);
}
else if (antennas[i].Type == "РРС")
{
imageh = doc.AddImage(rrsimg);
pictureh = imageh.CreatePicture();
pictureh.Width = 624;
pictureh.Height = 156;
pimg = doc.InsertParagraph();
pimg.AppendPicture(pictureh);
}
trsprev += trs;
freqs = "";
pows = "";
koefgs = "";
koefgrazs = "";
poteri = "";
poteriraz = "";
freqAvg = 0;
}
exBook.Save();
exBook.Close();
exApp.Workbooks.Close();
exApp.Quit();
workbook.Save();
workbook.Close();
application.Workbooks.Close();
application.Quit();
}
}
}

Could be that your program has two instances of the same file. Another thing can be save the file before you're trying to save the picture.

As pointed out in the question comments HRESULT: 0x80030020 (STG_E_SHAREVIOLATION) is "Access denied because another caller has the file open and locked" more information here. The file was simply still open/in use, and can be resolved by first deleting the old files.
There are some options, and it depends on how you intend to use the program. Adding a try/catch statement will keep the program from crashing. Besides that I don't see any particular best practice, it depends on usage. In my opinion it is entierly reasonable for the program to exit if it fails to save.
In the interest of offering a solution that you can adjust to your liking: first a method for saving charts that returns true if successful (not pretty but does the job):
using System.IO;
private static bool SaveExcelChartAsPNG(ChartObject co,
string path, string filename)
{
try
{
string filenamePNG = Path.ChangeExtension(filename, "png");
string fullFilenamePNG = Path.Combine(path, filenamePNG);
co.Select();
co.Chart.Export(fullFilenamePNG, "PNG", false);
}
catch
{
// Save was not successful
return false;
}
return true;
}
This solution will exit on unsuccessful save:
foreach (var co in chartObjects)
{
if (!SaveExcelChartAsPNG(exportPath, #"\leaf" + w + ".png"))
Application.Exit();
}
A longer example that will retry saving 10 times by incrementing the 'w' parameter, then try a random filename. If that doesnt work the program will exit.
//Save chart as image
w = 1;
foreach (var ws in exBook.Worksheets)
{
var chartObjects = (Excel.ChartObjects)(ws.ChartObjects(Type.Missing));
foreach (var co in chartObjects)
{
int retry = 0;
bool successfulSave = false;
while (!successfulSave && retry < 10) // retry by incerementing w parameter 10 times)
{
successfulSave = SaveExcelChartAsPNG(exportPath, #"\leaf" + w + ".png"))
retry++;
w++;
}
if (!successfulSave)
{
// Try again with random filename, otherwise exit
string filename = Path.GetRandomFileName();
if (!SaveExcelChartAsPNG(exportPath, filename))
{
// Save still not successful, exit
Application.Exit();
}
}
}
}
And a remark regarding the above code: (First off it's flawed, because if you already generate 10 charts 11 times you will always first generate charts 0-99 then you will end up with 10 charts with completely random names. In which case you might just want to generate random names to begin with.)
In most cases it is not good to catch all exceptions and return true/false. Future problems can arise when some other Exception is raised that is not related to filenames. Both the user and programmer will be left oblivious to what happened. It is better to demand filenames that can be used, perhaps make 'w' or the output filenames an input parameter to the program to offer some flexibility.
One last option could be to instead create a new random output directory to guarantee that it is empty and output the prefered filenames there. Also by using Path.GetRandomFileName() that has the benefit over Path.GetTempFileName() of not creating the file.

Related

How can I pass and format an SQL table into an excel using C#

Currently I have a table with 6 rows and 14 columns.
I'm trying to pass that table to my excel document and I have no problem doing that.
My problem is that I can't format it the way I want.
Idealy I want to have 3 rows, blank space and 3 rows again, but I can't do that.
This is the function I'm currently using to format the Sql table. Basically it writes in excel all the rows consecutively.
Instead of doing that I want it to have a black row between row 3 and row 4.
If someone could help I'd very thankful.
private int Export_putDataGeneric(Excel.Worksheet sh, DataTable ds, String D_ReferenceDate, int starting_row = 5, int[] column_mapping = null, bool isNumber = true)
{
int curr_row = 0;
if (column_mapping == null)
{
column_mapping = new int[ds.Columns.Count];
int start_char = 2;
for (int c = 0; c < ds.Columns.Count; c++)
{
column_mapping[c] = start_char;
start_char++;
}
}
var data = new Object[ds.Rows.Count, column_mapping[ds.Columns.Count - 1] - column_mapping[0] + 1];
foreach (DataRow row in ds.Rows)
{
for (int c = 0; c < ds.Columns.Count; c++)
{
data[curr_row, column_mapping[c] - column_mapping[0]] = row[c];
}
curr_row++;
}
int end_row = starting_row + ds.Rows.Count - 1;
Excel.Range beginWrite = sh.Cells[starting_row, column_mapping[0]] as Excel.Range;
Excel.Range endWrite = sh.Cells[end_row, column_mapping[ds.Columns.Count - 1]] as Excel.Range;
Excel.Range sheetData = sh.Range[beginWrite, endWrite];
sheetData.Value2 = data;
if (isNumber) sheetData.NumberFormat = "#,##0.00";
Marshal.ReleaseComObject(beginWrite);
Marshal.ReleaseComObject(endWrite);
Marshal.ReleaseComObject(sheetData);
beginWrite = null;
endWrite = null;
sheetData = null;
return end_row;
}
You can try using Range.Offset.
Check out the Microsoft Documentation
This question on SO might also help.

Optimize performance of data processing method

I am using the following code to take some data (in XML like format - Not well formed) from a .txt file and then write it to an .xlsx using EPPlus after doing some processing. StreamElements is basically a modified XmlReader. My question is about performance, I have made a couple of changes but don't see what else I can do. I'm going to use this for large datasets so I'm trying to modify to make this as efficient and fast as possible. Any help will be appreciated!
I tried using p.SaveAs() to do the excel writing but it did not really see a performance difference. Are there better faster ways to do the writing? Any suggestions are welcome.
using (ExcelPackage p = new ExcelPackage())
{
ExcelWorksheet ws = p.Workbook.Worksheets[1];
ws.Name = "data1";
int rowIndex = 1; int colIndex = 1;
foreach (var element in StreamElements(pa, "XML"))
{
var values = element.DescendantNodes().OfType<XText>()
.Select(v => Regex.Replace(v.Value, "\\s+", " "));
string[] data = string.Join(",", values).Split(',');
data[2] = toDateTime(data[2]);
for (int i = 0; i < data.Count(); i++)
{
if (rowIndex < 1000000)
{
var cell1 = ws.Cells[rowIndex, colIndex];
cell1.Value = data[i];
colIndex++;
}
}
rowIndex++;
}
}
ws.Cells[ws.Dimension.Address].AutoFitColumns();
Byte[] bin = p.GetAsByteArray();
using (FileStream fs = File.OpenWrite("C:\\test.xlsx"))
{
fs.Write(bin, 0, bin.Length);
}
}
}
Currently, for it to do the processing and then write 1 Million lines into an Excel worksheet, it takes about ~30-35 Minutes.
I've ran into this issue before and excel has a huge overhead when you're modifying worksheet cells individually one by one.
The solution to this is to create an object array and populate the worksheet using the WriteRange functionality.
using(ExcelPackage p = new ExcelPackage()) {
ExcelWorksheet ws = p.Workbook.Worksheets[1];
ws.Name = "data1";
//Starting cell
int startRow = 1;
int startCol = 1;
//Needed for 2D object array later on
int maxColCount = 0;
int maxRowCount = 0;
//Queue data
Queue<string[]> dataQueue = new Queue<string[]>();
//Tried not to touch this part
foreach(var element in StreamElements(pa, "XML")) {
var values = element.DescendantNodes().OfType<XText>()
.Select(v = > Regex.Replace(v.Value, "\\s+", " "));
//Removed unnecessary split and join, use ToArray instead
string[] eData = values.ToArray();
eData[2] = toDateTime(eData[2]);
//Push the data to queue and increment counters (if needed)
dataQueue.Enqueue(eData);
if(eData.Length > maxColCount)
maxColCount = eData.Length;
maxRowCount++;
}
//We now have the dimensions needed for our object array
object[,] excelArr = new object[maxRowCount, maxColCount];
//Dequeue data from Queue and populate object matrix
int i = 0;
while(dataQueue.Count > 0){
string[] eData = dataQueue.Dequeue();
for(int j = 0; j < eData.Length; j++){
excelArr[i, j] = eData[j];
}
i++;
}
//Write data to range
Excel.Range c1 = (Excel.Range)wsh.Cells[startRow, startCol];
Excel.Range c2 = (Excel.Range)wsh.Cells[startRow + maxRowCount - 1, maxColCount];
Excel.Range range = worksheet.Range[c1, c2];
range.Value2 = excelArr;
//Tried not to touch this stuff
ws.Cells[ws.Dimension.Address].AutoFitColumns();
Byte[] bin = p.GetAsByteArray();
using(FileStream fs = File.OpenWrite("C:\\test.xlsx")) {
fs.Write(bin, 0, bin.Length);
}
}
I didn't try compiling this code, so double check the indexing used; and check for any small syntax errors.
A few extra pointers to consider for performance:
Try to parallel the population of the object array, since it is primarily index based (maybe have a dictionary with an index tracker Dictionary<int, string[]>) and lookup in there for faster population of the object array. You would likely have to trade space for time.
See if you are able to hardcode the column and row counts, or figure it out quickly. In my code fix, I've set counters to count the maximum rows and columns on the fly; I wouldn't recommend it as a permanent solution.
AutoFitColumns is very costly, especially if you're dealing with over a million rows

'System.AccessViolationException' occurred

UPDATED: added full block of code where error occurs
UPDATE 2: I found a weird anomaly. The code has now been continuously breaking on that line, when the tabName variable equals "service line prior year". This morning, for grins, I changed the tab name to "test", so in turn the tabName variable equals "test", and it worked more often then not. I am really at a loss.
I have researched a ton and can't find anything that addresses what is happening in my code. It happens randomly though. Sometimes it doesn't happen, then other times it happens in the same spot, but all on this part of the code (on the line templateSheet = templateBook.Sheets[tabName];):
public void ExportToExcel(DataSet dataSet, string filePath, int i, int h, Excel.Application excelApp)
{
//create the excel definitions again.
//Excel.Application excelApp = new Excel.Application();
//excelApp.Visible = true;
FileInfo excelFileInfo = new FileInfo(filePath);
Boolean fileOpenTest = IsFileOpen(excelFileInfo);
Excel.Workbook templateBook;
Excel.Worksheet templateSheet;
//check to see if the template is already open, if its not then open it,
//if it is then bind it to work with it
if (!fileOpenTest)
{ templateBook = excelApp.Workbooks.Open(filePath); }
else
{ templateBook = (Excel.Workbook)System.Runtime.InteropServices.Marshal.BindToMoniker(filePath); }
//this grabs the name of the tab to dump the data into from the "Query Dumps" Tab
string tabName = lstQueryDumpSheet.Items[i].ToString();
templateSheet = templateBook.Sheets[tabName];
excelApp.Calculation = Excel.XlCalculation.xlCalculationManual;
templateSheet = templateBook.Sheets[tabName];
// Copy DataTable
foreach (System.Data.DataTable dt in dataSet.Tables)
{
// Copy the DataTable to an object array
object[,] rawData = new object[dt.Rows.Count + 1, dt.Columns.Count];
// Copy the values to the object array
for (int col = 0; col < dt.Columns.Count; col++)
{
for (int row = 0; row < dt.Rows.Count; row++)
{ rawData[row, col] = dt.Rows[row].ItemArray[col]; }
}
// Calculate the final column letter
string finalColLetter = string.Empty;
string colCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int colCharsetLen = colCharset.Length;
if (dt.Columns.Count > colCharsetLen)
{ finalColLetter = colCharset.Substring((dt.Columns.Count - 1) / colCharsetLen - 1, 1); }
finalColLetter += colCharset.Substring((dt.Columns.Count - 1) % colCharsetLen, 1);
//this grabs the cell address from the "Query Dump" sheet, splits it on the '=' and
//pulls out only the cell address (i.e., "address=a3" becomes "a3")
string dumpCellString = lstQueryDumpText.Items[i].ToString();
string dumpCell = dumpCellString.Split('=').Last();
//referts to the range in which we are dumping the DataSet. The upper right hand cell is
//defined by the 'dumpCell' varaible and the bottom right cell is defined by the
//final column letter and the count of rows.
string firstRef = "";
string baseRow = "";
if (char.IsLetter(dumpCell, 1))
{
char[] createCellRef = dumpCell.ToCharArray();
firstRef = createCellRef[0].ToString() + createCellRef[1].ToString();
for (int z = 2; z < createCellRef.Count(); z++)
{
baseRow = baseRow + createCellRef[z].ToString();
}
}
else
{
char[] createCellRef = dumpCell.ToCharArray();
firstRef = createCellRef[0].ToString();
for (int z = 1; z < createCellRef.Count(); z++)
{
baseRow = baseRow + createCellRef[z].ToString();
}
}
int baseRowInt = Convert.ToInt32(baseRow);
int startingCol = ColumnLetterToColumnIndex(firstRef);
int endingCol = ColumnLetterToColumnIndex(finalColLetter);
int finalCol = startingCol + endingCol;
string endCol = ColumnIndexToColumnLetter(finalCol - 1);
int endRow = (baseRowInt + (dt.Rows.Count - 1));
string cellCheck = endCol + endRow;
string excelRange;
if (dumpCell.ToUpper() == cellCheck.ToUpper())
{
excelRange = string.Format(dumpCell + ":" + dumpCell);
}
else
{
excelRange = string.Format(dumpCell + ":{0}{1}", endCol, endRow);
}
//this dumps the cells into the range on Excel as defined above
templateSheet.get_Range(excelRange, Type.Missing).Value2 = rawData;
//checks to see if all the SQL queries have been run from the "Query Dump" tab, if not, continue
//the loop, if it is the last one, then save the workbook and move on.
if (i == lstSqlAddress.Items.Count - 1)
{
excelApp.Calculation = Excel.XlCalculation.xlCalculationAutomatic;
/*Run through the value save sheet array then grab the address from the corresponding list
place in the address array. If the address reads "whole sheet" then save the whole page,
else set the addresses range and value save that.*/
//for (int y = 0; y < lstSaveSheet.Items.Count; y++)
//{
// MessageBox.Show("Save Sheet: " + lstSaveSheet.Items[y] + "\n" + "Save Address: " + lstSaveRange.Items[y]);
//}
//run the macro to hide the unused columns
excelApp.Run("ReportMakerExecute");
//save excel file as hospital name and move onto the next
SaveTemplateAs(templateBook, h);
//close the open Excel App before looping back
//Marshal.ReleaseComObject(templateSheet);
//Marshal.ReleaseComObject(templateBook);
//templateSheet = null;
//templateBook = null;
//GC.Collect();
//GC.WaitForPendingFinalizers();
}
//Close excel Applications
//excelApp.Quit();
//Marshal.ReleaseComObject(templateSheet);
//Marshal.FinalReleaseComObject(excelApp);
//excelApp = null;
//templateSheet = null;
// GC.Collect();
//GC.WaitForPendingFinalizers();
}
}
The try/catch block is of no use either. This is the error:
"An unhandled exception of type 'System.AccessViolationException' occurred inSQUiRE (Sql QUery REtriever) v1.exe. Additional information: Attempted to read or write protected memory. This is often an indication that other memory is corrupt."
System.AccessViolationException would normally happen when you try to access an unallocated memory in a native code (not .NET). Then .NET translates it to the managed world as this exception.
Your code itself does not have any unsafe block. So access violation must me happening inside Excel.
Given the fact that it sometimes happens, some times not, I would say that it can be caused by a parallel Excel usage (I think the Excel COM is not thread-safe).
I would recommend you putting all your code inside a lock block, to prevent Excel from begin used in parallel. Something like this:
public void ExportToExcel(DataSet dataSet, string filePath, int i, int h, Excel.Application excelApp)
{
lock(this.GetType()) // You can change here to other instance to me used a mutex
{
// Your original code here
}
}
So long story, three days of testing longer, it was because of an excel file that was trying to open and fill with SQL results. The buffer was filling up and causing an exception...it just happened at the same point in every run because the load time for the excel file was the determining factor in it working or failing.
So after the load i just added a delaying do...while that checked to see if the file was accessible or not and it stopped the failures. fileOpenTest was taken from here
do
{
Task.Delay(2000);
}
while(!fileOpenTest);

Setting data used by a line graph using EPPlus

I am trying to add a simple line graph in excel automatically by using EPPlus. I have and know the range of cells that contain the data that I want to use. I want the graph to look like the following:
The Before and After columns go on for a lot more than what is shown.
I am using this code to create the graph and position it, but I am not sure how to set the data that the graph uses:
ExcelChart ec = ws.Drawings.AddChart("Line Time Graph", OfficeOpenXml.Drawing.Chart.eChartType.Line);
ec.Title.Text = "Benchmarks";
ec.SetPosition(_times.Count + 2, 0, 1, 0);
It might have something to do with ec.Series.Add but I am not sure how to properly use it. If you could point me in the right direction that would be great.
Hope this helps you:
ExcelChart ec = ws.Drawings.AddChart("Line Time Graph", OfficeOpenXml.Drawing.Chart.eChartType.Line);
ec.Title.Text = "Benchmarks";
ec.SetPosition(_times.Count + 2, 0, 1, 0);
ec.SetSize(800, 600);
var ran1 = sheet.Cells["A3:A10"];
var ran2 = sheet.Cells["G3:G10"]; // label range if there is. Otherwise, let select blank range then edit XML data later to remove 'c:cat' tags (bellow example)
var serie1 = ec.Series.Add(ran1, ran2);
// use serie1 variable to format and so on
// set serie1.HeaderAddress to A2 also
var ran1 = sheet.Cells["B3:B10"];
var serie2 = ec.Series.Add(ran1, ran2);
// use serie2 variable to format and so on
Another full example I've just tested:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OfficeOpenXml;
using System.IO;
using OfficeOpenXml.Drawing.Chart;
namespace TestExcelEPPluss
{
class Program
{
static void Main(string[] args)
{
ExcelPackage package = new ExcelPackage();
var sheet = package.Workbook.Worksheets.Add("TestingGraph");
Random r = new Random();
var cell = sheet.Cells["A1"];
cell.Value = "Before";
cell = sheet.Cells["B1"];
cell.Value = "After";
for (int i = 0; i < 100; i++)
{
cell = sheet.Cells[i + 2, 1];
cell.Value = r.Next(300, 500);
cell = sheet.Cells[i + 2, 2];
cell.Value = r.Next(300, 500);
}
ExcelChart ec = (ExcelLineChart)sheet.Drawings.AddChart("chart_1", eChartType.Line);
ec.SetPosition(1, 0, 3, 0);
ec.SetSize(800, 300);
//ec.Legend.Add();
var ran1 = sheet.Cells["A2:A101"];
var ran2 = sheet.Cells["0:0"];
var serie1 = (ExcelLineChartSerie)ec.Series.Add(ran1, ran2);
serie1.Header = sheet.Cells["A1"].Value.ToString();
ran1 = sheet.Cells["B2:B101"];
var serie2 = ec.Series.Add(ran1, ran2);
serie2.Header = sheet.Cells["B1"].Value.ToString();
var xml = ec.ChartXml;
var lst = xml.GetElementsByTagName("c:lineChart");
foreach (System.Xml.XmlNode item in lst[0].ChildNodes)
{
if (item.Name.Equals("ser"))
{
foreach (System.Xml.XmlNode subitem in item.ChildNodes)
{
if (subitem.Name.Equals("c:cat"))
{
item.RemoveChild(subitem);
break;
}
}
}
}
string path = #"C:\test1.xlsx";
File.WriteAllBytes(path, package.GetAsByteArray());
package.Dispose();
Console.WriteLine("Done - Path: {0}", path);
Console.ReadLine();
}
}
}
c:cat is the category for ser series, let remove c:cat tags from charts xml, then your chart will use 1,2,3,4,5,... as default for the category (x axis here).

function works correctly from MATLAB, but not when called from .NET

I'm using MATLAB Builder NE for interoperability to call MATLAB functions from a C# .NET program built as a plug-in for the open source application ClearCanvas. When I run the code normally from the .NET program, I usually (but not always) get the error message
MWMCR::EvaluateFunction error ... Reference to non-existent element of a cell array. Error in => ComputeT1Maps.m at line 178.
The line of MATLAB code in question is as follows:
seriesHeader = currentSlab.Slice{1}.MetaData{1}.Header;
Header is a struct of the form given by MATLAB's dicominfo function and MetaData{n} is a struct that contains the filename and image header struct for the nth image file.
The function signature for the ComputeT1Maps function is:
function ComputeT1Maps(data, options)
To try to figure out this bug I put the following line in at the beginning of the ComputeT1Maps function in order to preserve state so I could see what values were passed to MATLAB from .NET:
save(fullfile('F:\MATLAB\T1Mapping', 'T1_debug.mat'), 'data', 'options', ...
'-mat', '-v7.3');
So, having preserved the inputs to this function (received from the .NET program that called it), I then tried running my ComputeT1Maps function from an interactive MATLAB session after loading in the saved variables, so that I could utilize MATLAB's debugging tools to figure out why I was getting the error. That's when things got really bizarre. The function works just fine from the interactive MATLAB session when given the exact same operands that were given to it when it was called from my .NET program. How can this be? How can the function fail when called from C# .NET, but run properly when given the exact same input in an interactive MATLAB session? Also, this same code used to work before and the error only began to happen after I updated both my local installation of MATLAB and the MCR to the latest version (2011b).
On the .NET side, the data that is passed to MATLAB is constructed by the following function:
public void ExchangeData(MultidimensionalDataCollection mdc, string outputPath, bool generateAncillaryTestImages,
bool excludeAcquisitions, double[] exclusionList, bool showProgressBar, bool displayComputedImages,
bool pauseAtEachSlice, bool softwareDiagnostics, bool displayProgressBar)
{
try
{
int subspaceIndex = 0;
int slabIndex = 0;
int sliceIndex = 0;
int imageIndex = 0;
MWStructArray topLevelGrouping;
MWCellArray geometricSubspaceList;
MWStructArray geometricGrouping;
MWCellArray slabList;
MWStructArray slabGrouping;
MWCellArray sliceList;
MWStructArray sliceGrouping;
MWCellArray imageMetaData;
MWStructArray perImageData;
MWArray[] result;
MWLogicalArray successFlag;
MWStructArray header;
MWCellArray sopInstanceUids;
MWStructArray t1MapOptions;
topLevelGrouping = new MWStructArray(1, 1, new string[] { "GeometricSubspace",
"GeometricSubspaceCount" });
topLevelGrouping["GeometricSubspaceCount", 1] = mdc.Count;
geometricSubspaceList = new MWCellArray(1, mdc.Count);
subspaceIndex = 0;
foreach (GeometricSubspace subspace in mdc)
{
subspaceIndex++;
geometricGrouping = new MWStructArray(1, 1, new string[] { "Slab",
"SlabCount" });
geometricGrouping["SlabCount", 1] = subspace.Count;
slabList = new MWCellArray(1, subspace.Count);
slabIndex = 0;
foreach (Slab slab in subspace)
{
slabIndex++;
slabGrouping = new MWStructArray(1, 1, new string[] { "Slice",
"SliceCount" });
slabGrouping["SliceCount", 1] = slab.Count;
sliceList = new MWCellArray(1, slab.Count);
sliceIndex = 0;
foreach (Slice slice in slab)
{
sliceIndex++;
sliceGrouping = new MWStructArray(1, 1, new string[] {
"ImageCount", "MetaData", "MultidimensionalPixelData",
"SliceLocation", "SopInstanceUids" });
sliceGrouping["ImageCount", 1] = slice.Count;
imageMetaData = new MWCellArray(1, slice.Count);
int rows, columns;
short[,,,] multidimensionalPixelData = null;
imageIndex = 0;
foreach (Image image in slice)
{
imageIndex++;
short[,] imageMatrix = null;
if (!image.ImageSopClass.DicomUid.Equals(DicomUids.MRImageStorage))
throw new NotSupportedException("SopClass " + image.ImageSopClass.Name + " is not supported.");
else
{
DicomUncompressedPixelData rawPixelData = image.PixelData;
imageMatrix = GetCCImageMatrix(rawPixelData, out rows, out columns);
if (imageIndex == 1)
{
multidimensionalPixelData = new short[slice.Count, 1, columns, rows];
}
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < columns; j++)
{
// Remember that C# array indices start with 0 while in MATLAB they start with 1
multidimensionalPixelData[imageIndex - 1, 0, i, j] = imageMatrix[i, j];
}
}
}
perImageData = new MWStructArray(1, 1, new string[] { "FileName", "Header" });
perImageData["FileName", 1] = image.FileName;
result = _mlT1Mapping.QT1GetDicomHeader(2, image.FileName);
if (result == null)
throw new Exception("GetDicomHeader failed to read the header file for filename: " +
image.FileName);
else
{
// MWStructArray
successFlag = (MWLogicalArray)result[0];
bool[] headerObtained = successFlag.ToVector();
if (headerObtained[0])
{
header = (MWStructArray)result[1];
perImageData["Header", 1] = header;
imageMetaData[1, imageIndex] = perImageData;
}
else
{
Console.WriteLine("GetDicomHeader failed to read the header file for filename: " +
image.FileName);
}
}
}
sliceList[1, sliceIndex] = sliceGrouping;
sliceGrouping["MetaData", 1] = imageMetaData;
sliceGrouping["SliceLocation", 1] = slice.SliceLocation;
List<string> theSops = slice._sopList;
sopInstanceUids = new MWCellArray(1, slice._sopList.Count);
int count = 0;
foreach (string sop in theSops)
{
count++;
sopInstanceUids[1, count] = sop;
}
sliceGrouping["SopInstanceUids", 1] = sopInstanceUids;
sliceGrouping["MultidimensionalPixelData", 1] = (MWNumericArray)multidimensionalPixelData;
}
slabList[1, slabIndex] = slabGrouping;
slabGrouping["Slice", 1] = sliceList;
}
geometricSubspaceList[1, subspaceIndex] = geometricGrouping;
geometricGrouping["Slab", 1] = slabList;
}
topLevelGrouping["GeometricSubspace", 1] = geometricSubspaceList;
t1MapOptions = new MWStructArray(1, 1, new string[] { "DirectoryPath",
"ComputeDifferenceImages", "ComputeMultiplicationImages", "DisplayComputedImages", "PauseAtEachSlice",
"SoftwareDiagnostics", "DisplayProgressBar"
});
t1MapOptions["DirectoryPath"] = (MWCharArray)outputPath;
t1MapOptions["SaveS0Maps"] = (MWLogicalArray)generateAncillaryTestImages;
t1MapOptions["ExcludeAcquisitions"] = (MWLogicalArray)excludeAcquisitions;
t1MapOptions["ExclusionList"] = (MWNumericArray)exclusionList;
t1MapOptions["DisplayComputedImages"] = (MWLogicalArray)displayComputedImages;
t1MapOptions["PauseAtEachSlice"] = (MWLogicalArray)pauseAtEachSlice;
t1MapOptions["SoftwareDiagnostics"] = (MWLogicalArray)softwareDiagnostics;
t1MapOptions["DisplayProgressBar"] = (MWLogicalArray)displayProgressBar;
_mlT1Mapping.ComputeT1Maps(topLevelGrouping, t1MapOptions);
}
catch (Exception)
{
throw;
}
}
I'm not yet 100% certain that I've fixed everything, but so far the tests appear to be succeeding after a few changes. It seems that the crux of the problem was that the order of some assignments was transposed. This occurred in a few places. For example, instead of:
sliceList[1, sliceIndex] = sliceGrouping;
sliceGrouping["MetaData", 1] = imageMetaData;
it should have been ordered as:
sliceGrouping["MetaData", 1] = imageMetaData;
sliceList[1, sliceIndex] = sliceGrouping;
The strange thing about this bug was that the code worked just fine in the previous version of MATLAB. It should have never worked at all!

Categories

Resources