diff --git a/Modules/QmitkExt/QmitkHistogramJSWidget.cpp b/Modules/QmitkExt/QmitkHistogramJSWidget.cpp index b2a4a419ff..dcd50b24b1 100644 --- a/Modules/QmitkExt/QmitkHistogramJSWidget.cpp +++ b/Modules/QmitkExt/QmitkHistogramJSWidget.cpp @@ -1,300 +1,301 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "QmitkHistogramJSWidget.h" #include "mitkPixelTypeMultiplex.h" #include #include "mitkRenderingManager.h" #include "mitkBaseRenderer.h" #include "mitkImageTimeSelector.h" #include "mitkExtractSliceFilter.h" QmitkHistogramJSWidget::QmitkHistogramJSWidget(QWidget *parent) : QWebView(parent) { // set histogram type to barchart in first instance m_UseLineGraph = false; // set html from source connect(page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(AddJSObject())); QUrl myUrl = QUrl("qrc:/qmitk/Histogram.html"); setUrl(myUrl); // set Scrollbars to be always disabled page()->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff); page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff); m_ParametricPath = ParametricPathType::New(); } QmitkHistogramJSWidget::~QmitkHistogramJSWidget() { } // adds an Object of Type QmitkHistogramJSWidget to the JavaScript, using QtWebkitBridge void QmitkHistogramJSWidget::AddJSObject() { page()->mainFrame()->addToJavaScriptWindowObject(QString("histogramData"), this); } // reloads WebView, everytime its size has been changed, so the size of the Histogram fits to the size of the widget void QmitkHistogramJSWidget::resizeEvent(QResizeEvent* resizeEvent) { QWebView::resizeEvent(resizeEvent); + page()->mainFrame()->evaluateJavaScript("disconnectSignals()"); this->reload(); } // method to expose data to JavaScript by using properties void QmitkHistogramJSWidget::ComputeHistogram(HistogramType* histogram) { m_Histogram = histogram; HistogramConstIteratorType startIt = m_Histogram->End(); HistogramConstIteratorType endIt = m_Histogram->End(); HistogramConstIteratorType it = m_Histogram->Begin(); ClearData(); unsigned int i = 0; bool firstValue = false; // removes frequencies of 0, which are outside the first and last bin for (; it != m_Histogram->End(); ++it) { if (it.GetFrequency() > 0.0) { endIt = it; if (!firstValue) { firstValue = true; startIt = it; } } } ++endIt; // generating Lists of measurement and frequencies for (it = startIt ; it != endIt; ++it, ++i) { QVariant frequency = QVariant::fromValue(it.GetFrequency()); QVariant measurement = it.GetMeasurementVector()[0]; m_Frequency.insert(i, frequency); m_Measurement.insert(i, measurement); } m_IntensityProfile = false; this->SignalDataChanged(); } void QmitkHistogramJSWidget::ClearData() { m_Frequency.clear(); m_Measurement.clear(); } void QmitkHistogramJSWidget::ClearHistogram() { this->ClearData(); this->SignalDataChanged(); } QList QmitkHistogramJSWidget::GetFrequency() { return m_Frequency; } QList QmitkHistogramJSWidget::GetMeasurement() { return m_Measurement; } bool QmitkHistogramJSWidget::GetUseLineGraph() { return m_UseLineGraph; } void QmitkHistogramJSWidget::OnBarRadioButtonSelected() { if (m_UseLineGraph) { m_UseLineGraph = false; this->SignalGraphChanged(); } } void QmitkHistogramJSWidget::OnLineRadioButtonSelected() { if (!m_UseLineGraph) { m_UseLineGraph = true; this->SignalGraphChanged(); } } void QmitkHistogramJSWidget::SetImage(mitk::Image* image) { m_Image = image; } void QmitkHistogramJSWidget::SetPlanarFigure(const mitk::PlanarFigure* planarFigure) { m_PlanarFigure = planarFigure; } template void ReadPixel(mitk::PixelType ptype, mitk::Image::Pointer image, mitk::Index3D indexPoint, double& value) { if (image->GetDimension() == 2) { mitk::ImagePixelReadAccessor readAccess(image, image->GetSliceData(0)); itk::Index<2> idx; idx[0] = indexPoint[0]; idx[1] = indexPoint[1]; value = readAccess.GetPixelByIndex(idx); } else if (image->GetDimension() == 3) { mitk::ImagePixelReadAccessor readAccess(image, image->GetVolumeData(0)); itk::Index<3> idx; idx[0] = indexPoint[0]; idx[1] = indexPoint[1]; idx[2] = indexPoint[2]; value = readAccess.GetPixelByIndex(idx); } else { //unhandled } } void QmitkHistogramJSWidget::ComputeIntensityProfile(unsigned int timeStep) { this->ClearData(); m_ParametricPath->Initialize(); if (m_PlanarFigure.IsNull()) { mitkThrow() << "PlanarFigure not set!"; } if (m_Image.IsNull()) { mitkThrow() << "Image not set!"; } // Get 2D geometry frame of PlanarFigure mitk::Geometry2D* planarFigureGeometry2D = dynamic_cast(m_PlanarFigure->GetGeometry(0)); if (planarFigureGeometry2D == NULL) { mitkThrow() << "PlanarFigure has no valid geometry!"; } // Get 3D geometry from Image (needed for conversion of point to index) mitk::Geometry3D* imageGeometry = m_Image->GetGeometry(0); if (imageGeometry == NULL) { mitkThrow() << "Image has no valid geometry!"; } // Get first poly-line of PlanarFigure (other possible poly-lines in PlanarFigure // are not supported) const VertexContainerType vertexContainer = m_PlanarFigure->GetPolyLine(0); VertexContainerType::const_iterator it; for (it = vertexContainer.begin(); it != vertexContainer.end(); ++it) { // Map PlanarFigure 2D point to 3D point mitk::Point3D point3D; planarFigureGeometry2D->Map(it->Point, point3D); // Convert world to index coordinates mitk::Point3D indexPoint3D; imageGeometry->WorldToIndex(point3D, indexPoint3D); ParametricPathType::OutputType index; index[0] = indexPoint3D[0]; index[1] = indexPoint3D[1]; index[2] = indexPoint3D[2]; // Add index to parametric path m_ParametricPath->AddVertex(index); } m_DerivedPath = m_ParametricPath; if (m_DerivedPath.IsNull()) { mitkThrow() << "No path set!"; } // Fill item model with line profile data double distance = 0.0; mitk::Point3D currentWorldPoint; double t; unsigned int i = 0; for (i = 0, t = m_DerivedPath->StartOfInput(); ;++i) { const PathType::OutputType &continousIndex = m_DerivedPath->Evaluate(t); mitk::Point3D worldPoint; imageGeometry->IndexToWorld(continousIndex, worldPoint); if (i == 0) { currentWorldPoint = worldPoint; } distance += currentWorldPoint.EuclideanDistanceTo(worldPoint); mitk::Index3D indexPoint; imageGeometry->WorldToIndex(worldPoint, indexPoint); const mitk::PixelType ptype = m_Image->GetPixelType(); double intensity = 0.0; if (m_Image->GetDimension() == 4) { mitk::ImageTimeSelector::Pointer timeSelector = mitk::ImageTimeSelector::New(); timeSelector->SetInput(m_Image); timeSelector->SetTimeNr(timeStep); timeSelector->Update(); mitk::Image::Pointer image = timeSelector->GetOutput(); mitkPixelTypeMultiplex3( ReadPixel, ptype, image, indexPoint, intensity); } else { mitkPixelTypeMultiplex3( ReadPixel, ptype, m_Image, indexPoint, intensity); } m_Measurement.insert(i, distance); m_Frequency.insert(i, intensity); // Go to next index; when iteration offset reaches zero, iteration is finished PathType::OffsetType offset = m_DerivedPath->IncrementInput(t); if (!(offset[0] || offset[1] || offset[2])) { break; } currentWorldPoint = worldPoint; } m_IntensityProfile = true; m_UseLineGraph = true; this->SignalDataChanged(); } bool QmitkHistogramJSWidget::GetIntensityProfile() { return m_IntensityProfile; } diff --git a/Modules/QmitkExt/resources/Histogram.js b/Modules/QmitkExt/resources/Histogram.js index 2e73055140..b939c22873 100644 --- a/Modules/QmitkExt/resources/Histogram.js +++ b/Modules/QmitkExt/resources/Histogram.js @@ -1,539 +1,546 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ var margin = { top : 10, bottom : 50, left : 45, right : 20, }; var height = histogramData.height - margin.top - margin.bottom; var width = histogramData.width - margin.left - margin.right; var tension = 0.8; var connected = false; var dur = 1000; var binSize = 10; var min; var max; /* * Connecting signals from qt side with JavaScript methods. */ if (!connected) { connected = true; histogramData.SignalDataChanged.connect(updateHistogram); histogramData.SignalGraphChanged.connect(updateHistogram); } +function disconnectSignals() +{ + histogramData.SignalDataChanged.disconnect(updateHistogram); + histogramData.SignalGraphChanged.disconnect(updateHistogram); + delete histogramData; +} + /* * Predefinition of scales. */ var xScale = d3.scale.linear() .domain([d3.min(histogramData.measurement)-binSize/2,d3.max(histogramData.measurement)+binSize/2]) .range([0,width]); var yScale = d3.scale.linear() .domain([d3.min(histogramData.frequency),d3.max(histogramData.frequency)]) .range([height,margin.top]); /* * Predefinition of axis elements. */ var xAxis = d3.svg.axis() .scale(xScale) .orient("bottom") .tickFormat(d3.format("s")); var yAxis = d3.svg.axis() .scale(yScale) .orient("left") .tickFormat(d3.format("s")); /* * Predefinition of the zoom. */ var zoombie = d3.behavior.zoom().x(xScale).scaleExtent([1, 50]).on("zoom", zoom); /* * Creation of the svg element, which holds the complete histogram. */ var svg = d3.select("body") .append("svg") .attr("class", "svg") .attr("width", width + margin.right + margin.left) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate (" + margin.left + "," + margin.top + ")") .call(zoombie) .on("mousemove", myMouseMove); /* * Appending a rectangle to the svg, to guarantee the possibility * of zooming on the whole histogram. */ svg.append("rect") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .attr("opacity", 0); /* * Appending a second svg to main svg, which holds only the graph. */ var vis = svg.append("svg") .attr("width", width) .attr("height", height); /* * Predefinition of the lines. */ var line = d3.svg.line() .interpolate("linear") .x(function(d,i) { return xScale(histogramData.measurement[i]-binSize/2); }) .y(function(d) { return yScale(d); }); var linenull = d3.svg.line() .interpolate("linear") .x(function(d,i) { return xScale(histogramData.measurement[i]-binSize/2); }) .y(function(d) { return yScale(0); }); updateHistogram(); /* * Method to update the histogram data * and to change the displayed graph. */ function updateHistogram() { calcBinSize(); if (!histogramData.useLineGraph) { barChart(); } else if (histogramData.useLineGraph) { linePlot() } } /* * Calculation of the bin size. */ function calcBinSize() { min = d3.min(histogramData.measurement); max = d3.max(histogramData.measurement); binSize = ((max - min) / (histogramData.measurement.length)); } /* * Method to display histogram as a barchart. */ function barChart() { definition(); /* * Change zoom to a fixed y-axis. */ zoombie = d3.behavior.zoom().x(xScale).scaleExtent([1, 50]).on("zoom", zoom); svg.call(zoombie); /* * Element to animate transition from linegraph to barchart. */ vis.selectAll("path.line").remove(); vis.selectAll("circle").remove(); /* * Definition of the bar elements. */ var bar = vis.selectAll("rect.bar").data(histogramData.frequency); /* * Definition how to handle new bar elements. */ bar.enter().append("rect") .attr("class", "bar") .on("mouseover", myMouseOver) .on("mouseout", myMouseOut) .attr("x", function(d,i) { return xScale(histogramData.measurement[i]-binSize/2); }) .attr("y", height) .attr("height", 0) .attr("width", barWidth) /* * Definition how to handle changed bar elements. */ bar.transition() .duration(dur) .attr("x", function(d,i) { return xScale(histogramData.measurement[i]-binSize/2); }) .attr("y", myYPostion) .attr("height", barHeight) .attr("width", barWidth); /* * Definition how to handle bar elements which doesn't exist anymore.' */ bar.exit() .transition() .duration(dur) .attr("y", height) .attr("height", 0) .remove(); /* * Update of axis elements. * First delete old ones, then generate new. */ svg.selectAll("g") .remove(); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis); } /* * Method to display histogram as a linegraph. */ function linePlot() { definition(); /* * Change zoom to a zoomable y-axis. */ zoombie = d3.behavior.zoom().x(xScale).y(yScale).scaleExtent([1, 50]).on("zoom", zoom); svg.call(zoombie); /* * Elements to animate transitions from barchart to linegraph. * Different transition when an intesity profile is generated. */ if(!histogramData.intensityProfile) { vis.selectAll("rect.bar") .transition() .duration(dur) .attr("height", 0) .remove(); } else { vis.selectAll("rect.bar") .transition() .duration(dur) .attr("y", height) // <-- .attr("height", 0) .remove(); } /* * Creating circle elements, when an intensity profile is generated to show tooltips. * Due performance losses tooltips are not supported for line histograms. */ if(histogramData.intensityProfile) { var circles = vis.selectAll("circle").data(histogramData.frequency); /* * Definition how to handle new circle elements. */ circles.enter() .append("circle") .on("mouseover", myMouseOverLine) .on("mouseout", myMouseOutLine) .attr("cx", function(d,i) { return xScale(histogramData.measurement[i]-binSize/2); }) .attr("cy", function (d) { return yScale(d) }) .attr("r", 5) .attr("opacity", 0) .style("stroke", "red") .style("stroke-width", 1) .style("fill-opacity", 0); /* * Definition how to handle bar elements which doesn't exist anymore. */ circles.exit().remove(); } else { /* * Removing of all circle elements if a line histogram is generated. */ vis.selectAll("circle").remove(); } /* * Creating a new path element. */ var graph = vis.selectAll("path.line") .data([histogramData.frequency]); /* * Definition how to handle a new path element, using predefined lines. */ graph.enter() .append("path") .attr("class", "line") .transition() .duration(dur) .attr("d", line); /* * Definition how to handle change points in an existing path element. */ graph.transition() .duration(dur) .attr("d", line); /* * Update of axis elements. * First delete old ones, then generate new. */ svg.selectAll("g") .remove(); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis); } function definition() { /* * Match scale to current data. */ xScale = d3.scale.linear() .domain([d3.min(histogramData.measurement)-binSize/2,d3.max(histogramData.measurement)+binSize/2]) .range([0,width]); yScale = d3.scale.linear() .domain([d3.min(histogramData.frequency),d3.max(histogramData.frequency)]) .range([height,margin.top]); /* * Match axes to current scale */ xAxis = d3.svg.axis() .scale(xScale) .orient("bottom") .tickFormat(d3.format("s")); yAxis = d3.svg.axis() .scale(yScale) .orient("left") .tickFormat(d3.format("s")); } /* * Method to calculate barwidth in px. */ function barWidth(d, i) { var bw; if (i != (histogramData.measurement.length-1)) { bw =(xScale(histogramData.measurement[i + 1]) - xScale(histogramData.measurement[i])) * (histogramData.frequency.length / (histogramData.frequency.length + 1)) - 1; } else { bw =(xScale(histogramData.measurement[i]) - xScale(histogramData.measurement[i - 1])) * (histogramData.frequency.length / (histogramData.frequency.length + 1)) - 1; } /* * Ensure barwidth is not smaller than 1px. */ bw = bw > 1 ? bw : 1; return bw; } /* * Method to calculate barheight in px. * Ensure barheight is not smaller than 1px. */ function barHeight(d) { var bh; bh = height - yScale(d); bh = bh >=2 ? bh : 2; return bh; } /* * Method to calculate dynamical y positions. */ function myYPostion(d) { var myy = yScale(d); myy = (height-myy) > 2 ? myy : (height-2); if (d == 0) { return height; } return myy; } /* * Method to fit parameters of bars/line when zoomed. * Update axes elements. * Resets the view if scale is 1. */ function zoom() { if (zoombie.scale() == 1) { zoombie.translate([0,0]); xScale.domain([d3.min(histogramData.measurement)-binSize/2,d3.max(histogramData.measurement)+binSize/2]); yScale.domain([d3.min(histogramData.frequency),d3.max(histogramData.frequency)]); } if (!histogramData.useLineGraph) { svg.select(".x.axis").call(xAxis); vis.selectAll(".bar") .attr("width", barWidth) .attr("x", function(d, i) { return xScale(histogramData.measurement[i]-binSize/2); }); } else { svg.select(".x.axis").call(xAxis); svg.select(".y.axis").call(yAxis); vis.selectAll("path.line") .attr("transform", "translate(" + zoombie.translate() + ")scale(" + zoombie.scale() + ")") .style("stroke-width", 1 / zoombie.scale()); vis.selectAll("circle") .attr("cx", function(d, i) { return xScale(histogramData.measurement[i]-binSize/2); }) .attr("cy", function(d) { return yScale(d); }); } } /* * Method to show infobox, while mouse is over a bin. */ function myMouseOver() { var myBar = d3.select(this); var reScale = d3.scale.linear() .domain(xScale.range()) .range(xScale.domain()); var y = myBar.data(); var x = reScale(myBar.attr("x")); myBar.style("fill", "red"); d3.select(".infobox").style("display", "block"); if ((min >= 0) && (max <= 2)) //tooltip for float images { d3.select(".measurement").text("Greayvalue: " + (Math.round(x*1000)/1000)); } else { d3.select(".measurement").text("Greyvalue: " + (Math.round(x*10)/10) + " ... " + (Math.round((x+binSize)*10)/10)); } d3.select(".frequency").text("Frequency: " + y); } /* * Hide infobox, when mouse not over a bin. */ function myMouseOut() { var myBar = d3.select(this); myBar.style("fill", d3.rgb(0,71,185)); d3.select(".infobox").style("display", "none"); } /* * Show tooltip, while mouse is over a circle in an intesity profile. */ function myMouseOverLine() { var myCircle = d3.select(this) var reScale = d3.scale.linear() .domain(xScale.range()) .range(xScale.domain()); var y = myCircle.data(); var x = reScale(myCircle.attr("cx")); x = x >= 0 ? x : 0; myCircle.attr("opacity", 1); d3.select(".infobox").style("display", "block"); d3.select(".measurement").text("Distance: " + (Math.round(x*100)/100) + " mm"); d3.select(".frequency").text("Intesity: " + y); } /* * Hide infobox, when mouse not over a circle. */ function myMouseOutLine() { var myCircle = d3.select(this); myCircle.attr("opacity", 0); d3.select(".infobox").style("display", "none"); } /* * Update mousecoordinates when mouse is moved. * Tooltip is shown on the right side of the mouse until it reaches the right boundary, * the it switches to the left side. */ function myMouseMove() { var infobox = d3.select(".infobox"); var coords = d3.mouse(this); if ((coords[0]+120)<(width-margin.right)) { infobox.style("left", coords[0] + 75 + "px"); infobox.style("top", coords[1] + "px"); } else { infobox.style("left", coords[0] - 90 + "px"); infobox.style("top",coords[1] + "px"); } }