diff --git a/src/Cxx/Visualization/CurvatureBandsWithGlyphs.cxx b/src/Cxx/Visualization/CurvatureBandsWithGlyphs.cxx index c4d18fe0d4913ffae5ff2bd3a8c27155d6b9140a..6f29ad65212252820d2006bd8d95dd9cca3383fa 100644 --- a/src/Cxx/Visualization/CurvatureBandsWithGlyphs.cxx +++ b/src/Cxx/Visualization/CurvatureBandsWithGlyphs.cxx @@ -2,7 +2,6 @@ #include <vtkArrowSource.h> #include <vtkBandedPolyDataContourFilter.h> #include <vtkCamera.h> -#include <vtkCameraOrientationWidget.h> #include <vtkCleanPolyData.h> #include <vtkColorSeries.h> #include <vtkColorTransferFunction.h> @@ -45,6 +44,11 @@ #include <vtkVariantArray.h> #include <vtkVersion.h> +#if VTK_VERSION_NUMBER >= 90020210809ULL +#define HAS_COW +#include <vtkCameraOrientationWidget.h> +#endif + #include <algorithm> #include <array> #include <cctype> @@ -191,20 +195,25 @@ void PrintBandsFrequencies(std::map<int, std::vector<double>> const& bands, int main(int, char*[]) { // Get the surface - //std::string desiredSurface = "Hills"; - //std::string desiredSurface = "ParametricTorus"; - //std::string desiredSurface = "Plane"; - std::string desiredSurface = "RandomHills"; - //std::string desiredSurface = "Sphere"; - //std::string desiredSurface = "Torus"; + std::string desiredSurface{"RandomHills"}; + // desiredSurface = "Hills"; + // desiredSurface = "ParametricTorus"; + // desiredSurface = "Plane"; + // desiredSurface = "RandomHills"; + // desiredSurface = "Sphere"; + // desiredSurface = "Torus"; auto source = GetSource(desiredSurface); // The length of the normal arrow glyphs. auto scaleFactor = 1.0; if (desiredSurface == "Hills") - scaleFactor = 0.5; + { + scaleFactor = 0.5; + } if (desiredSurface == "Sphere") + { scaleFactor = 2.0; + } std::cout << desiredSurface << std::endl; auto gaussianCurvature = true; @@ -270,19 +279,21 @@ int main(int, char*[]) lut->SetTableRange(scalarRangeCurvatures); lut1->SetTableRange(scalarRangeElevation); auto numberOfBands = lut->GetNumberOfTableValues(); - auto bands = GetBands(scalarRangeCurvatures, numberOfBands, 10); + auto precision = 10; + auto bands = GetBands(scalarRangeCurvatures, numberOfBands, precision, false); + if (desiredSurface == "RandomHills") { // These are my custom bands. // Generated by first running: - // bands = GetBands(scalarRangeCurvatures, numberOfBands, false); + // bands = GetBands(scalarRangeCurvatures, numberOfBands, precision, false); // then: // std::vector<int> freq = Frequencies(bands, src); // PrintBandsFrequencies(bands, freq); // Finally using the output to create this table: // std::vector<std::array<double, 2>> myBands = { // {-0.630, -0.190}, {-0.190, -0.043}, {-0.043, -0.0136}, - // {-0.0136, 0.0158}, {0.0158, 0.0452}, {0.0452, 0.0746}, + // {-0.0136, 0.0158}, {0.0158, 0.0452}, {0.0452, 0.0746},12 // {0.0746, 0.104}, {0.104, 0.251}, {0.251, 1.131}}; // This demonstrates that the gaussian curvature of the surface // is mostly planar with some hyperbolic regions (saddle points) @@ -466,10 +477,12 @@ int main(int, char*[]) renWin->AddRenderer(ren); // Important: The interactor must be set prior to enabling the widget. iren->SetRenderWindow(renWin); +#ifdef HAS_COW vtkNew<vtkCameraOrientationWidget> camOrientManipulator; camOrientManipulator->SetParentRenderer(ren); // Enable the widget. camOrientManipulator->On(); +#endif // add actors ren->AddViewProp(srcActor); diff --git a/src/Cxx/Visualization/ElevationBandsWithGlyphs.cxx b/src/Cxx/Visualization/ElevationBandsWithGlyphs.cxx index 82d040ea21ce1962651f260288ed9bd5f3da0624..0e465eb893588e12859cee750edafb483e0a79dd 100644 --- a/src/Cxx/Visualization/ElevationBandsWithGlyphs.cxx +++ b/src/Cxx/Visualization/ElevationBandsWithGlyphs.cxx @@ -2,11 +2,13 @@ #include <vtkArrowSource.h> #include <vtkBandedPolyDataContourFilter.h> #include <vtkCamera.h> -#include <vtkCameraOrientationWidget.h> #include <vtkCleanPolyData.h> #include <vtkColorSeries.h> #include <vtkColorTransferFunction.h> +#include <vtkDelaunay2D.h> +#include <vtkDoubleArray.h> #include <vtkElevationFilter.h> +#include <vtkFloatArray.h> #include <vtkGlyph3D.h> #include <vtkInteractorStyleTrackballCamera.h> #include <vtkLookupTable.h> @@ -20,19 +22,28 @@ #include <vtkPointData.h> #include <vtkPolyData.h> #include <vtkPolyDataMapper.h> +#include <vtkPolyDataNormals.h> #include <vtkProperty.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> #include <vtkRenderer.h> #include <vtkReverseSense.h> #include <vtkScalarBarActor.h> +#include <vtkSmartPointer.h> #include <vtkSphereSource.h> #include <vtkSuperquadricSource.h> #include <vtkTextProperty.h> #include <vtkTransform.h> #include <vtkTransformPolyDataFilter.h> #include <vtkTriangleFilter.h> +#include <vtkVariant.h> #include <vtkVariantArray.h> +#include <vtkVersion.h> + +#if VTK_VERSION_NUMBER >= 90020210809ULL +#define HAS_COW +#include <vtkCameraOrientationWidget.h> +#endif #include <algorithm> #include <array> @@ -44,23 +55,52 @@ #include <iomanip> #include <iostream> #include <iterator> +#include <numeric> +#include <set> #include <sstream> #include <string> #include <vector> namespace { +//! Generate elevations over the surface. +/*! +@param src - the vtkPolyData source. +@return elev - the elevations. +*/ +vtkSmartPointer<vtkPolyData> GetElevations(vtkPolyData* src); -//! Divide a range into bands +vtkSmartPointer<vtkPolyData> GetHills(); +vtkSmartPointer<vtkPolyData> GetParametricHills(); +vtkSmartPointer<vtkPolyData> GetParametricTorus(); +vtkSmartPointer<vtkPolyData> GetPlane(); +vtkSmartPointer<vtkPolyData> GetSphere(); +vtkSmartPointer<vtkPolyData> GetTorus(); +vtkSmartPointer<vtkPolyData> GetSource(std::string const& source); + +vtkSmartPointer<vtkColorSeries> GetColorSeries(); + +vtkSmartPointer<vtkLookupTable> GetCategoricalLUT(); + +vtkSmartPointer<vtkLookupTable> GetOrdinaLUT(); + +vtkSmartPointer<vtkLookupTable> GetDivergingLUT(); + +vtkSmartPointer<vtkLookupTable> ReverseLUT(vtkLookupTable* lut); + +//! Glyph the normals on the surface. /*! -@param dR - [min, max] the range that is to be covered by the bands. -@param numberOfBands - the number of bands, a positive integer. -@param nearestInteger - if True then [floor(min), ceil(max)] is used. -@return A map consisting of the band inxex and [min, midpoint, max] for each -band. +@param src - the vtkPolyData source. +#param scaleFactor - the scale factor for the glyphs. +@param reverseNormals - if True the normals on the surface are reversed. +@return The glyphs. */ -std::map<int, std::vector<double>> MakeBands(double const dR[2], - int const& numberOfBands, - bool const& nearestInteger); +vtkNew<vtkGlyph3D> GetGlyphs(vtkPolyData* src, double const& scaleFactor = 1.0, + bool const& reverseNormals = false); + +std::map<int, std::vector<double>> GetBands(double const dR[2], + int const& numberOfBands, + int const& precision = 2, + bool const& nearestInteger = false); //! Divide a range into custom bands /*! @@ -76,8 +116,8 @@ like this: [[r1, r2], [r2, r3], [r3, r4]...] band. */ std::map<int, std::vector<double>> -MakeCustomBands(double const dR[2], int const& numberOfBands, - std::vector<std::array<double, 2>> const& myBands); +GetCustomBands(double const dR[2], int const& numberOfBands, + std::vector<std::array<double, 2>> const& myBands); //! Divide a range into integral bands /*! @@ -86,155 +126,76 @@ Divide a range into bands @returnA map consisting of the band inxex and [min, midpoint, max] for each band. */ -std::map<int, std::vector<double>> MakeIntegralBands(double const dR[2]); - -//! Generate elevations over the surface. -/*! -@param src - the vtkPolyData source. -@return elev - the elevations. -*/ -vtkSmartPointer<vtkPolyData> MakeElevations(vtkPolyData* src); - -//! Make a parametric hills surface as the source. -/*! -@return - vtkPolyData. -*/ -vtkSmartPointer<vtkPolyData> MakeParametricHills(); - -//! Make a parametric torus as the source. -/*! -@return - vtkPolyData. -*/ -vtkSmartPointer<vtkPolyData> MakeParametricTorus(); - -//! Make a Plane as the source. -/*! -@return - vtkPolyData. -*/ -vtkSmartPointer<vtkPolyData> MakePlane(); - -//! Make a MakeSphere as the source. -/*! -@return - vtkPolyData. -*/ -vtkSmartPointer<vtkPolyData> MakeSphere(); - -//! Make a torus as the source. -/*! -@return - vtkPolyData. -*/ -vtkSmartPointer<vtkPolyData> MakeTorus(); - -vtkSmartPointer<vtkColorSeries> GetColorSeries(); - -vtkSmartPointer<vtkLookupTable> MakeCategoricalLUT(); - -vtkSmartPointer<vtkLookupTable> MakeOrdinaLUT(); - -vtkSmartPointer<vtkLookupTable> MakeDivergingLUT(); - -vtkSmartPointer<vtkLookupTable> ReverseLUT(vtkLookupTable* lut); +std::map<int, std::vector<double>> GetIntegralBands(double const dR[2]); //! Count the number of scalars in each band. -/*! -@param bands - the bands. -@param src - the vtkPolyData source. -@return The frequencies of the scalars in each band. -*/ -std::map<int, int> Frequencies(std::map<int, std::vector<double>>& bands, - vtkPolyData* src); - -std::pair<int, int> AdjustFrequencyRanges(std::map<int, int>& freq); - -void PrintBands(std::map<int, std::vector<double>> const& bands); - -void PrintFrequencies(std::map<int, int> const& freq); - -void PrintBandsFrequencies(std::map<int, std::vector<double>> const& bands, +/* + * The scalars used are the active scalars in the polydata. + * + * @param bands - the bands. + * @param src - the vtkPolyData source. + * @return The frequencies of the scalars in each band. + */ +std::map<int, int> GetFrequencies(std::map<int, std::vector<double>>& bands, + vtkPolyData* src); +//! +/* + * The bands and frequencies are adjusted so that the first and last + * frequencies in the range are non-zero. + * @param bands: The bands. + * @param freq: The frequencies. + */ +void AdjustFrequencyRanges(std::map<int, std::vector<double>>& bands, std::map<int, int>& freq); -//! Glyph the normals on the surface. -/*! -@param src - the vtkPolyData source. -@param reverseNormals - if True the normals on the surface are reversed. -@return The glyphs. -*/ -vtkNew<vtkGlyph3D> MakeGlyphs(vtkPolyData* src, bool const& reverseNormals); +void PrintBandsFrequencies(std::map<int, std::vector<double>> const& bands, + std::map<int, int>& freq, int const& precision = 2); } // namespace -//! Make and display the surface. int main(int, char*[]) { // Get the surface - // std::string desiredSurface = "ParametricTorus"; - // std::string desiredSurface = "Plane"; - std::string desiredSurface = "RandomHills"; - // std::string desiredSurface = "Sphere"; - // std::string desiredSurface = "Torus"; - auto lcSurface = desiredSurface; - std::transform(lcSurface.begin(), lcSurface.end(), lcSurface.begin(), - [](char c) { return std::tolower(c); }); - std::map<std::string, int> availableSurfaces = {{"parametrictorus", 0}, - {"plane", 1}, - {"randomhills", 2}, - {"sphere", 3}, - {"torus", 4}}; - vtkSmartPointer<vtkPolyData> src; - if (availableSurfaces.find(lcSurface) == availableSurfaces.end()) - { - std::cout << "No surface specified." << std::endl; - return EXIT_FAILURE; - } - switch (availableSurfaces[lcSurface]) - { - case 0: { - src = MakeParametricTorus(); - break; - } - case 1: { - src = MakePlane(); - src = MakeElevations(src); - break; - } - case 2: { - src = MakeParametricHills(); - break; - } - case 3: { - src = MakeSphere(); - src = MakeElevations(src); - break; - } - case 4: { - src = MakeTorus(); - src = MakeElevations(src); - break; - } - default: { - std::cout << "No surface specified." << std::endl; - return EXIT_FAILURE; + std::string desiredSurface{"RandomHills"}; + // desiredSurface = "Hills"; + // desiredSurface = "ParametricTorus"; + // desiredSurface = "Plane"; + // desiredSurface = "RandomHills"; + // desiredSurface = "Sphere"; + // desiredSurface = "Torus"; + auto source = GetSource(desiredSurface); + + // The length of the normal arrow glyphs. + auto scaleFactor = 1.0; + if (desiredSurface == "Hills") + { + scaleFactor = 0.5; } + if (desiredSurface == "Sphere") + { + scaleFactor = 2.0; } std::cout << desiredSurface << std::endl; - src->GetPointData()->SetActiveScalars("Elevation"); - auto scalarRange = src->GetPointData()->GetScalars("Elevation")->GetRange(); + source->GetPointData()->SetActiveScalars("Elevation"); + auto scalarRange = + source->GetPointData()->GetScalars("Elevation")->GetRange(); - auto lut = MakeCategoricalLUT(); - auto lut1 = MakeOrdinaLUT(); + auto lut = GetCategoricalLUT(); + auto lut1 = GetOrdinaLUT(); lut->SetTableRange(scalarRange); lut1->SetTableRange(scalarRange); vtkIdType numberOfBands = lut->GetNumberOfTableValues(); - std::map<int, std::vector<double>> bands; + auto precision = 10; + auto bands = GetBands(scalarRange, numberOfBands, precision, false); if (desiredSurface == "RandomHills") { // These are my custom bands. // Generated by first running: - // bands = MakeBands(scalarRange, numberOfBands, false); + // bands = GetBands(scalarRange, numberOfBands, precision, false); // then: - // std::vector<int> freq = Frequencies(bands, src); + // std::vector<int> freq = Frequencies(bands, source); // PrintBandsFrequencies(bands, freq); // Finally using the output to create this table: std::vector<std::array<double, 2>> myBands = { @@ -242,72 +203,27 @@ int main(int, char*[]) {4.0, 5.0}, {5.0, 6.0}, {6.0, 7.0}, {7.0, 8.0}}; // Comment this out if you want to see how allocating // equally spaced bands works. - bands = MakeCustomBands(scalarRange, numberOfBands, myBands); - // bands = MakeBands(scalarRange, numberOfBands, false); + bands = GetCustomBands(scalarRange, numberOfBands, myBands); + // bands = GetBands(scalarRange, numberOfBands, precision, false); // Adjust the number of table values - lut->SetNumberOfTableValues(static_cast<vtkIdType>(bands.size())); - lut1->SetNumberOfTableValues(static_cast<vtkIdType>(bands.size())); - } - else - { - bands = MakeBands(scalarRange, numberOfBands, false); } + lut->SetNumberOfTableValues(static_cast<vtkIdType>(bands.size())); + lut1->SetNumberOfTableValues(static_cast<vtkIdType>(bands.size())); // Let's do a frequency table. - // The number of scalars in each band. - std::map<int, int> freq = Frequencies(bands, src); - - std::vector<int> keys; - for (int k = 0; k < freq.size(); ++k) - { - keys.push_back(k); - } - auto minKey = std::min_element(keys.begin(), keys.end()); - auto maxKey = std::max_element(keys.begin(), keys.end()); - if (lcSurface == "sphere") - { - freq[0] = 0; - } - auto freqRange = AdjustFrequencyRanges(freq); - std::map<int, int>::iterator freqItr; - freqItr = freq.find(freqRange.first); - freq.erase(freq.begin(), freqItr); - freqItr = ++freq.find(freqRange.second); - freq.erase(freqItr, freq.end()); - std::map<int, std::vector<double>>::iterator bandItr; - bandItr = bands.find(freqRange.first); - bands.erase(bands.begin(), bandItr); - bandItr = ++bands.find(freqRange.second); - bands.erase(bandItr, bands.end()); - // Reindex freq and bands. - std::map<int, int> adjFreq; - int idx = 0; - for (auto p : freq) - { - adjFreq[idx] = p.second; - ++idx; - } - std::map<int, std::vector<double>> adjBands; - idx = 0; - for (auto p : bands) - { - adjBands[idx] = p.second; - ++idx; - } - // PrintBandsFrequencies(bands, freq); - PrintBandsFrequencies(adjBands, adjFreq); - freq.erase(freq.begin(), freq.end()); - bands.erase(bands.begin(), bands.end()); + auto freq = GetFrequencies(bands, source); + AdjustFrequencyRanges(bands, freq); + PrintBandsFrequencies(bands, freq); - scalarRange[0] = adjBands.begin()->second[0]; - scalarRange[1] = std::prev(adjBands.end())->second[2]; + scalarRange[0] = bands.begin()->second[0]; + scalarRange[1] = std::prev(bands.end())->second[2]; lut->SetTableRange(scalarRange); - lut->SetNumberOfTableValues(adjBands.size()); + lut->SetNumberOfTableValues(bands.size()); // We will use the midpoint of the band as the label. std::vector<std::string> labels; - for (std::map<int, std::vector<double>>::const_iterator p = adjBands.begin(); - p != adjBands.end(); ++p) + for (std::map<int, std::vector<double>>::const_iterator p = bands.begin(); + p != bands.end(); ++p) { std::ostringstream os; os << std::fixed << std::setw(6) << std::setprecision(2) << p->second[1]; @@ -330,23 +246,21 @@ int main(int, char*[]) // Create the contour bands. vtkNew<vtkBandedPolyDataContourFilter> bcf; - bcf->SetInputData(src); + bcf->SetInputData(source); // Use either the minimum or maximum value for each band. int i = 0; - for (std::map<int, std::vector<double>>::const_iterator p = adjBands.begin(); - p != adjBands.end(); ++p) + for (std::map<int, std::vector<double>>::const_iterator p = bands.begin(); + p != bands.end(); ++p) { bcf->SetValue(i, p->second[2]); ++i; } - // We will use an indexed lookup table. bcf->SetScalarModeToIndex(); bcf->GenerateContourEdgesOn(); // Generate the glyphs on the original surface. - - auto glyph = MakeGlyphs(src, false); + auto glyph = GetGlyphs(source, scaleFactor, false); // ------------------------------------------------------------ // Create the mappers and actors @@ -423,10 +337,12 @@ int main(int, char*[]) renWin->AddRenderer(ren); // Important: The interactor must be set prior to enabling the widget. iren->SetRenderWindow(renWin); +#ifdef HAS_COW vtkNew<vtkCameraOrientationWidget> camOrientManipulator; camOrientManipulator->SetParentRenderer(ren); // Enable the widget. camOrientManipulator->On(); +#endif // add actors ren->AddViewProp(srcActor); @@ -455,126 +371,123 @@ int main(int, char*[]) } namespace { - -std::map<int, std::vector<double>> MakeBands(double const dR[2], - int const& numberOfBands, - bool const& nearestInteger) +vtkSmartPointer<vtkPolyData> GetElevations(vtkPolyData* src) { - std::map<int, std::vector<double>> bands; - if ((dR[1] < dR[0]) || (numberOfBands <= 0)) - { - return bands; - } - double x[2]; - for (int i = 0; i < 2; ++i) - { - x[i] = dR[i]; - } - if (nearestInteger) - { - x[0] = std::floor(x[0]); - x[1] = std::ceil(x[1]); - } - double dx = (x[1] - x[0]) / static_cast<double>(numberOfBands); - std::vector<double> b; - b.push_back(x[0]); - b.push_back(x[0] + dx / 2.0); - b.push_back(x[0] + dx); - for (int i = 0; i < numberOfBands; ++i) + double bounds[6] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + src->GetBounds(bounds); + if (std::abs(bounds[2]) < 1.0e-8 && std::abs(bounds[3]) < 1.0e-8) { - bands[i] = b; - for (std::vector<double>::iterator p = b.begin(); p != b.end(); ++p) - { - *p = *p + dx; - } + bounds[3] = bounds[2] + 1; } - return bands; + vtkNew<vtkElevationFilter> elevFilter; + elevFilter->SetInputData(src); + elevFilter->SetLowPoint(0, bounds[2], 0); + elevFilter->SetHighPoint(0, bounds[3], 0); + elevFilter->SetScalarRange(bounds[2], bounds[3]); + elevFilter->Update(); + + return elevFilter->GetPolyDataOutput(); } -std::map<int, std::vector<double>> -MakeCustomBands(double const dR[2], int const& numberOfBands, - std::vector<std::array<double, 2>> const& myBands) +vtkSmartPointer<vtkPolyData> GetHills() { - std::map<int, std::vector<double>> bands; - if ((dR[1] < dR[0]) || (numberOfBands <= 0)) + // Create four hills on a plane. + // This will have regions of negative, zero and positive Gsaussian curvatures. + + auto xRes = 50; + auto yRes = 50; + auto xMin = -5.0; + auto xMax = 5.0; + auto dx = (xMax - xMin) / (xRes - 1.0); + auto yMin = -5.0; + auto yMax = 5.0; + auto dy = (yMax - yMin) / (xRes - 1.0); + + // Make a grid. + vtkNew<vtkPoints> points; + for (auto i = 0; i < xRes; ++i) { - return bands; + auto x = xMin + i * dx; + for (auto j = 0; j < yRes; ++j) + { + auto y = yMin + j * dy; + points->InsertNextPoint(x, y, 0); + } } - std::vector<std::array<double, 2>> x; - std::copy(myBands.begin(), myBands.end(), std::back_inserter(x)); + // Add the grid points to a polydata object. + vtkNew<vtkPolyData> plane; + plane->SetPoints(points); - // Determine the index of the range minimum and range maximum. - int idxMin = 0; - for (auto idx = 0; idx < myBands.size(); ++idx) + // Triangulate the grid. + vtkNew<vtkDelaunay2D> delaunay; + delaunay->SetInputData(plane); + delaunay->Update(); + + auto polydata = delaunay->GetOutput(); + + vtkNew<vtkDoubleArray> elevation; + elevation->SetNumberOfTuples(points->GetNumberOfPoints()); + + // We define the parameters for the hills here. + // [[0: x0, 1: y0, 2: x variance, 3: y variance, 4: amplitude]...] + std::vector<std::array<double, 5>> hd{{-2.5, -2.5, 2.5, 6.5, 3.5}, + {2.5, 2.5, 2.5, 2.5, 2}, + {5.0, -2.5, 1.5, 1.5, 2.5}, + {-5.0, 5, 2.5, 3.0, 3}}; + std::array<double, 2> xx{0.0, 0.0}; + for (auto i = 0; i < points->GetNumberOfPoints(); ++i) { - if (dR[0] < myBands[idx][1] && dR[0] >= myBands[idx][0]) + auto x = polydata->GetPoint(i); + for (size_t j = 0; j < hd.size(); ++j) { - idxMin = idx; - break; + xx[0] = std::pow(x[0] - hd[j][0] / hd[j][2], 2.0); + xx[1] = std::pow(x[1] - hd[j][1] / hd[j][3], 2.0); + x[2] += hd[j][4] * std::exp(-(xx[0] + xx[1]) / 2.0); } + polydata->GetPoints()->SetPoint(i, x); + elevation->SetValue(i, x[2]); } - int idxMax = static_cast<int>(myBands.size()) - 1; - for (auto idx = myBands.size() - 1; idx >= 0; --idx) + + vtkNew<vtkFloatArray> textures; + textures->SetNumberOfComponents(2); + textures->SetNumberOfTuples(2 * polydata->GetNumberOfPoints()); + textures->SetName("Textures"); + + for (auto i = 0; i < xRes; ++i) { - if (dR[1] < myBands[idx][1] && dR[1] >= myBands[idx][0]) + float tc[2]; + tc[0] = i / (xRes - 1.0); + for (auto j = 0; j < yRes; ++j) { - idxMax = static_cast<int>(idx); - break; + // tc[1] = 1.0 - j / (yRes - 1.0); + tc[1] = j / (yRes - 1.0); + textures->SetTuple(static_cast<vtkIdType>(i) * yRes + j, tc); } } - // Set the minimum to match the range minimum. - x[idxMin][0] = dR[0]; - x[idxMax][1] = dR[1]; - for (int i = idxMin; i < idxMax + 1; ++i) - { - std::vector<double> b(3); - b[0] = x[i][0]; - b[1] = x[i][0] + (x[i][1] - x[i][0]) / 2.0; - b[2] = x[i][1]; - bands[i] = b; - } - return bands; -} + polydata->GetPointData()->SetScalars(elevation); + polydata->GetPointData()->GetScalars()->SetName("Elevation"); + polydata->GetPointData()->SetTCoords(textures); -std::map<int, std::vector<double>> MakeIntegralBands(double const dR[2]) -{ - std::map<int, std::vector<double>> bands; - if (dR[1] < dR[0]) - { - return bands; - } - double x[2]; - for (int i = 0; i < 2; ++i) - { - x[i] = dR[i]; - } - x[0] = std::floor(x[0]); - x[1] = std::ceil(x[1]); - int numberOfBands = static_cast<int>(std::abs(x[1]) + std::abs(x[0])); - return MakeBands(x, numberOfBands, false); -} + vtkNew<vtkPolyDataNormals> normals; + normals->SetInputData(polydata); + normals->SetInputData(polydata); + normals->SetFeatureAngle(30); + normals->SplittingOff(); -vtkSmartPointer<vtkPolyData> MakeElevations(vtkPolyData* src) -{ - double bounds[6] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - src->GetBounds(bounds); - if (std::abs(bounds[2]) < 1.0e-8 && std::abs(bounds[3]) < 1.0e-8) - { - bounds[3] = bounds[2] + 1; - } - vtkNew<vtkElevationFilter> elevFilter; - elevFilter->SetInputData(src); - elevFilter->SetLowPoint(0, bounds[2], 0); - elevFilter->SetHighPoint(0, bounds[3], 0); - elevFilter->SetScalarRange(bounds[2], bounds[3]); - elevFilter->Update(); + vtkNew<vtkTransform> tr1; + tr1->RotateX(-90); - return elevFilter->GetPolyDataOutput(); + vtkNew<vtkTransformPolyDataFilter> tf1; + tf1->SetInputConnection(normals->GetOutputPort()); + tf1->SetTransform(tr1); + tf1->Update(); + + return tf1->GetOutput(); } -vtkSmartPointer<vtkPolyData> MakeParametricHills() +vtkSmartPointer<vtkPolyData> GetParametricHills() { vtkNew<vtkParametricRandomHills> fn; fn->AllowRandomGenerationOn(); @@ -606,7 +519,7 @@ vtkSmartPointer<vtkPolyData> MakeParametricHills() return transformFilter->GetOutput(); } -vtkSmartPointer<vtkPolyData> MakeParametricTorus() +vtkSmartPointer<vtkPolyData> GetParametricTorus() { vtkNew<vtkParametricTorus> fn; fn->SetRingRadius(5); @@ -636,7 +549,7 @@ vtkSmartPointer<vtkPolyData> MakeParametricTorus() return transformFilter->GetOutput(); } -vtkSmartPointer<vtkPolyData> MakePlane() +vtkSmartPointer<vtkPolyData> GetPlane() { vtkNew<vtkPlaneSource> source; source->SetOrigin(-10.0, -10.0, 0.0); @@ -670,7 +583,7 @@ vtkSmartPointer<vtkPolyData> MakePlane() return cleaner->GetOutput(); } -vtkSmartPointer<vtkPolyData> MakeSphere() +vtkSmartPointer<vtkPolyData> GetSphere() { vtkNew<vtkSphereSource> source; source->SetCenter(0.0, 0.0, 0.0); @@ -682,7 +595,7 @@ vtkSmartPointer<vtkPolyData> MakeSphere() return source->GetOutput(); } -vtkSmartPointer<vtkPolyData> MakeTorus() +vtkSmartPointer<vtkPolyData> GetTorus() { vtkNew<vtkSuperquadricSource> source; source->SetCenter(0.0, 0.0, 0.0); @@ -711,6 +624,44 @@ vtkSmartPointer<vtkPolyData> MakeTorus() return cleaner->GetOutput(); } +vtkSmartPointer<vtkPolyData> GetSource(std::string const& source) +{ + std::string surface = source; + std::transform(surface.begin(), surface.end(), surface.begin(), + [](unsigned char c) { return std::tolower(c); }); + std::map<std::string, int> available_surfaces = { + {"hills", 0}, {"parametrictorus", 1}, {"plane", 2}, + {"randomhills", 3}, {"sphere", 4}, {"torus", 5}}; + if (available_surfaces.find(surface) == available_surfaces.end()) + { + std::cout << "The surface is not available." << std::endl; + std::cout << "Using RandomHills instead." << std::endl; + surface = "randomhills"; + } + switch (available_surfaces[surface]) + { + case 0: + return GetHills(); + break; + case 1: + return GetParametricTorus(); + break; + case 2: + return GetElevations(GetPlane()); + break; + case 3: + return GetParametricHills(); + break; + case 4: + return GetElevations(GetSphere()); + break; + case 5: + return GetElevations(GetTorus()); + break; + } + return GetParametricHills(); +} + vtkSmartPointer<vtkColorSeries> GetColorSeries() { @@ -729,7 +680,7 @@ vtkSmartPointer<vtkColorSeries> GetColorSeries() return colorSeries; } -vtkSmartPointer<vtkLookupTable> MakeCategoricalLUT() +vtkSmartPointer<vtkLookupTable> GetCategoricalLUT() { vtkSmartPointer<vtkColorSeries> colorSeries = GetColorSeries(); // Make the lookup table. @@ -740,7 +691,7 @@ vtkSmartPointer<vtkLookupTable> MakeCategoricalLUT() return lut; } -vtkSmartPointer<vtkLookupTable> MakeOrdinaLUT() +vtkSmartPointer<vtkLookupTable> GetOrdinaLUT() { vtkSmartPointer<vtkColorSeries> colorSeries = GetColorSeries(); // Make the lookup table. @@ -764,7 +715,7 @@ vtkSmartPointer<vtkLookupTable> MakeOrdinaLUT() * */ // clang-format on -vtkSmartPointer<vtkLookupTable> MakeDivergingLUT() +vtkSmartPointer<vtkLookupTable> GetDivergingLUT() { vtkNew<vtkColorTransferFunction> ctf; @@ -816,11 +767,169 @@ vtkSmartPointer<vtkLookupTable> ReverseLUT(vtkLookupTable* lut) return lutr; } -std::map<int, int> Frequencies(std::map<int, std::vector<double>>& bands, - vtkPolyData* src) +vtkNew<vtkGlyph3D> GetGlyphs(vtkPolyData* src, double const& scaleFactor, + bool const& reverseNormals) +{ + // Sometimes the contouring algorithm can create a volume whose gradient + // vector and ordering of polygon(using the right hand rule) are + // inconsistent. vtkReverseSense cures this problem. + vtkNew<vtkReverseSense> reverse; + vtkNew<vtkMaskPoints> maskPts; + maskPts->SetOnRatio(5); + maskPts->RandomModeOn(); + if (reverseNormals) + { + reverse->SetInputData(src); + reverse->ReverseCellsOn(); + reverse->ReverseNormalsOn(); + maskPts->SetInputConnection(reverse->GetOutputPort()); + } + else + { + maskPts->SetInputData(src); + } + + // Source for the glyph filter + vtkNew<vtkArrowSource> arrow; + arrow->SetTipResolution(16); + arrow->SetTipLength(0.3); + arrow->SetTipRadius(0.1); + + vtkNew<vtkGlyph3D> glyph; + glyph->SetSourceConnection(arrow->GetOutputPort()); + glyph->SetInputConnection(maskPts->GetOutputPort()); + glyph->SetVectorModeToUseNormal(); + glyph->SetScaleFactor(scaleFactor); + glyph->SetColorModeToColorByVector(); + glyph->SetScaleModeToScaleByVector(); + glyph->OrientOn(); + glyph->Update(); + + return glyph; +} + +std::map<int, std::vector<double>> GetBands(double const dR[2], + int const& numberOfBands, + int const& precision, + bool const& nearestInteger) +{ + auto prec = abs(precision); + prec = (prec > 14) ? 14 : prec; + + auto RoundOff = [&prec](const double& x) { + auto pow_10 = std::pow(10.0, prec); + return std::round(x * pow_10) / pow_10; + }; + + std::map<int, std::vector<double>> bands; + if ((dR[1] < dR[0]) || (numberOfBands <= 0)) + { + return bands; + } + double x[2]; + for (int i = 0; i < 2; ++i) + { + x[i] = dR[i]; + } + if (nearestInteger) + { + x[0] = std::floor(x[0]); + x[1] = std::ceil(x[1]); + } + double dx = (x[1] - x[0]) / static_cast<double>(numberOfBands); + std::vector<double> b; + b.push_back(x[0]); + b.push_back(x[0] + dx / 2.0); + b.push_back(x[0] + dx); + for (int i = 0; i < numberOfBands; ++i) + { + if (i == 0) + { + for (std::vector<double>::iterator p = b.begin(); p != b.end(); ++p) + { + *p = RoundOff(*p); + } + b[0] = x[0]; + } + bands[i] = b; + for (std::vector<double>::iterator p = b.begin(); p != b.end(); ++p) + { + *p = RoundOff(*p + dx); + } + } + return bands; +} + +std::map<int, std::vector<double>> +GetCustomBands(double const dR[2], int const& numberOfBands, + std::vector<std::array<double, 2>> const& myBands) +{ + std::map<int, std::vector<double>> bands; + if ((dR[1] < dR[0]) || (numberOfBands <= 0)) + { + return bands; + } + + std::vector<std::array<double, 2>> x; + std::copy(myBands.begin(), myBands.end(), std::back_inserter(x)); + + // Determine the index of the range minimum and range maximum. + int idxMin = 0; + for (auto idx = 0; idx < static_cast<int>(myBands.size()); ++idx) + { + if (dR[0] < myBands[idx][1] && dR[0] >= myBands[idx][0]) + { + idxMin = idx; + break; + } + } + int idxMax = static_cast<int>(myBands.size()) - 1; + for (int idx = static_cast<int>(myBands.size()) - 1; idx >= 0; --idx) + { + if (dR[1] < myBands[idx][1] && dR[1] >= myBands[idx][0]) + { + idxMax = static_cast<int>(idx); + break; + } + } + + // Set the minimum to match the range minimum. + x[idxMin][0] = dR[0]; + x[idxMax][1] = dR[1]; + for (int i = idxMin; i < idxMax + 1; ++i) + { + std::vector<double> b(3); + b[0] = x[i][0]; + b[1] = x[i][0] + (x[i][1] - x[i][0]) / 2.0; + b[2] = x[i][1]; + bands[i] = b; + } + return bands; +} + +std::map<int, std::vector<double>> GetIntegralBands(double const dR[2]) +{ + std::map<int, std::vector<double>> bands; + if (dR[1] < dR[0]) + { + return bands; + } + double x[2]; + for (int i = 0; i < 2; ++i) + { + x[i] = dR[i]; + } + x[0] = std::floor(x[0]); + x[1] = std::ceil(x[1]); + int numberOfBands = static_cast<int>(std::abs(x[1]) + std::abs(x[0])); + return GetBands(x, numberOfBands, false); +} + +std::map<int, int> GetFrequencies(std::map<int, std::vector<double>>& bands, + vtkPolyData* src) { std::map<int, int> freq; - for (int i = 0; i < bands.size(); ++i) + for (auto i = 0; i < static_cast<int>(bands.size()); ++i) { freq[i] = 0; } @@ -828,7 +937,7 @@ std::map<int, int> Frequencies(std::map<int, std::vector<double>>& bands, for (int i = 0; i < tuples; ++i) { double* x = src->GetPointData()->GetScalars()->GetTuple(i); - for (int j = 0; j < bands.size(); ++j) + for (auto j = 0; j < static_cast<int>(bands.size()); ++j) { if (*x <= bands[j][2]) { @@ -840,10 +949,12 @@ std::map<int, int> Frequencies(std::map<int, std::vector<double>>& bands, return freq; } -std::pair<int, int> AdjustFrequencyRanges(std::map<int, int>& freq) +void AdjustFrequencyRanges(std::map<int, std::vector<double>>& bands, + std::map<int, int>& freq) { + // Get the indices of the first and last non-zero elements. auto first = 0; - for (auto i = 0; i < freq.size(); ++i) + for (auto i = 0; i < static_cast<int>(freq.size()); ++i) { if (freq[i] != 0) { @@ -858,7 +969,7 @@ std::pair<int, int> AdjustFrequencyRanges(std::map<int, int>& freq) } std::reverse(keys.begin(), keys.end()); auto last = keys[0]; - for (auto i = 0; i < keys.size(); ++i) + for (size_t i = 0; i < keys.size(); ++i) { if (freq[keys[i]] != 0) { @@ -866,76 +977,56 @@ std::pair<int, int> AdjustFrequencyRanges(std::map<int, int>& freq) break; } } - return std::pair<int, int>(first, last); -} - -void PrintBands(std::map<int, std::vector<double>> const& bands) -{ - std::ostringstream os; - os << "Bands:\n"; - size_t idx = 0; - for (std::map<int, std::vector<double>>::const_iterator p = bands.begin(); - p != bands.end(); ++p) + // Now adjust the ranges. + std::map<int, int>::iterator freqItr; + freqItr = freq.find(first); + freq.erase(freq.begin(), freqItr); + freqItr = ++freq.find(last); + freq.erase(freqItr, freq.end()); + std::map<int, std::vector<double>>::iterator bandItr; + bandItr = bands.find(first); + bands.erase(bands.begin(), bandItr); + bandItr = ++bands.find(last); + bands.erase(bandItr, bands.end()); + // Reindex freq and bands. + std::map<int, int> adjFreq; + int idx = 0; + for (auto p : freq) { - for (std::vector<double>::const_iterator q = p->second.begin(); - q != p->second.end(); ++q) - { - if (q == p->second.begin()) - { - os << std::setw(4) << idx << " ["; - } - if (q == std::prev(p->second.end())) - { - os << std::fixed << std::setw(6) << std::setprecision(3) << *q << "]\n"; - } - else - { - os << std::fixed << std::setw(6) << std::setprecision(3) << *q << ", "; - } - } + adjFreq[idx] = p.second; ++idx; } - std::cout << os.str() << std::endl; -} - -void PrintFrequencies(std::map<int, int> const& freq) -{ - std::ostringstream os; - int i = 0; - for (std::map<int, int>::const_iterator p = freq.begin(); p != freq.end(); - ++p) + std::map<int, std::vector<double>> adjBands; + idx = 0; + for (auto p : bands) { - if (i == 0) - { - os << "Frequencies: ["; - } - if (p == std::prev(freq.end())) - { - os << i << ": " << p->second << "]\n"; - } - else - { - os << i << ": " << p->second << ", "; - } - ++i; + adjBands[idx] = p.second; + ++idx; } - std::cout << os.str() << endl; + bands = adjBands; + freq = adjFreq; } void PrintBandsFrequencies(std::map<int, std::vector<double>> const& bands, - std::map<int, int>& freq) + std::map<int, int>& freq, int const& precision) { + auto prec = abs(precision); + prec = (prec > 14) ? 14 : prec; + if (bands.size() != freq.size()) { std::cout << "Bands and frequencies must be the same size." << std::endl; return; } std::ostringstream os; - os << "Bands & frequencies:\n"; + os << "Bands & Frequencies:\n"; size_t idx = 0; + auto total = 0; + auto width = prec + 6; for (std::map<int, std::vector<double>>::const_iterator p = bands.begin(); p != bands.end(); ++p) { + total += freq[p->first]; for (std::vector<double>::const_iterator q = p->second.begin(); q != p->second.end(); ++q) { @@ -945,57 +1036,21 @@ void PrintBandsFrequencies(std::map<int, std::vector<double>> const& bands, } if (q == std::prev(p->second.end())) { - os << std::fixed << std::setw(6) << std::setprecision(3) << *q - << "]: " << std::setw(6) << freq[p->first] << "\n"; + os << std::fixed << std::setw(width) << std::setprecision(prec) << *q + << "]: " << std::setw(8) << freq[p->first] << "\n"; } else { - os << std::fixed << std::setw(6) << std::setprecision(3) << *q << ", "; + os << std::fixed << std::setw(width) << std::setprecision(prec) << *q + << ", "; } } ++idx; } + width = 3 * width + 13; + os << std::left << std::setw(width) << "Total" << std::right << std::setw(8) + << total << std::endl; std::cout << os.str() << endl; } -vtkNew<vtkGlyph3D> MakeGlyphs(vtkPolyData* src, bool const& reverseNormals) -{ - // Sometimes the contouring algorithm can create a volume whose gradient - // vector and ordering of polygon(using the right hand rule) are - // inconsistent. vtkReverseSense cures this problem. - vtkNew<vtkReverseSense> reverse; - vtkNew<vtkMaskPoints> maskPts; - maskPts->SetOnRatio(5); - maskPts->RandomModeOn(); - if (reverseNormals) - { - reverse->SetInputData(src); - reverse->ReverseCellsOn(); - reverse->ReverseNormalsOn(); - maskPts->SetInputConnection(reverse->GetOutputPort()); - } - else - { - maskPts->SetInputData(src); - } - - // Source for the glyph filter - vtkNew<vtkArrowSource> arrow; - arrow->SetTipResolution(16); - arrow->SetTipLength(0.3); - arrow->SetTipRadius(0.1); - - vtkNew<vtkGlyph3D> glyph; - glyph->SetSourceConnection(arrow->GetOutputPort()); - glyph->SetInputConnection(maskPts->GetOutputPort()); - glyph->SetVectorModeToUseNormal(); - glyph->SetScaleFactor(1.0); - glyph->SetColorModeToColorByVector(); - glyph->SetScaleModeToScaleByVector(); - glyph->OrientOn(); - glyph->Update(); - - return glyph; -} - } // namespace diff --git a/src/Python/Utilities/VTKImportsForPython.py b/src/Python/Utilities/VTKImportsForPython.py index 05ae68119552510f4c893e159b54ce9440032706..6653b8f926db6cdf2b7adf135aa8f24feb72eed2 100755 --- a/src/Python/Utilities/VTKImportsForPython.py +++ b/src/Python/Utilities/VTKImportsForPython.py @@ -131,7 +131,7 @@ def main(json_path, src_paths, ofn): if path.is_file() and path.suffix == '.py': classes_constants.update(get_classes_constants(path)) elif path.is_dir(): - paths = list(Path(fn).rglob('*.py')) + paths = list(Path(fn).glob('*.py')) program_path = Path(__file__) for path in paths: if path.resolve() != program_path.resolve(): @@ -165,7 +165,9 @@ def main(json_path, src_paths, ofn): res = format_imports(imports) if ofn: - path = Path(ofn).with_suffix('.txt') + path = Path(ofn) + if path.suffix == '': + path = Path(ofn).with_suffix('.txt') path.write_text('\n'.join(res)) else: print('\n'.join(res)) diff --git a/src/Python/Visualization/CurvatureBandsWithGlyphs.py b/src/Python/Visualization/CurvatureBandsWithGlyphs.py index 1839b195770820803304e240d9127e3801d089cd..e61b63e81aaf0ab93caf66b4ffa552417880f713 100755 --- a/src/Python/Visualization/CurvatureBandsWithGlyphs.py +++ b/src/Python/Visualization/CurvatureBandsWithGlyphs.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import math -from collections import OrderedDict import numpy as np from vtk.util import numpy_support @@ -22,7 +21,8 @@ from vtkmodules.vtkCommonCore import ( vtkLookupTable, vtkPoints, vtkVariant, - vtkVariantArray + vtkVariantArray, + vtkVersion ) from vtkmodules.vtkCommonDataModel import vtkPolyData from vtkmodules.vtkCommonTransforms import vtkTransform @@ -162,7 +162,7 @@ def main(argv): # Let's do a frequency table. # The number of scalars in each band. - freq = frequencies(bands, cc.GetOutput()) + freq = get_frequencies(bands, cc.GetOutput()) bands, freq = adjust_ranges(bands, freq) print_bands_frequencies(bands, freq) @@ -293,10 +293,12 @@ def main(argv): ren_win.AddRenderer(ren) # Important: The interactor must be set prior to enabling the widget. iren.SetRenderWindow(ren_win) - cam_orient_manipulator = vtkCameraOrientationWidget() - cam_orient_manipulator.SetParentRenderer(ren) - # Enable the widget. - cam_orient_manipulator.On() + if vtk_version_ok(9, 0, 20210718): + cam_orient_manipulator = vtkCameraOrientationWidget() + cam_orient_manipulator = vtkCameraOrientationWidget() + cam_orient_manipulator.SetParentRenderer(ren) + # Enable the widget. + cam_orient_manipulator.On() # add actors ren.AddViewProp(src_actor) @@ -321,83 +323,179 @@ def main(argv): iren.Start() -def get_custom_bands(d_r, number_of_bands, my_bands): +def vtk_version_ok(major, minor, build): """ - Divide a range into custom bands. - - You need to specify each band as an list [r1, r2] where r1 < r2 and - append these to a list. - The list should ultimately look - like this: [[r1, r2], [r2, r3], [r3, r4]...] + Check the VTK version. - :param: d_r - [min, max] the range that is to be covered by the bands. - :param: number_of_bands - the number of bands, a positive integer. - :return: A dictionary consisting of band number and [min, midpoint, max] for each band. + :param major: Requested major version. + :param minor: Requested minor version. + :param build: Requested build version. + :return: True if the requested VTK version is >= the actual VTK version. """ - bands = dict() - if (d_r[1] < d_r[0]) or (number_of_bands <= 0): - return bands - x = my_bands - # Determine the index of the range minimum and range maximum. - idx_min = 0 - for idx in range(0, len(my_bands)): - if my_bands[idx][1] > d_r[0] >= my_bands[idx][0]: - idx_min = idx - break + requested_version = (100 * int(major) + int(minor)) * 100000000 + int(build) + ver = vtkVersion() + actual_version = (100 * ver.GetVTKMajorVersion() + ver.GetVTKMinorVersion()) \ + * 100000000 + ver.GetVTKBuildVersion() + if actual_version >= requested_version: + return True + else: + return False - idx_max = len(my_bands) - 1 - for idx in range(len(my_bands) - 1, -1, -1): - if my_bands[idx][1] > d_r[1] >= my_bands[idx][0]: - idx_max = idx - break - # Set the minimum to match the range minimum. - x[idx_min][0] = d_r[0] - x[idx_max][1] = d_r[1] - x = x[idx_min: idx_max + 1] - for idx, e in enumerate(x): - bands[idx] = [e[0], e[0] + (e[1] - e[0]) / 2, e[1]] - return bands +def adjust_edge_curvatures(source, curvature_name, epsilon=1.0e-08): + """ + This function adjusts curvatures along the edges of the surface by replacing + the value with the average value of the curvatures of points in the neighborhood. + Remember to update the vtkCurvatures object before calling this. -def frequencies(bands, src): - """ - Count the number of scalars in each band. - :param: bands - the bands. - :param: src - the vtkPolyData source. - :return: The frequencies of the scalars in each band. + :param source: A vtkPolyData object corresponding to the vtkCurvatures object. + :param curvature_name: The name of the curvature, 'Gauss_Curvature' or 'Mean_Curvature'. + :param epsilon: Absolute curvature values less than this will be set to zero. + :return: """ - freq = OrderedDict() - for i in range(len(bands)): - freq[i] = 0 - tuples = src.GetPointData().GetScalars().GetNumberOfTuples() - for i in range(tuples): - x = src.GetPointData().GetScalars().GetTuple1(i) - for j in range(len(bands)): - if x <= bands[j][2]: - freq[j] = freq[j] + 1 - break - return freq + def point_neighbourhood(pt_id): + """ + Find the ids of the neighbours of pt_id. + + :param pt_id: The point id. + :return: The neighbour ids. + """ + """ + Extract the topological neighbors for point pId. In two steps: + 1) source.GetPointCells(pt_id, cell_ids) + 2) source.GetCellPoints(cell_id, cell_point_ids) for all cell_id in cell_ids + """ + cell_ids = vtkIdList() + source.GetPointCells(pt_id, cell_ids) + neighbour = set() + for cell_idx in range(0, cell_ids.GetNumberOfIds()): + cell_id = cell_ids.GetId(cell_idx) + cell_point_ids = vtkIdList() + source.GetCellPoints(cell_id, cell_point_ids) + for cell_pt_idx in range(0, cell_point_ids.GetNumberOfIds()): + neighbour.add(cell_point_ids.GetId(cell_pt_idx)) + return neighbour + + def compute_distance(pt_id_a, pt_id_b): + """ + Compute the distance between two points given their ids. + + :param pt_id_a: + :param pt_id_b: + :return: + """ + pt_a = np.array(source.GetPoint(pt_id_a)) + pt_b = np.array(source.GetPoint(pt_id_b)) + return np.linalg.norm(pt_a - pt_b) + + # Get the active scalars + source.GetPointData().SetActiveScalars(curvature_name) + np_source = dsa.WrapDataObject(source) + curvatures = np_source.PointData[curvature_name] + + # Get the boundary point IDs. + array_name = 'ids' + id_filter = vtkIdFilter() + id_filter.SetInputData(source) + id_filter.SetPointIds(True) + id_filter.SetCellIds(False) + id_filter.SetPointIdsArrayName(array_name) + id_filter.SetCellIdsArrayName(array_name) + id_filter.Update() + + edges = vtkFeatureEdges() + edges.SetInputConnection(id_filter.GetOutputPort()) + edges.BoundaryEdgesOn() + edges.ManifoldEdgesOff() + edges.NonManifoldEdgesOff() + edges.FeatureEdgesOff() + edges.Update() + + edge_array = edges.GetOutput().GetPointData().GetArray(array_name) + boundary_ids = [] + for i in range(edges.GetOutput().GetNumberOfPoints()): + boundary_ids.append(edge_array.GetValue(i)) + # Remove duplicate Ids. + p_ids_set = set(boundary_ids) -def adjust_frequency_ranges(freq): + # Iterate over the edge points and compute the curvature as the weighted + # average of the neighbours. + count_invalid = 0 + for p_id in boundary_ids: + p_ids_neighbors = point_neighbourhood(p_id) + # Keep only interior points. + p_ids_neighbors -= p_ids_set + # Compute distances and extract curvature values. + curvs = [curvatures[p_id_n] for p_id_n in p_ids_neighbors] + dists = [compute_distance(p_id_n, p_id) for p_id_n in p_ids_neighbors] + curvs = np.array(curvs) + dists = np.array(dists) + curvs = curvs[dists > 0] + dists = dists[dists > 0] + if len(curvs) > 0: + weights = 1 / np.array(dists) + weights /= weights.sum() + new_curv = np.dot(curvs, weights) + else: + # Corner case. + count_invalid += 1 + # Assuming the curvature of the point is planar. + new_curv = 0.0 + # Set the new curvature value. + curvatures[p_id] = new_curv + + # Set small values to zero. + if epsilon != 0.0: + curvatures = np.where(abs(curvatures) < epsilon, 0, curvatures) + # Curvatures is now an ndarray + curv = numpy_support.numpy_to_vtk(num_array=curvatures.ravel(), + deep=True, + array_type=VTK_DOUBLE) + curv.SetName(curvature_name) + source.GetPointData().RemoveArray(curvature_name) + source.GetPointData().AddArray(curv) + source.GetPointData().SetActiveScalars(curvature_name) + + +def constrain_curvatures(source, curvature_name, lower_bound=0.0, upper_bound=0.0): """ - Get the indices of the first and last non-zero elements. - :param freq: The frequency dictionary. - :return: The indices of the first and last non-zero elements. + This function constrains curvatures to the range [lower_bound ... upper_bound]. + + Remember to update the vtkCurvatures object before calling this. + + :param source: A vtkPolyData object corresponding to the vtkCurvatures object. + :param curvature_name: The name of the curvature, 'Gauss_Curvature' or 'Mean_Curvature'. + :param lower_bound: The lower bound. + :param upper_bound: The upper bound. + :return: """ - first = 0 - for k, v in freq.items(): - if v != 0: - first = k - break - rev_keys = list(freq.keys())[::-1] - last = rev_keys[0] - for idx in list(freq.keys())[::-1]: - if freq[idx] != 0: - last = idx - break - return first, last + + bounds = list() + if lower_bound < upper_bound: + bounds.append(lower_bound) + bounds.append(upper_bound) + else: + bounds.append(upper_bound) + bounds.append(lower_bound) + + # Get the active scalars + source.GetPointData().SetActiveScalars(curvature_name) + np_source = dsa.WrapDataObject(source) + curvatures = np_source.PointData[curvature_name] + + # Set upper and lower bounds. + curvatures = np.where(curvatures < bounds[0], bounds[0], curvatures) + curvatures = np.where(curvatures > bounds[1], bounds[1], curvatures) + # Curvatures is now an ndarray + curv = numpy_support.numpy_to_vtk(num_array=curvatures.ravel(), + deep=True, + array_type=VTK_DOUBLE) + curv.SetName(curvature_name) + source.GetPointData().RemoveArray(curvature_name) + source.GetPointData().AddArray(curv) + source.GetPointData().SetActiveScalars(curvature_name) def get_elevations(src): @@ -651,160 +749,24 @@ def get_torus(): return cleaner.GetOutput() -def adjust_edge_curvatures(source, curvature_name, epsilon=1.0e-08): - """ - This function adjusts curvatures along the edges of the surface by replacing - the value with the average value of the curvatures of points in the neighborhood. - - Remember to update the vtkCurvatures object before calling this. - - :param source: A vtkPolyData object corresponding to the vtkCurvatures object. - :param curvature_name: The name of the curvature, 'Gauss_Curvature' or 'Mean_Curvature'. - :param epsilon: Absolute curvature values less than this will be set to zero. - :return: - """ - - def point_neighbourhood(pt_id): - """ - Find the ids of the neighbours of pt_id. - - :param pt_id: The point id. - :return: The neighbour ids. - """ - """ - Extract the topological neighbors for point pId. In two steps: - 1) source.GetPointCells(pt_id, cell_ids) - 2) source.GetCellPoints(cell_id, cell_point_ids) for all cell_id in cell_ids - """ - cell_ids = vtkIdList() - source.GetPointCells(pt_id, cell_ids) - neighbour = set() - for cell_idx in range(0, cell_ids.GetNumberOfIds()): - cell_id = cell_ids.GetId(cell_idx) - cell_point_ids = vtkIdList() - source.GetCellPoints(cell_id, cell_point_ids) - for cell_pt_idx in range(0, cell_point_ids.GetNumberOfIds()): - neighbour.add(cell_point_ids.GetId(cell_pt_idx)) - return neighbour - - def compute_distance(pt_id_a, pt_id_b): - """ - Compute the distance between two points given their ids. - - :param pt_id_a: - :param pt_id_b: - :return: - """ - pt_a = np.array(source.GetPoint(pt_id_a)) - pt_b = np.array(source.GetPoint(pt_id_b)) - return np.linalg.norm(pt_a - pt_b) - - # Get the active scalars - source.GetPointData().SetActiveScalars(curvature_name) - np_source = dsa.WrapDataObject(source) - curvatures = np_source.PointData[curvature_name] - - # Get the boundary point IDs. - array_name = 'ids' - id_filter = vtkIdFilter() - id_filter.SetInputData(source) - id_filter.SetPointIds(True) - id_filter.SetCellIds(False) - id_filter.SetPointIdsArrayName(array_name) - id_filter.SetCellIdsArrayName(array_name) - id_filter.Update() - - edges = vtkFeatureEdges() - edges.SetInputConnection(id_filter.GetOutputPort()) - edges.BoundaryEdgesOn() - edges.ManifoldEdgesOff() - edges.NonManifoldEdgesOff() - edges.FeatureEdgesOff() - edges.Update() - - edge_array = edges.GetOutput().GetPointData().GetArray(array_name) - boundary_ids = [] - for i in range(edges.GetOutput().GetNumberOfPoints()): - boundary_ids.append(edge_array.GetValue(i)) - # Remove duplicate Ids. - p_ids_set = set(boundary_ids) - - # Iterate over the edge points and compute the curvature as the weighted - # average of the neighbours. - count_invalid = 0 - for p_id in boundary_ids: - p_ids_neighbors = point_neighbourhood(p_id) - # Keep only interior points. - p_ids_neighbors -= p_ids_set - # Compute distances and extract curvature values. - curvs = [curvatures[p_id_n] for p_id_n in p_ids_neighbors] - dists = [compute_distance(p_id_n, p_id) for p_id_n in p_ids_neighbors] - curvs = np.array(curvs) - dists = np.array(dists) - curvs = curvs[dists > 0] - dists = dists[dists > 0] - if len(curvs) > 0: - weights = 1 / np.array(dists) - weights /= weights.sum() - new_curv = np.dot(curvs, weights) - else: - # Corner case. - count_invalid += 1 - # Assuming the curvature of the point is planar. - new_curv = 0.0 - # Set the new curvature value. - curvatures[p_id] = new_curv - - # Set small values to zero. - if epsilon != 0.0: - curvatures = np.where(abs(curvatures) < epsilon, 0, curvatures) - # Curvatures is now an ndarray - curv = numpy_support.numpy_to_vtk(num_array=curvatures.ravel(), - deep=True, - array_type=VTK_DOUBLE) - curv.SetName(curvature_name) - source.GetPointData().RemoveArray(curvature_name) - source.GetPointData().AddArray(curv) - source.GetPointData().SetActiveScalars(curvature_name) - - -def constrain_curvatures(source, curvature_name, lower_bound=0.0, upper_bound=0.0): - """ - This function constrains curvatures to the range [lower_bound ... upper_bound]. - - Remember to update the vtkCurvatures object before calling this. - - :param source: A vtkPolyData object corresponding to the vtkCurvatures object. - :param curvature_name: The name of the curvature, 'Gauss_Curvature' or 'Mean_Curvature'. - :param lower_bound: The lower bound. - :param upper_bound: The upper bound. - :return: - """ - - bounds = list() - if lower_bound < upper_bound: - bounds.append(lower_bound) - bounds.append(upper_bound) - else: - bounds.append(upper_bound) - bounds.append(lower_bound) - - # Get the active scalars - source.GetPointData().SetActiveScalars(curvature_name) - np_source = dsa.WrapDataObject(source) - curvatures = np_source.PointData[curvature_name] - - # Set upper and lower bounds. - curvatures = np.where(curvatures < bounds[0], bounds[0], curvatures) - curvatures = np.where(curvatures > bounds[1], bounds[1], curvatures) - # Curvatures is now an ndarray - curv = numpy_support.numpy_to_vtk(num_array=curvatures.ravel(), - deep=True, - array_type=VTK_DOUBLE) - curv.SetName(curvature_name) - source.GetPointData().RemoveArray(curvature_name) - source.GetPointData().AddArray(curv) - source.GetPointData().SetActiveScalars(curvature_name) +def get_source(source): + surface = source.lower() + available_surfaces = ['hills', 'parametrictorus', 'plane', 'randomhills', 'sphere', 'torus'] + if surface not in available_surfaces: + return None + elif surface == 'hills': + return get_hills() + elif surface == 'parametrictorus': + return get_parametric_torus() + elif surface == 'plane': + return get_elevations(get_plane()) + elif surface == 'randomhills': + return get_parametric_hills() + elif surface == 'sphere': + return get_elevations(get_sphere()) + elif surface == 'torus': + return get_elevations(get_torus()) + return None def get_color_series(): @@ -950,26 +912,6 @@ def get_glyphs(src, scale_factor=1.0, reverse_normals=False): return glyph -def get_source(source): - surface = source.lower() - available_surfaces = ['hills', 'parametrictorus', 'plane', 'randomhills', 'sphere', 'torus'] - if surface not in available_surfaces: - return None - elif surface == 'hills': - return get_hills() - elif surface == 'parametrictorus': - return get_parametric_torus() - elif surface == 'plane': - return get_elevations(get_plane()) - elif surface == 'randomhills': - return get_parametric_hills() - elif surface == 'sphere': - return get_elevations(get_sphere()) - elif surface == 'torus': - return get_elevations(get_torus()) - return None - - def get_bands(d_r, number_of_bands, precision=2, nearest_integer=False): """ Divide a range into bands @@ -1003,6 +945,45 @@ def get_bands(d_r, number_of_bands, precision=2, nearest_integer=False): return bands +def get_custom_bands(d_r, number_of_bands, my_bands): + """ + Divide a range into custom bands. + + You need to specify each band as an list [r1, r2] where r1 < r2 and + append these to a list. + The list should ultimately look + like this: [[r1, r2], [r2, r3], [r3, r4]...] + + :param: d_r - [min, max] the range that is to be covered by the bands. + :param: number_of_bands - the number of bands, a positive integer. + :return: A dictionary consisting of band number and [min, midpoint, max] for each band. + """ + bands = dict() + if (d_r[1] < d_r[0]) or (number_of_bands <= 0): + return bands + x = my_bands + # Determine the index of the range minimum and range maximum. + idx_min = 0 + for idx in range(0, len(my_bands)): + if my_bands[idx][1] > d_r[0] >= my_bands[idx][0]: + idx_min = idx + break + + idx_max = len(my_bands) - 1 + for idx in range(len(my_bands) - 1, -1, -1): + if my_bands[idx][1] > d_r[1] >= my_bands[idx][0]: + idx_max = idx + break + + # Set the minimum to match the range minimum. + x[idx_min][0] = d_r[0] + x[idx_max][1] = d_r[1] + x = x[idx_min: idx_max + 1] + for idx, e in enumerate(x): + bands[idx] = [e[0], e[0] + (e[1] - e[0]) / 2, e[1]] + return bands + + def get_frequencies(bands, src): """ Count the number of scalars in each band. diff --git a/src/Python/Visualization/ElevationBandsWithGlyphs.py b/src/Python/Visualization/ElevationBandsWithGlyphs.py index 8dd6bfb75e64380b0d1a79bb52ab2f68d6b5c96f..594efa4057dbe851eded1f3b42f1d9264bcde34a 100755 --- a/src/Python/Visualization/ElevationBandsWithGlyphs.py +++ b/src/Python/Visualization/ElevationBandsWithGlyphs.py @@ -1,57 +1,122 @@ #!/usr/bin/env python import math -from collections import OrderedDict -import vtk +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingOpenGL2 +from vtkmodules.vtkCommonColor import ( + vtkColorSeries, + vtkNamedColors +) +from vtkmodules.vtkCommonComputationalGeometry import ( + vtkParametricRandomHills, + vtkParametricTorus +) +from vtkmodules.vtkCommonCore import ( + vtkDoubleArray, + vtkFloatArray, + vtkLookupTable, + vtkPoints, + vtkVariant, + vtkVariantArray, + vtkVersion +) +from vtkmodules.vtkCommonDataModel import vtkPolyData +from vtkmodules.vtkCommonTransforms import vtkTransform +from vtkmodules.vtkFiltersCore import ( + vtkCleanPolyData, + vtkDelaunay2D, + vtkElevationFilter, + vtkGlyph3D, + vtkMaskPoints, + vtkPolyDataNormals, + vtkReverseSense, + vtkTriangleFilter +) +from vtkmodules.vtkFiltersGeneral import ( + vtkTransformPolyDataFilter +) +from vtkmodules.vtkFiltersModeling import vtkBandedPolyDataContourFilter +from vtkmodules.vtkFiltersSources import ( + vtkArrowSource, + vtkParametricFunctionSource, + vtkPlaneSource, + vtkSphereSource, + vtkSuperquadricSource +) +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera +from vtkmodules.vtkInteractionWidgets import vtkCameraOrientationWidget +from vtkmodules.vtkRenderingAnnotation import vtkScalarBarActor +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkColorTransferFunction, + vtkPolyDataMapper, + vtkRenderWindow, + vtkRenderWindowInteractor, + vtkRenderer +) + + +def vtk_version_ok(major, minor, build): + """ + Check the VTK version. + + :param major: Requested major version. + :param minor: Requested minor version. + :param build: Requested build version. + :return: True if the requested VTK version is >= the actual VTK version. + """ + requested_version = (100 * int(major) + int(minor)) * 100000000 + int(build) + ver = vtkVersion() + actual_version = (100 * ver.GetVTKMajorVersion() + ver.GetVTKMinorVersion()) \ + * 100000000 + ver.GetVTKBuildVersion() + if actual_version >= requested_version: + return True + else: + return False -def main(): +def main(argv): # ------------------------------------------------------------ # Create the surface, lookup tables, contour filter etc. # ------------------------------------------------------------ - # desired_surface = 'ParametricTorus' - # desired_surface = 'Plane' + # desired_surface = 'Hills' + # desired_surface = 'ParametricTorus' + # desired_surface = 'Plane' desired_surface = 'RandomHills' - # desired_surface = 'Sphere' - # desired_surface = 'Torus' - surface = desired_surface.lower() - available_surfaces = ['parametrictorus', 'plane', 'randomhills', 'sphere', 'torus'] - if surface not in available_surfaces: - print('No surface specified.') - return - if surface == 'parametrictorus': - src = make_parametric_torus() - elif surface == 'plane': - src = make_elevations(make_plane()) - elif surface == 'randomhills': - src = make_parametric_hills() - elif surface == 'sphere': - src = make_elevations(make_sphere()) - elif surface == 'torus': - src = make_elevations(make_torus()) - else: - print('No surface specified.') + # desired_surface = 'Sphere' + # desired_surface = 'Torus' + source = get_source(desired_surface) + if not source: + print('The surface is not available.') return + + # The length of the normal arrow glyphs. + scale_factor = 1.0 + if desired_surface == 'Hills': + scale_factor = 0.5 + elif desired_surface == 'Sphere': + scale_factor = 2.0 print(desired_surface) - src.GetPointData().SetActiveScalars('Elevation') - scalar_range = src.GetPointData().GetScalars('Elevation').GetRange() + source.GetPointData().SetActiveScalars('Elevation') + scalar_range = source.GetPointData().GetScalars('Elevation').GetRange() - lut = make_categorical_lut() - lut1 = make_ordinal_lut() + lut = get_categorical_lut() + lut1 = get_ordinal_lut() lut.SetTableRange(scalar_range) lut1.SetTableRange(scalar_range) number_of_bands = lut.GetNumberOfTableValues() lut.SetNumberOfTableValues(number_of_bands) - bands = make_bands(scalar_range, number_of_bands, False) + precision = 10 + bands = get_bands(scalar_range, number_of_bands, precision, False) - if surface == 'randomhills': + if desired_surface == 'RandomHills': # These are my custom bands. # Generated by first running: - # bands = make_bands(scalar_range, number_of_bands, False) + # bands = get_bands(scalar_range, number_of_bands, precision, False) # then: - # freq = frequencies(bands, src) + # freq = get_frequencies(bands, source) # print_bands_frequencies(bands, freq) # Finally using the output to create this table: my_bands = [ @@ -60,52 +125,29 @@ def main(): [6.0, 7.0], [7.0, 8.0]] # Comment this out if you want to see how allocating # equally spaced bands works. - bands = make_custom_bands(scalar_range, number_of_bands, my_bands) - # bands = make_bands(scalar_range, number_of_bands, False) - # Adjust the number of table values - number_of_bands = len(bands) - lut.SetNumberOfTableValues(number_of_bands) - lut1.SetNumberOfTableValues(number_of_bands) + bands = get_custom_bands(scalar_range, number_of_bands, my_bands) + # bands = get_bands(scalar_range, number_of_bands, precision, False) # Let's do a frequency table. # The number of scalars in each band. - freq = frequencies(bands, src) + freq = get_frequencies(bands, source) + bands, freq = adjust_ranges(bands, freq) + print_bands_frequencies(bands, freq) - min_key = min(freq.keys()) - max_key = max(freq.keys()) - first, last = adjust_frequency_ranges(freq) - for idx in range(min_key, first): - freq.pop(idx) - bands.pop(idx) - for idx in range(last + 1, max_key + 1): - freq.popitem() - bands.popitem() - old_keys = freq.keys() - adj_freq = OrderedDict() - adj_bands = OrderedDict() - - for idx, k in enumerate(old_keys): - adj_freq[idx] = freq[k] - adj_bands[idx] = bands[k] - # print_bands_frequencies(bands, freq) - print_bands_frequencies(adj_bands, adj_freq) - - min_key = min(adj_freq.keys()) - max_key = max(adj_freq.keys()) - scalar_range_curvatures = (adj_bands[min_key][0], adj_bands[max_key][2]) - lut.SetTableRange(scalar_range_curvatures) - lut.SetNumberOfTableValues(len(adj_bands)) - lut1.SetNumberOfTableValues(len(adj_bands)) + scalar_range = (bands[0][0], bands[len(bands) - 1][2]) + lut.SetTableRange(scalar_range) + lut.SetNumberOfTableValues(len(bands)) + lut1.SetNumberOfTableValues(len(bands)) # We will use the midpoint of the band as the label. labels = [] - for i in range(len(adj_bands)): - labels.append('{:4.2f}'.format(adj_bands[i][1])) + for k in bands: + labels.append('{:4.2f}'.format(bands[k][1])) # Annotate - values = vtk.vtkVariantArray() + values = vtkVariantArray() for i in range(len(labels)): - values.InsertNextValue(vtk.vtkVariant(labels[i])) + values.InsertNextValue(vtkVariant(labels[i])) for i in range(values.GetNumberOfTuples()): lut.SetAnnotation(i, values.GetValue(i).ToString()) @@ -113,47 +155,47 @@ def main(): lutr = reverse_lut(lut) # Create the contour bands. - bcf = vtk.vtkBandedPolyDataContourFilter() - bcf.SetInputData(src) + bcf = vtkBandedPolyDataContourFilter() + bcf.SetInputData(source) # Use either the minimum or maximum value for each band. - for i in range(len(adj_bands)): - bcf.SetValue(i, adj_bands[i][2]) + for i in range(len(bands)): + bcf.SetValue(i, bands[i][2]) # We will use an indexed lookup table. bcf.SetScalarModeToIndex() bcf.GenerateContourEdgesOn() # Generate the glyphs on the original surface. - glyph = make_glyphs(src, False) + glyph = get_glyphs(source, scale_factor, False) # ------------------------------------------------------------ # Create the mappers and actors # ------------------------------------------------------------ - colors = vtk.vtkNamedColors() + colors = vtkNamedColors() # Set the background color. colors.SetColor('BkgColor', [179, 204, 255, 255]) colors.SetColor("ParaViewBkg", [82, 87, 110, 255]) - src_mapper = vtk.vtkPolyDataMapper() + src_mapper = vtkPolyDataMapper() src_mapper.SetInputConnection(bcf.GetOutputPort()) src_mapper.SetScalarRange(scalar_range) src_mapper.SetLookupTable(lut) src_mapper.SetScalarModeToUseCellData() - src_actor = vtk.vtkActor() + src_actor = vtkActor() src_actor.SetMapper(src_mapper) # Create contour edges - edge_mapper = vtk.vtkPolyDataMapper() + edge_mapper = vtkPolyDataMapper() edge_mapper.SetInputData(bcf.GetContourEdgesOutput()) edge_mapper.SetResolveCoincidentTopologyToPolygonOffset() - edge_actor = vtk.vtkActor() + edge_actor = vtkActor() edge_actor.SetMapper(edge_mapper) edge_actor.GetProperty().SetColor(colors.GetColor3d('Black')) - glyph_mapper = vtk.vtkPolyDataMapper() + glyph_mapper = vtkPolyDataMapper() glyph_mapper.SetInputConnection(glyph.GetOutputPort()) glyph_mapper.SetScalarModeToUsePointFieldData() glyph_mapper.SetColorModeToMapScalars() @@ -163,14 +205,14 @@ def main(): glyph_mapper.SetLookupTable(lut1) glyph_mapper.SetScalarRange(scalar_range) - glyph_actor = vtk.vtkActor() + glyph_actor = vtkActor() glyph_actor.SetMapper(glyph_mapper) window_width = 800 window_height = 800 # Add a scalar bar. - scalar_bar = vtk.vtkScalarBarActor() + scalar_bar = vtkScalarBarActor() # This LUT puts the lowest value at the top of the scalar bar. # scalar_bar->SetLookupTable(lut); # Use this LUT if you want the highest value at the top. @@ -190,19 +232,21 @@ def main(): # ------------------------------------------------------------ # Create the RenderWindow, Renderer and Interactor # ------------------------------------------------------------ - ren = vtk.vtkRenderer() - ren_win = vtk.vtkRenderWindow() - iren = vtk.vtkRenderWindowInteractor() - style = vtk.vtkInteractorStyleTrackballCamera() + ren = vtkRenderer() + ren_win = vtkRenderWindow() + iren = vtkRenderWindowInteractor() + style = vtkInteractorStyleTrackballCamera() iren.SetInteractorStyle(style) ren_win.AddRenderer(ren) # Important: The interactor must be set prior to enabling the widget. iren.SetRenderWindow(ren_win) - cam_orient_manipulator = vtk.vtkCameraOrientationWidget() - cam_orient_manipulator.SetParentRenderer(ren) - # Enable the widget. - cam_orient_manipulator.On() + if vtk_version_ok(9, 0, 20210718): + cam_orient_manipulator = vtkCameraOrientationWidget() + cam_orient_manipulator = vtkCameraOrientationWidget() + cam_orient_manipulator.SetParentRenderer(ren) + # Enable the widget. + cam_orient_manipulator.On() # add actors ren.AddViewProp(src_actor) @@ -226,111 +270,7 @@ def main(): iren.Start() -def make_bands(d_r, number_of_bands, nearest_integer): - """ - Divide a range into bands - :param: d_r - [min, max] the range that is to be covered by the bands. - :param: number_of_bands - the number of bands, a positive integer. - :param: nearest_integer - if True then [floor(min), ceil(max)] is used. - :return: A dictionary consisting of the band number and [min, midpoint, max] for each band. - """ - bands = OrderedDict() - if (d_r[1] < d_r[0]) or (number_of_bands <= 0): - return bands - x = list(d_r) - if nearest_integer: - x[0] = math.floor(x[0]) - x[1] = math.ceil(x[1]) - dx = (x[1] - x[0]) / float(number_of_bands) - b = [x[0], x[0] + dx / 2.0, x[0] + dx] - i = 0 - while i < number_of_bands: - bands[i] = b - b = [b[0] + dx, b[1] + dx, b[2] + dx] - i += 1 - return bands - - -def make_custom_bands(d_r, number_of_bands, my_bands): - """ - Divide a range into custom bands. - - You need to specify each band as an list [r1, r2] where r1 < r2 and - append these to a list. - The list should ultimately look - like this: [[r1, r2], [r2, r3], [r3, r4]...] - - :param: d_r - [min, max] the range that is to be covered by the bands. - :param: number_of_bands - the number of bands, a positive integer. - :return: A dixtionary consisting of band number and [min, midpoint, max] for each band. - """ - bands = OrderedDict() - if (d_r[1] < d_r[0]) or (number_of_bands <= 0): - return bands - x = my_bands - # Determine the index of the range minimum and range maximum. - idx_min = 0 - for idx in range(0, len(my_bands)): - if my_bands[idx][1] > d_r[0] >= my_bands[idx][0]: - idx_min = idx - break - - idx_max = len(my_bands) - 1 - for idx in range(len(my_bands) - 1, -1, -1): - if my_bands[idx][1] > d_r[1] >= my_bands[idx][0]: - idx_max = idx - break - - # Set the minimum to match the range minimum. - x[idx_min][0] = d_r[0] - x[idx_max][1] = d_r[1] - x = x[idx_min: idx_max + 1] - for idx, e in enumerate(x): - bands[idx] = [e[0], e[0] + (e[1] - e[0]) / 2, e[1]] - return bands - - -def frequencies(bands, src): - """ - Count the number of scalars in each band. - :param: bands - the bands. - :param: src - the vtkPolyData source. - :return: The frequencies of the scalars in each band. - """ - freq = OrderedDict() - for i in range(len(bands)): - freq[i] = 0 - tuples = src.GetPointData().GetScalars().GetNumberOfTuples() - for i in range(tuples): - x = src.GetPointData().GetScalars().GetTuple1(i) - for j in range(len(bands)): - if x <= bands[j][2]: - freq[j] = freq[j] + 1 - break - return freq - - -def adjust_frequency_ranges(freq): - """ - Get the indices of the first and last non-zero elements. - :param freq: The frequency dictionary. - :return: The indices of the first and last non-zero elements. - """ - first = 0 - for k, v in freq.items(): - if v != 0: - first = k - break - rev_keys = list(freq.keys())[::-1] - last = rev_keys[0] - for idx in list(freq.keys())[::-1]: - if freq[idx] != 0: - last = idx - break - return first, last - - -def make_elevations(src): +def get_elevations(src): """ Generate elevations over the surface. :param: src - the vtkPolyData source. @@ -340,7 +280,7 @@ def make_elevations(src): src.GetBounds(bounds) if abs(bounds[2]) < 1.0e-8 and abs(bounds[3]) < 1.0e-8: bounds[3] = bounds[2] + 1 - elev_filter = vtk.vtkElevationFilter() + elev_filter = vtkElevationFilter() elev_filter.SetInputData(src) elev_filter.SetLowPoint(0, bounds[2], 0) elev_filter.SetHighPoint(0, bounds[3], 0) @@ -349,12 +289,94 @@ def make_elevations(src): return elev_filter.GetPolyDataOutput() -def make_parametric_hills(): +def get_hills(): + # Create four hills on a plane. + # This will have regions of negative, zero and positive Gsaussian curvatures. + + x_res = 50 + y_res = 50 + x_min = -5.0 + x_max = 5.0 + dx = (x_max - x_min) / (x_res - 1) + y_min = -5.0 + y_max = 5.0 + dy = (y_max - y_min) / (x_res - 1) + + # Make a grid. + points = vtkPoints() + for i in range(0, x_res): + x = x_min + i * dx + for j in range(0, y_res): + y = y_min + j * dy + points.InsertNextPoint(x, y, 0) + + # Add the grid points to a polydata object. + plane = vtkPolyData() + plane.SetPoints(points) + + # Triangulate the grid. + delaunay = vtkDelaunay2D() + delaunay.SetInputData(plane) + delaunay.Update() + + polydata = delaunay.GetOutput() + + elevation = vtkDoubleArray() + elevation.SetNumberOfTuples(points.GetNumberOfPoints()) + + # We define the parameters for the hills here. + # [[0: x0, 1: y0, 2: x variance, 3: y variance, 4: amplitude]...] + hd = [[-2.5, -2.5, 2.5, 6.5, 3.5], [2.5, 2.5, 2.5, 2.5, 2], + [5.0, -2.5, 1.5, 1.5, 2.5], [-5.0, 5, 2.5, 3.0, 3]] + xx = [0.0] * 2 + for i in range(0, points.GetNumberOfPoints()): + x = list(polydata.GetPoint(i)) + for j in range(0, len(hd)): + xx[0] = (x[0] - hd[j][0] / hd[j][2]) ** 2.0 + xx[1] = (x[1] - hd[j][1] / hd[j][3]) ** 2.0 + x[2] += hd[j][4] * math.exp(-(xx[0] + xx[1]) / 2.0) + polydata.GetPoints().SetPoint(i, x) + elevation.SetValue(i, x[2]) + + textures = vtkFloatArray() + textures.SetNumberOfComponents(2) + textures.SetNumberOfTuples(2 * polydata.GetNumberOfPoints()) + textures.SetName("Textures") + + for i in range(0, x_res): + tc = [i / (x_res - 1.0), 0.0] + for j in range(0, y_res): + # tc[1] = 1.0 - j / (y_res - 1.0) + tc[1] = j / (y_res - 1.0) + textures.SetTuple(i * y_res + j, tc) + + polydata.GetPointData().SetScalars(elevation) + polydata.GetPointData().GetScalars().SetName("Elevation") + polydata.GetPointData().SetTCoords(textures) + + normals = vtkPolyDataNormals() + normals.SetInputData(polydata) + normals.SetInputData(polydata) + normals.SetFeatureAngle(30) + normals.SplittingOff() + + tr1 = vtkTransform() + tr1.RotateX(-90) + + tf1 = vtkTransformPolyDataFilter() + tf1.SetInputConnection(normals.GetOutputPort()) + tf1.SetTransform(tr1) + tf1.Update() + + return tf1.GetOutput() + + +def get_parametric_hills(): """ Make a parametric hills surface as the source. :return: vtkPolyData with normal and scalar data. """ - fn = vtk.vtkParametricRandomHills() + fn = vtkParametricRandomHills() fn.AllowRandomGenerationOn() fn.SetRandomSeed(1) fn.SetNumberOfHills(30) @@ -363,7 +385,7 @@ def make_parametric_hills(): # if fn.GetClassName() == 'vtkParametricRandomHills': # fn.ClockwiseOrderingOff() - source = vtk.vtkParametricFunctionSource() + source = vtkParametricFunctionSource() source.SetParametricFunction(fn) source.SetUResolution(50) source.SetVResolution(50) @@ -375,10 +397,10 @@ def make_parametric_hills(): # Rename the scalars to 'Elevation' since we are using the Z-scalars as elevations. source.GetOutput().GetPointData().GetScalars().SetName('Elevation') - transform = vtk.vtkTransform() + transform = vtkTransform() transform.Translate(0.0, 5.0, 15.0) transform.RotateX(-90.0) - transform_filter = vtk.vtkTransformPolyDataFilter() + transform_filter = vtkTransformPolyDataFilter() transform_filter.SetInputConnection(source.GetOutputPort()) transform_filter.SetTransform(transform) transform_filter.Update() @@ -386,17 +408,17 @@ def make_parametric_hills(): return transform_filter.GetOutput() -def make_parametric_torus(): +def get_parametric_torus(): """ Make a parametric torus as the source. :return: vtkPolyData with normal and scalar data. """ - fn = vtk.vtkParametricTorus() + fn = vtkParametricTorus() fn.SetRingRadius(5) fn.SetCrossSectionRadius(2) - source = vtk.vtkParametricFunctionSource() + source = vtkParametricFunctionSource() source.SetParametricFunction(fn) source.SetUResolution(50) source.SetVResolution(50) @@ -409,9 +431,9 @@ def make_parametric_torus(): # Rename the scalars to 'Elevation' since we are using the Z-scalars as elevations. source.GetOutput().GetPointData().GetScalars().SetName('Elevation') - transform = vtk.vtkTransform() + transform = vtkTransform() transform.RotateX(-90.0) - transform_filter = vtk.vtkTransformPolyDataFilter() + transform_filter = vtkTransformPolyDataFilter() transform_filter.SetInputConnection(source.GetOutputPort()) transform_filter.SetTransform(transform) transform_filter.Update() @@ -419,13 +441,13 @@ def make_parametric_torus(): return transform_filter.GetOutput() -def make_plane(): +def get_plane(): """ Make a plane as the source. :return: vtkPolyData with normal and scalar data. """ - source = vtk.vtkPlaneSource() + source = vtkPlaneSource() source.SetOrigin(-10.0, -10.0, 0.0) source.SetPoint2(-10.0, 10.0, 0.0) source.SetPoint1(10.0, -10.0, 0.0) @@ -433,10 +455,10 @@ def make_plane(): source.SetYResolution(20) source.Update() - transform = vtk.vtkTransform() + transform = vtkTransform() transform.Translate(0.0, 0.0, 0.0) transform.RotateX(-90.0) - transform_filter = vtk.vtkTransformPolyDataFilter() + transform_filter = vtkTransformPolyDataFilter() transform_filter.SetInputConnection(source.GetOutputPort()) transform_filter.SetTransform(transform) transform_filter.Update() @@ -444,12 +466,12 @@ def make_plane(): # We have a m x n array of quadrilaterals arranged as a regular tiling in a # plane. So pass it through a triangle filter since the curvature filter only # operates on polys. - tri = vtk.vtkTriangleFilter() + tri = vtkTriangleFilter() tri.SetInputConnection(transform_filter.GetOutputPort()) # Pass it though a CleanPolyDataFilter and merge any points which # are coincident, or very close - cleaner = vtk.vtkCleanPolyData() + cleaner = vtkCleanPolyData() cleaner.SetInputConnection(tri.GetOutputPort()) cleaner.SetTolerance(0.005) cleaner.Update() @@ -457,8 +479,8 @@ def make_plane(): return cleaner.GetOutput() -def make_sphere(): - source = vtk.vtkSphereSource() +def get_sphere(): + source = vtkSphereSource() source.SetCenter(0.0, 0.0, 0.0) source.SetRadius(10.0) source.SetThetaResolution(32) @@ -468,12 +490,12 @@ def make_sphere(): return source.GetOutput() -def make_torus(): +def get_torus(): """ Make a torus as the source. :return: vtkPolyData with normal and scalar data. """ - source = vtk.vtkSuperquadricSource() + source = vtkSuperquadricSource() source.SetCenter(0.0, 0.0, 0.0) source.SetScale(1.0, 1.0, 1.0) source.SetPhiResolution(64) @@ -485,13 +507,13 @@ def make_torus(): # The quadric is made of strips, so pass it through a triangle filter as # the curvature filter only operates on polys - tri = vtk.vtkTriangleFilter() + tri = vtkTriangleFilter() tri.SetInputConnection(source.GetOutputPort()) # The quadric has nasty discontinuities from the way the edges are generated # so let's pass it though a CleanPolyDataFilter and merge any points which # are coincident, or very close - cleaner = vtk.vtkCleanPolyData() + cleaner = vtkCleanPolyData() cleaner.SetInputConnection(tri.GetOutputPort()) cleaner.SetTolerance(0.005) cleaner.Update() @@ -499,80 +521,28 @@ def make_torus(): return cleaner.GetOutput() -def clipper(src, dx, dy, dz): - """ - Clip a vtkPolyData source. - A cube is made whose size corresponds the the bounds of the source. - Then each side is shrunk by the appropriate dx, dy or dz. After - this operation the source is clipped by the cube. - :param: src - the vtkPolyData source - :param: dx - the amount to clip in the x-direction - :param: dy - the amount to clip in the y-direction - :param: dz - the amount to clip in the z-direction - :return: vtkPolyData. - """ - bounds = [0, 0, 0, 0, 0, 0] - src.GetBounds(bounds) - - plane1 = vtk.vtkPlane() - plane1.SetOrigin(bounds[0] + dx, 0, 0) - plane1.SetNormal(1, 0, 0) - - plane2 = vtk.vtkPlane() - plane2.SetOrigin(bounds[1] - dx, 0, 0) - plane2.SetNormal(-1, 0, 0) - - plane3 = vtk.vtkPlane() - plane3.SetOrigin(0, bounds[2] + dy, 0) - plane3.SetNormal(0, 1, 0) - - plane4 = vtk.vtkPlane() - plane4.SetOrigin(0, bounds[3] - dy, 0) - plane4.SetNormal(0, -1, 0) - - plane5 = vtk.vtkPlane() - plane5.SetOrigin(0, 0, bounds[4] + dz) - plane5.SetNormal(0, 0, 1) - - plane6 = vtk.vtkPlane() - plane6.SetOrigin(0, 0, bounds[5] - dz) - plane6.SetNormal(0, 0, -1) - - clip_function = vtk.vtkImplicitBoolean() - clip_function.SetOperationTypeToUnion() - clip_function.AddFunction(plane1) - clip_function.AddFunction(plane2) - clip_function.AddFunction(plane3) - clip_function.AddFunction(plane4) - clip_function.AddFunction(plane5) - clip_function.AddFunction(plane6) - - # Clip it. - pd_clipper = vtk.vtkClipPolyData() - pd_clipper.SetClipFunction(clip_function) - pd_clipper.SetInputData(src) - pd_clipper.GenerateClipScalarsOff() - pd_clipper.GenerateClippedOutputOff() - # pd_clipper.GenerateClippedOutputOn() - pd_clipper.Update() - return pd_clipper.GetOutput() - - -def calculate_curvatures(src): - """ - The source must be triangulated. - :param: src - the source. - :return: vtkPolyData with normal and scalar data representing curvatures. - """ - curvature = vtk.vtkCurvatures() - curvature.SetCurvatureTypeToGaussian() - curvature.SetInputData(src) - curvature.Update() - return curvature.GetOutput() +def get_source(source): + surface = source.lower() + available_surfaces = ['hills', 'parametrictorus', 'plane', 'randomhills', 'sphere', 'torus'] + if surface not in available_surfaces: + return None + elif surface == 'hills': + return get_hills() + elif surface == 'parametrictorus': + return get_parametric_torus() + elif surface == 'plane': + return get_elevations(get_plane()) + elif surface == 'randomhills': + return get_parametric_hills() + elif surface == 'sphere': + return get_elevations(get_sphere()) + elif surface == 'torus': + return get_elevations(get_torus()) + return None def get_color_series(): - color_series = vtk.vtkColorSeries() + color_series = vtkColorSeries() # Select a color scheme. # color_series_enum = color_series.BREWER_DIVERGING_BROWN_BLUE_GREEN_9 # color_series_enum = color_series.BREWER_DIVERGING_SPECTRAL_10 @@ -586,39 +556,71 @@ def get_color_series(): return color_series -def make_categorical_lut(): +def get_categorical_lut(): """ Make a lookup table using vtkColorSeries. :return: An indexed (categorical) lookup table. """ color_series = get_color_series() # Make the lookup table. - lut = vtk.vtkLookupTable() + lut = vtkLookupTable() color_series.BuildLookupTable(lut, color_series.CATEGORICAL) lut.SetNanColor(0, 0, 0, 1) return lut -def make_ordinal_lut(): +def get_ordinal_lut(): """ Make a lookup table using vtkColorSeries. :return: An ordinal (not indexed) lookup table. """ color_series = get_color_series() # Make the lookup table. - lut = vtk.vtkLookupTable() + lut = vtkLookupTable() color_series.BuildLookupTable(lut, color_series.ORDINAL) lut.SetNanColor(0, 0, 0, 1) return lut +def get_diverging_lut(): + """ + See: [Diverging Color Maps for Scientific Visualization](https://www.kennethmoreland.com/color-maps/) + start point midPoint end point + cool to warm: 0.230, 0.299, 0.754 0.865, 0.865, 0.865 0.706, 0.016, 0.150 + purple to orange: 0.436, 0.308, 0.631 0.865, 0.865, 0.865 0.759, 0.334, 0.046 + green to purple: 0.085, 0.532, 0.201 0.865, 0.865, 0.865 0.436, 0.308, 0.631 + blue to brown: 0.217, 0.525, 0.910 0.865, 0.865, 0.865 0.677, 0.492, 0.093 + green to red: 0.085, 0.532, 0.201 0.865, 0.865, 0.865 0.758, 0.214, 0.233 + + :return: + """ + ctf = vtkColorTransferFunction() + ctf.SetColorSpaceToDiverging() + # Cool to warm. + ctf.AddRGBPoint(0.0, 0.085, 0.532, 0.201) + ctf.AddRGBPoint(0.5, 0.865, 0.865, 0.865) + ctf.AddRGBPoint(1.0, 0.758, 0.214, 0.233) + + table_size = 256 + lut = vtkLookupTable() + lut.SetNumberOfTableValues(table_size) + lut.Build() + + for i in range(0, table_size): + rgba = list(ctf.GetColor(float(i) / table_size)) + rgba.append(1) + lut.SetTableValue(i, rgba) + + return lut + + def reverse_lut(lut): """ Create a lookup table with the colors reversed. :param: lut - An indexed lookup table. :return: The reversed indexed lookup table. """ - lutr = vtk.vtkLookupTable() + lutr = vtkLookupTable() lutr.DeepCopy(lut) t = lut.GetNumberOfTableValues() - 1 rev_range = reversed(list(range(t + 1))) @@ -635,7 +637,7 @@ def reverse_lut(lut): return lutr -def make_glyphs(src, reverse_normals): +def get_glyphs(src, scale_factor=1.0, reverse_normals=False): """ Glyph the normals on the surface. @@ -650,10 +652,10 @@ def make_glyphs(src, reverse_normals): # Sometimes the contouring algorithm can create a volume whose gradient # vector and ordering of polygon (using the right hand rule) are # inconsistent. vtkReverseSense cures this problem. - reverse = vtk.vtkReverseSense() + reverse = vtkReverseSense() # Choose a random subset of points. - mask_pts = vtk.vtkMaskPoints() + mask_pts = vtkMaskPoints() mask_pts.SetOnRatio(5) mask_pts.RandomModeOn() if reverse_normals: @@ -665,16 +667,16 @@ def make_glyphs(src, reverse_normals): mask_pts.SetInputData(src) # Source for the glyph filter - arrow = vtk.vtkArrowSource() + arrow = vtkArrowSource() arrow.SetTipResolution(16) arrow.SetTipLength(0.3) arrow.SetTipRadius(0.1) - glyph = vtk.vtkGlyph3D() + glyph = vtkGlyph3D() glyph.SetSourceConnection(arrow.GetOutputPort()) glyph.SetInputConnection(mask_pts.GetOutputPort()) glyph.SetVectorModeToUseNormal() - glyph.SetScaleFactor(1.0) + glyph.SetScaleFactor(scale_factor) glyph.SetColorModeToColorByVector() glyph.SetScaleModeToScaleByVector() glyph.OrientOn() @@ -682,46 +684,166 @@ def make_glyphs(src, reverse_normals): return glyph -def print_bands(bands): - s = f'Bands:\n' - for k, v in bands.items(): - for j, q in enumerate(v): - if j == 0: - s += f'{k:4d} [' - if j == len(v) - 1: - s += f'{q:8.3f}]\n' - else: - s += f'{q:8.3f}, ' - print(s) - +def get_bands(d_r, number_of_bands, precision=2, nearest_integer=False): + """ + Divide a range into bands + :param: d_r - [min, max] the range that is to be covered by the bands. + :param: number_of_bands - The number of bands, a positive integer. + :param: precision - The decimal precision of the bounds. + :param: nearest_integer - If True then [floor(min), ceil(max)] is used. + :return: A dictionary consisting of the band number and [min, midpoint, max] for each band. + """ + prec = abs(precision) + if prec > 14: + prec = 14 -def print_frequencies(freq): - s = '' - for i, p in freq.items(): + bands = dict() + if (d_r[1] < d_r[0]) or (number_of_bands <= 0): + return bands + x = list(d_r) + if nearest_integer: + x[0] = math.floor(x[0]) + x[1] = math.ceil(x[1]) + dx = (x[1] - x[0]) / float(number_of_bands) + b = [x[0], x[0] + dx / 2.0, x[0] + dx] + i = 0 + while i < number_of_bands: + b = list(map(lambda ele_b: round(ele_b, prec), b)) if i == 0: - s += f'Frequencies: [' - if i == len(freq) - 1: - s += f'{i}: {p} ]' - else: - s += f'{i}: {p}, ' - print(s) + b[0] = x[0] + bands[i] = b + b = [b[0] + dx, b[1] + dx, b[2] + dx] + i += 1 + return bands -def print_bands_frequencies(bands, freq): +def get_custom_bands(d_r, number_of_bands, my_bands): + """ + Divide a range into custom bands. + + You need to specify each band as an list [r1, r2] where r1 < r2 and + append these to a list. + The list should ultimately look + like this: [[r1, r2], [r2, r3], [r3, r4]...] + + :param: d_r - [min, max] the range that is to be covered by the bands. + :param: number_of_bands - the number of bands, a positive integer. + :return: A dictionary consisting of band number and [min, midpoint, max] for each band. + """ + bands = dict() + if (d_r[1] < d_r[0]) or (number_of_bands <= 0): + return bands + x = my_bands + # Determine the index of the range minimum and range maximum. + idx_min = 0 + for idx in range(0, len(my_bands)): + if my_bands[idx][1] > d_r[0] >= my_bands[idx][0]: + idx_min = idx + break + + idx_max = len(my_bands) - 1 + for idx in range(len(my_bands) - 1, -1, -1): + if my_bands[idx][1] > d_r[1] >= my_bands[idx][0]: + idx_max = idx + break + + # Set the minimum to match the range minimum. + x[idx_min][0] = d_r[0] + x[idx_max][1] = d_r[1] + x = x[idx_min: idx_max + 1] + for idx, e in enumerate(x): + bands[idx] = [e[0], e[0] + (e[1] - e[0]) / 2, e[1]] + return bands + + +def get_frequencies(bands, src): + """ + Count the number of scalars in each band. + The scalars used are the active scalars in the polydata. + + :param: bands - The bands. + :param: src - The vtkPolyData source. + :return: The frequencies of the scalars in each band. + """ + freq = dict() + for i in range(len(bands)): + freq[i] = 0 + tuples = src.GetPointData().GetScalars().GetNumberOfTuples() + for i in range(tuples): + x = src.GetPointData().GetScalars().GetTuple1(i) + for j in range(len(bands)): + if x <= bands[j][2]: + freq[j] += 1 + break + return freq + + +def adjust_ranges(bands, freq): + """ + The bands and frequencies are adjusted so that the first and last + frequencies in the range are non-zero. + :param bands: The bands dictionary. + :param freq: The frequency dictionary. + :return: Adjusted bands and frequencies. + """ + # Get the indices of the first and last non-zero elements. + first = 0 + for k, v in freq.items(): + if v != 0: + first = k + break + rev_keys = list(freq.keys())[::-1] + last = rev_keys[0] + for idx in list(freq.keys())[::-1]: + if freq[idx] != 0: + last = idx + break + # Now adjust the ranges. + min_key = min(freq.keys()) + max_key = max(freq.keys()) + for idx in range(min_key, first): + freq.pop(idx) + bands.pop(idx) + for idx in range(last + 1, max_key + 1): + freq.popitem() + bands.popitem() + old_keys = freq.keys() + adj_freq = dict() + adj_bands = dict() + + for idx, k in enumerate(old_keys): + adj_freq[idx] = freq[k] + adj_bands[idx] = bands[k] + + return adj_bands, adj_freq + + +def print_bands_frequencies(bands, freq, precision=2): + prec = abs(precision) + if prec > 14: + prec = 14 + if len(bands) != len(freq): - print('Bands and frequencies must be the same size.') + print('Bands and Frequencies must be the same size.') return - s = f'Bands & frequencies:\n' + s = f'Bands & Frequencies:\n' + total = 0 + width = prec + 6 for k, v in bands.items(): + total += freq[k] for j, q in enumerate(v): if j == 0: s += f'{k:4d} [' if j == len(v) - 1: - s += f'{q:8.3f}]: {freq[k]:6d}\n' + s += f'{q:{width}.{prec}f}]: {freq[k]:8d}\n' else: - s += f'{q:8.3f}, ' + s += f'{q:{width}.{prec}f}, ' + width = 3 * width + 13 + s += f'{"Total":{width}s}{total:8d}\n' print(s) if __name__ == '__main__': - main() + import sys + + main(sys.argv)