diff --git a/src/Cxx.md b/src/Cxx.md index f6d42977f191ca7be231c7ff408d1ec4a09d902c..45372abf982c1bcfd3133283559e08b72e8880a7 100644 --- a/src/Cxx.md +++ b/src/Cxx.md @@ -635,6 +635,7 @@ This section includes vtkUnstructuredGrid. [Casting](/Cxx/PolyData/Casting) | Casting VTK objects. [CheckVTKVersion](/Cxx/Utilities/CheckVTKVersion) | Check VTK Version and provide alternatives for different VTK versions [ColorLookupTable](/Cxx/Utilities/ColorLookupTable) | Color Lookup Table. +[ColorMapToLUT](/Cxx/Utilities/ColorMapToLUT) | Use vtkDiscretizableColorTransferFunction to generate a VTK colormap. [ColorTransferFunction](/Cxx/Utilities/ColorTransferFunction) | Color Transfer Function. [CommandSubclass](/Cxx/Utilities/CommandSubclass) | Instead of using a callback function, it is more powerful to subclass vtkCommand. [ConstrainedDelaunay2D](/Cxx/Filtering/ConstrainedDelaunay2D) | Perform a 2D Delaunay triangulation on a point set respecting a specified boundary. diff --git a/src/Cxx/Utilities/ColorMapToLUT.cxx b/src/Cxx/Utilities/ColorMapToLUT.cxx new file mode 100644 index 0000000000000000000000000000000000000000..4b83db31fa3fe08addbf87d2b84575e22a0b357c --- /dev/null +++ b/src/Cxx/Utilities/ColorMapToLUT.cxx @@ -0,0 +1,106 @@ +#include <vtkActor.h> +#include <vtkConeSource.h> +#include <vtkDiscretizableColorTransferFunction.h> +#include <vtkElevationFilter.h> +#include <vtkInteractorStyleTrackballCamera.h> +#include <vtkNamedColors.h> +#include <vtkNew.h> +#include <vtkPolyDataMapper.h> +#include <vtkProperty.h> +#include <vtkRenderWindow.h> +#include <vtkRenderWindowInteractor.h> +#include <vtkRenderer.h> +// #include <vtkSphereSource.h> + +#include <array> + +namespace { +vtkNew<vtkDiscretizableColorTransferFunction> getCTF(); + +} + +int main(int, char*[]) +{ + std::array<unsigned char, 4> bkg{82, 87, 110, 255}; + vtkNew<vtkNamedColors> colors; + colors->SetColor("ParaViewBkg", bkg.data()); + + vtkNew<vtkRenderer> ren; + ren->SetBackground(colors->GetColor3d("ParaViewBkg").GetData()); + vtkNew<vtkRenderWindow> renWin; + renWin->SetSize(640, 480); + renWin->SetWindowName("ColorMapToLUT"); + renWin->AddRenderer(ren); + vtkNew<vtkRenderWindowInteractor> iRen; + iRen->SetRenderWindow(renWin); + + vtkNew<vtkInteractorStyleTrackballCamera> style; + iRen->SetInteractorStyle(style); + + // vtkNew<vtkSphereSource> sphere; + // sphere->SetThetaResolution(64); + // sphere->SetPhiResolution(32); + // auto bounds = sphere->GetOutput()->GetBounds(); + + vtkNew<vtkConeSource> cone; + cone->SetResolution(6); + cone->SetDirection(0, 1, 0); + cone->SetHeight(1); + cone->Update(); + auto bounds = cone->GetOutput()->GetBounds(); + + vtkNew<vtkElevationFilter> elevation_filter; + elevation_filter->SetLowPoint(0, bounds[2], 0); + elevation_filter->SetHighPoint(0, bounds[3], 0); + elevation_filter->SetInputConnection(cone->GetOutputPort()); + // elevation_filter->SetInputConnection(sphere->GetOutputPort()); + + auto ctf = getCTF(); + + vtkNew<vtkPolyDataMapper> mapper; + mapper->SetInputConnection(elevation_filter->GetOutputPort()); + mapper->SetLookupTable(ctf); + mapper->SetColorModeToMapScalars(); + mapper->InterpolateScalarsBeforeMappingOn(); + + vtkNew<vtkActor> actor; + actor->SetMapper(mapper); + + ren->AddActor(actor); + + renWin->Render(); + iRen->Start(); + + return EXIT_SUCCESS; +} + +namespace { + +vtkNew<vtkDiscretizableColorTransferFunction> getCTF() +{ + // name: Fast, creator: Francesca Samsel, file name: Fast.xml + vtkNew<vtkDiscretizableColorTransferFunction> ctf; + + ctf->SetColorSpaceToLab(); + ctf->SetScaleToLinear(); + + ctf->SetNanColor(0, 0, 0); + + ctf->AddRGBPoint(0, 0.08800000000000002, 0.18810000000000007, 0.55); + ctf->AddRGBPoint(0.16144, 0.21989453864645603, 0.5170512023315895, + 0.7093372214401806); + ctf->AddRGBPoint(0.351671, 0.5048913252297864, 0.8647869538833338, + 0.870502284878942); + ctf->AddRGBPoint(0.501285, 1, 1, 0.83); + ctf->AddRGBPoint(0.620051, 0.9418960444346476, 0.891455547964053, + 0.5446035798119958); + ctf->AddRGBPoint(0.835408342528245, 0.75, 0.44475, 0.255); + ctf->AddRGBPoint(1, 0.56, 0.055999999999999994, 0.055999999999999994); + + ctf->SetNumberOfValues(7); + ctf->DiscretizeOff(); + + return ctf; +} + +} // namespace diff --git a/src/Cxx/Utilities/ColorMapToLUT.md b/src/Cxx/Utilities/ColorMapToLUT.md new file mode 100644 index 0000000000000000000000000000000000000000..f5d0b1d66b9c8dff677ad160d1044c83bd506404 --- /dev/null +++ b/src/Cxx/Utilities/ColorMapToLUT.md @@ -0,0 +1,12 @@ +### Description + +Demonstrate a cone using the vtkDiscretizableColorTransferFunction to generate the colormap. + +These two Python programs can be used to generate the function `def get_ctf(): ...` or `vtkNew<vtkDiscretizableColorTransferFunction> getCTF() ...` for either an XML description of a colormap or a JSON one. + +- [ColorMapToLUT_XML](../../../Python/Utilities/ColorMapToLUT_XML/) +- [ColorMapToLUT_JSON](../../../Python/Utilities/ColorMapToLUT_JSON/) + +Feel free to use either of these programs to generate different colormaps until you find one you like. + +A good initial source for color maps is: [SciVisColor](https://sciviscolor.org/) -- this will provide you with plenty of XML examples. diff --git a/src/Python.md b/src/Python.md index 87055c636db4df14c94cfd3dc89062141d21404d..6fcae4fa2c3163019b5da604d5ee6ab602a42809 100644 --- a/src/Python.md +++ b/src/Python.md @@ -365,6 +365,9 @@ This section includes vtkUnstructuredGrid. | Example Name | Description | Image | | -------------- | ------------- | ------- | [CheckVTKVersion](/Python/Utilities/CheckVTKVersion) | Check the VTK version and provide alternatives for different VTK versions. +[ColorMapToLUT_JSON](/Python/Utilities/ColorMapToLUT_JSON) | Take an JSON description of a colormap and convert it to a VTK colormap. +[ColorMapToLUT_XML](/Python/Utilities/ColorMapToLUT_XML) | Take an XML description of a colormap and convert it to a VTK colormap. +[ColorMapToLUT](/Python/Utilities/ColorMapToLUT) | Use vtkDiscretizableColorTransferFunction to generate a VTK colormap. [ConstrainedDelaunay2D](/Python/Filtering/ConstrainedDelaunay2D) | Perform a 2D Delaunay triangulation on a point set respecting a specified boundary. [Delaunay2D](/Python/Filtering/Delaunay2D) | [LUTUtilities](/Python/Utilities/LUTUtilities) | A utility class for vtkLookupTable allowing you to output the table contents or to compare tables. diff --git a/src/Python/Utilities/ColorMapToLUT.md b/src/Python/Utilities/ColorMapToLUT.md new file mode 100644 index 0000000000000000000000000000000000000000..9479f33cdcc33bfbfbef2b2040dddaab556912d4 --- /dev/null +++ b/src/Python/Utilities/ColorMapToLUT.md @@ -0,0 +1,12 @@ +### Description + +Demonstrate a cone using the vtkDiscretizableColorTransferFunction to generate the colormap. + +These two Python programs can be used to generate the function `def get_ctf(): ...` or `vtkNew<vtkDiscretizableColorTransferFunction> getCTF() ...` for either an XML description of a colormap or a JSON one. + +- [ColorMapToLUT_XML](../ColorMapToLUT_XML/) +- [ColorMapToLUT_JSON](../ColorMapToLUT_JSON/) + +Feel free to use either of these programs to generate different colormaps until you find one you like. + +A good initial source for color maps is: [SciVisColor](https://sciviscolor.org/) -- this will provide you with plenty of XML examples. diff --git a/src/Python/Utilities/ColorMapToLUT.py b/src/Python/Utilities/ColorMapToLUT.py new file mode 100755 index 0000000000000000000000000000000000000000..ede92404c81bf4ba53cbb1fad5b5852fa876f8bc --- /dev/null +++ b/src/Python/Utilities/ColorMapToLUT.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingOpenGL2 +from vtkmodules.vtkCommonColor import vtkNamedColors +from vtkmodules.vtkFiltersCore import vtkElevationFilter +from vtkmodules.vtkFiltersSources import vtkConeSource, vtkSphereSource +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkDiscretizableColorTransferFunction, + vtkPolyDataMapper, + vtkRenderWindow, + vtkRenderWindowInteractor, + vtkRenderer +) + + +def main(): + colors = vtkNamedColors() + colors.SetColor('ParaViewBkg', 82, 87, 110, 255) + + ren = vtkRenderer() + ren.SetBackground(colors.GetColor3d('ParaViewBkg')) + ren_win = vtkRenderWindow() + ren_win.SetSize(640, 480) + ren_win.SetWindowName('ColorMapToLUT') + ren_win.AddRenderer(ren) + iren = vtkRenderWindowInteractor() + iren.SetRenderWindow(ren_win) + + style = vtkInteractorStyleTrackballCamera() + iren.SetInteractorStyle(style) + + sphere = vtkSphereSource() + sphere.SetThetaResolution(64) + sphere.SetPhiResolution(32) + + cone = vtkConeSource() + cone.SetResolution(6) + cone.SetDirection(0, 1, 0) + cone.SetHeight(1) + cone.Update() + bounds = cone.GetOutput().GetBounds() + + elevation_filter = vtkElevationFilter() + elevation_filter.SetLowPoint(0, bounds[2], 0) + elevation_filter.SetHighPoint(0, bounds[3], 0) + elevation_filter.SetInputConnection(cone.GetOutputPort()) + # elevation_filter.SetInputConnection(sphere.GetOutputPort()) + + ctf = get_ctf() + + mapper = vtkPolyDataMapper() + mapper.SetInputConnection(elevation_filter.GetOutputPort()) + mapper.SetLookupTable(ctf) + mapper.SetColorModeToMapScalars() + mapper.InterpolateScalarsBeforeMappingOn() + + actor = vtkActor() + actor.SetMapper(mapper) + + ren.AddActor(actor) + + ren_win.Render() + iren.Start() + + +def get_ctf(): + # name: Fast, creator: Francesca Samsel, file name: Fast.xml + ctf = vtkDiscretizableColorTransferFunction() + + ctf.SetColorSpaceToLab() + ctf.SetScaleToLinear() + + ctf.SetNanColor(0, 0, 0) + + ctf.AddRGBPoint(0, 0.08800000000000002, 0.18810000000000007, 0.55) + ctf.AddRGBPoint(0.16144, 0.21989453864645603, 0.5170512023315895, 0.7093372214401806) + ctf.AddRGBPoint(0.351671, 0.5048913252297864, 0.8647869538833338, 0.870502284878942) + ctf.AddRGBPoint(0.501285, 1, 1, 0.83) + ctf.AddRGBPoint(0.620051, 0.9418960444346476, 0.891455547964053, 0.5446035798119958) + ctf.AddRGBPoint(0.835408342528245, 0.75, 0.44475, 0.255) + ctf.AddRGBPoint(1, 0.56, 0.055999999999999994, 0.055999999999999994) + + ctf.SetNumberOfValues(7) + ctf.DiscretizeOn() + + return ctf + + +if __name__ == '__main__': + main() diff --git a/src/Python/Utilities/ColorMapToLUT_JSON.md b/src/Python/Utilities/ColorMapToLUT_JSON.md new file mode 100644 index 0000000000000000000000000000000000000000..08a0cb41155964f4b9af33717daba659a1a19ff2 --- /dev/null +++ b/src/Python/Utilities/ColorMapToLUT_JSON.md @@ -0,0 +1,16 @@ +### Description + +Generate a VTK colormap from an ParaView JSON description of a colormap. + +A cone is rendered to demonstrate the resultant colormap. C++ and Python functions can also be generated which implement the colormap. These can be copied into [ColorMapToLUT.cxx]() or [ColorMapToLUT.py]() or into your own code. + +This program was inspired by this discussion: [Replacement default color map and background palette](https://discourse.paraview.org/t/replacement-default-color-map-and-background-palette/12712), the **Fast** colormap from this discussion is used as test data here. + +A good initial source for color maps is: [SciVisColor](https://sciviscolor.org/) -- this will provide you with plenty of XML examples. + +Further information: + +- [VTK Examples - Some ColorMap to LookupTable tools]() +- [How to export ParaView colormap into a format that could be read by matplotlib](https://discourse.paraview.org/t/how-to-export-paraview-colormap-into-a-format-that-could-be-read-by-matplotlib/2436) +- [How to export ParaView colormap into a format that could be read by matplotlib?](https://discourse.paraview.org/t/how-to-export-paraview-colormap-into-a-format-that-could-be-read-by-matplotlib/2394) +- [Color map advice and resources](https://discourse.paraview.org/t/color-map-advice-and-resources/6452/4) diff --git a/src/Python/Utilities/ColorMapToLUT_JSON.py b/src/Python/Utilities/ColorMapToLUT_JSON.py new file mode 100755 index 0000000000000000000000000000000000000000..2bc548e877b4e77516fe4d31178377c1436c5aca --- /dev/null +++ b/src/Python/Utilities/ColorMapToLUT_JSON.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 + +import json +import sys +from pathlib import Path + +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingOpenGL2 +from vtkmodules.vtkCommonColor import vtkNamedColors +from vtkmodules.vtkFiltersCore import vtkElevationFilter +from vtkmodules.vtkFiltersSources import vtkConeSource, vtkSphereSource +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderWindow, + vtkRenderWindowInteractor, + vtkRenderer +) +from vtkmodules.vtkRenderingCore import ( + vtkDiscretizableColorTransferFunction, +) + + +def get_program_parameters(argv): + import argparse + description = 'Take a JSON description of a colormap and convert it to a VTK colormap.' + epilogue = ''' + A color transfer function in C++ or Python can be optionally generated. + ''' + parser = argparse.ArgumentParser(description=description, epilog=epilogue, + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument('file_name', help='The path to the JSONL file e.g Fast.xml.') + parser.add_argument('-d', action='store_true', dest='discretize', help='Discretize the colormap.') + parser.add_argument('-n', dest='table_size', default=None, type=int, + help='Specify the size of the colormap.') + parser.add_argument('-g', dest='generate_function', default=None, + help='Generate code for the color transfer function,' + ' specify the desired language one of: Cxx, Python.') + + args = parser.parse_args() + return args.file_name, args.discretize, args.table_size, args.generate_function + + +def main(file_name, discretize, table_size, generate_function): + if file_name: + fn_path = Path(file_name) + if not fn_path.suffix: + fn_path = fn_path.with_suffix(".json") + if not fn_path.is_file(): + print('Unable to find: ', fn_path) + return + else: + print('Please enter a path to the JSON file.') + return + parameters = parse_json(fn_path) + # Do some checks. + if len(parameters['data_values']) != len(parameters['color_values']): + sys.exit('The data values length must be the same as colors.') + if len(parameters['opacity_values']) > 0: + if len(parameters['opacity_values']) != len(parameters['color_values']): + sys.exit('The opacity values length must be the same as colors.') + + if generate_function is not None: + generate_function = generate_function.lower() + available_languages = {k.lower(): k for k in ['Cxx', 'Python']} + available_languages.update({'cpp': 'Cxx', 'c++': 'Cxx'}) + if generate_function not in available_languages: + print(f'The language: {generate_function} is not available.') + tmp = ', '.join(sorted([lang for lang in set(available_languages.values())])) + print(f'Choose one of these: {tmp}.') + return + else: + language = available_languages[generate_function] + else: + language = None + + ctf = make_ctf(parameters, discretize, table_size) + if language is not None and language in ['Cxx', 'Python']: + if language == 'Python': + generate_ctf_python(parameters, discretize, table_size) + else: + generate_ctf_cpp(parameters, discretize, table_size) + + colors = vtkNamedColors() + colors.SetColor('ParaViewBkg', 82, 87, 110, 255) + + sphere = vtkSphereSource() + sphere.SetThetaResolution(64) + sphere.SetPhiResolution(32) + + cone = vtkConeSource() + cone.SetResolution(6) + cone.SetDirection(0, 1, 0) + cone.SetHeight(1) + cone.Update() + bounds = cone.GetOutput().GetBounds() + + elevation_filter = vtkElevationFilter() + elevation_filter.SetLowPoint(0, bounds[2], 0) + elevation_filter.SetHighPoint(0, bounds[3], 0) + elevation_filter.SetInputConnection(cone.GetOutputPort()) + # elevation_filter.SetInputConnection(sphere.GetOutputPort()) + + mapper = vtkPolyDataMapper() + mapper.SetInputConnection(elevation_filter.GetOutputPort()) + mapper.SetLookupTable(ctf) + mapper.SetColorModeToMapScalars() + mapper.InterpolateScalarsBeforeMappingOn() + + actor = vtkActor() + actor.SetMapper(mapper) + # actor.GetProperty().SetDiffuseColor(colors.GetColor3d('bisque')) + + # Visualize + ren = vtkRenderer() + ren_win = vtkRenderWindow() + ren_win.AddRenderer(ren) + iren = vtkRenderWindowInteractor() + iren.SetRenderWindow(ren_win) + + style = vtkInteractorStyleTrackballCamera() + iren.SetInteractorStyle(style) + + ren.AddActor(actor) + ren.SetBackground(colors.GetColor3d('ParaViewBkg')) + + ren_win.SetSize(640, 480) + ren_win.SetWindowName('ColorMapToLUT_JSON') + + ren_win.Render() + iren.Start() + + +def parse_json(fn_path): + """ + Parse the exported ParaView JSON file of a colormap. + :param fn_path: The path to the JSON file. + :return: The parameters for the color map. + """ + with open(fn_path) as data_file: + json_data = json.load(data_file) + data_values = list() + color_values = list() + opacity_values = list() + color_map_details = dict() + nan = None + above = None + below = None + for k, v in json_data[0].items(): + if 'Points' in k: + n = 4 + data_color = [v[i * n:(i + 1) * n] for i in range((len(v) + n - 1) // n)] + for dc in data_color: + if len(dc) == 4: + data_values.append(dc[0]) + color_values.append(tuple(dc[1:])) + if k == 'ColorSpace': + color_map_details['space'] = v + if k == 'Creator': + color_map_details['creator'] = v + if k == 'Name': + color_map_details['name'] = v + if k == 'NanColor': + nan = tuple(v[0:3]) + return {'path': fn_path.name, 'color_map_details': color_map_details, 'data_values': data_values, + 'color_values': color_values, 'opacity_values': opacity_values, 'NaN': nan, 'Above': above, 'Below': below} + + +def make_ctf(parameters, discretize, table_size=None): + """ + Generate the discretizable color transfer function + :param parameters: The parameters. + :param discretize: True if the values are to be mapped after discretization. + :param table_size: The table size. + :return: The discretizable color transfer function. + """ + + ctf = vtkDiscretizableColorTransferFunction() + + interp_space = parameters['color_map_details'].get('interpolationspace', None) + if interp_space: + interp_space = interp_space.lower() + if interp_space == 'hsv': + ctf.SetColorSpaceToHSV() + elif interp_space == 'lab': + ctf.SetColorSpaceToLab() + elif interp_space == 'ciede2000': + ctf.SetColorSpaceToLabCIEDE2000() + elif interp_space == 'diverging': + ctf.SetColorSpaceToDiverging() + elif interp_space == 'step': + ctf.SetColorSpaceToStep() + else: + ctf.SetColorSpaceToRGB() + else: + ctf.SetColorSpaceToRGB() + + scale = parameters['color_map_details'].get('interpolationtype', None) + if scale: + scale = scale.lower() + if scale == 'log10': + ctf.SetScaleToLog10() + else: + ctf.SetScaleToLinear() + else: + ctf.SetScaleToLinear() + + if parameters['NaN'] is not None: + ctf.SetNanColor(*parameters['NaN']) + + if parameters['Above'] is not None: + ctf.SetAboveRangeColor(*parameters['Above']) + ctf.UseAboveRangeColorOn() + + if parameters['Below'] is not None: + ctf.SetBelowRangeColor(*parameters['Below']) + ctf.UseBelowRangeColorOn() + + space = parameters['color_map_details'].get('space', None) + if space: + space = space.lower() + for i in range(0, len(parameters['data_values'])): + idx = parameters['data_values'][i] + color = parameters['color_values'][i] + if space == 'hsv': + ctf.AddHSVPoint(idx, *color) + else: + ctf.AddRGBPoint(idx, *color) + + if table_size is not None: + ctf.SetNumberOfValues(table_size) + else: + ctf.SetNumberOfValues(len(parameters["data_values"])) + + if discretize: + ctf.DiscretizeOn() + else: + ctf.DiscretizeOff() + + return ctf + + +def generate_ctf_python(parameters, discretize, table_size=None): + """ + Generate a function do the ctf. + + :param parameters: The parameters. + :param discretize: True if the values are to be mapped after discretization. + :param table_size: The table size. + :return: The discretizable color transfer function. + """ + indent = ' ' * 4 + + comment = f'{indent}#' + if 'name' in parameters['color_map_details']: + comment += f' name: {parameters["color_map_details"]["name"]},' + if 'creator' in parameters['color_map_details']: + comment += f' creator: {parameters["color_map_details"]["creator"]},' + comment += f' file name: {parameters["path"]}' + + s = ['', f'def get_ctf():', comment, f'{indent}ctf = vtkDiscretizableColorTransferFunction()', ''] + + interp_space = parameters['color_map_details'].get('interpolationspace', None) + if interp_space: + interp_space = interp_space.lower() + if interp_space == 'hsv': + s.append(f'{indent}ctf.SetColorSpaceToHSV()') + elif interp_space == 'lab': + s.append(f'{indent}ctf.SetColorSpaceToLab()') + elif interp_space == 'ciede2000': + s.append(f'{indent}ctf.SetColorSpaceToLabCIEDE2000()') + elif interp_space == 'diverging': + s.append(f'{indent}ctf.SetColorSpaceToDiverging()') + elif interp_space == 'step': + s.append(f'{indent}ctf.SetColorSpaceToStep()') + else: + s.append(f'{indent}ctf.SetColorSpaceToRGB()') + else: + s.append(f'{indent}ctf.SetColorSpaceToRGB()') + + scale = parameters['color_map_details'].get('interpolationtype', None) + if scale: + scale = scale.lower() + if scale == 'log10': + s.append(f'{indent}ctf.SetScaleToLog10()') + else: + s.append(f'{indent}ctf.SetScaleToLinear()') + else: + s.append(f'{indent}ctf.SetScaleToLinear()') + s.append('') + + if parameters['NaN'] is not None: + color = ', '.join(list(map(str, parameters['NaN']))) + s.append(f'{indent}ctf.SetNanColor({color})') + + if parameters['Above'] is not None: + color = ', '.join(list(map(str, parameters['Above']))) + s.append(f'{indent}ctf.SetAboveRangeColor({color})') + s.append(f'{indent}ctf.UseAboveRangeColorOn()') + + if parameters['Below'] is not None: + color = ', '.join(list(map(str, parameters['Below']))) + s.append(f'{indent}ctf.SetBelowRangeColor({color})') + s.append(f'{indent}ctf.UseBelowRangeColorOn()') + s.append('') + + space = parameters['color_map_details'].get('space', None) + if space: + space = space.lower() + for i in range(0, len(parameters['data_values'])): + color = ', '.join(list(map(str, parameters['color_values'][i]))) + idx = parameters['data_values'][i] + if space == 'hsv': + s.append(f'{indent}ctf.AddHSVPoint({idx}, {color})') + else: + s.append(f'{indent}ctf.AddRGBPoint({idx}, {color})') + s.append('') + + if table_size is not None: + s.append(f'{indent}ctf.SetNumberOfValues({table_size})') + else: + s.append(f'{indent}ctf.SetNumberOfValues({len(parameters["data_values"])})') + + if discretize: + s.append(f'{indent}ctf.DiscretizeOn()') + else: + s.append(f'{indent}ctf.DiscretizeOff()') + s.append('') + + s.append(f'{indent}return ctf') + s.append('') + + print('\n'.join(s)) + + +def generate_ctf_cpp(parameters, discretize, table_size=None): + """ + Generate a function do the ctf. + + :param parameters: The parameters. + :param discretize: True if the values are to be mapped after discretization. + :param table_size: The table size. + :return: The discretizable color transfer function. + """ + indent = ' ' * 2 + + comment = f'{indent}//' + if 'name' in parameters['color_map_details']: + comment += f' name: {parameters["color_map_details"]["name"]},' + if 'creator' in parameters['color_map_details']: + comment += f' creator: {parameters["color_map_details"]["creator"]},' + comment += f' file name: {parameters["path"]}' + + s = ['', f'vtkNew<vtkDiscretizableColorTransferFunction> getCTF()', '{', comment, + f'{indent}vtkNew<vtkDiscretizableColorTransferFunction> ctf;', ''] + + interp_space = parameters['color_map_details'].get('interpolationspace', None) + if interp_space: + interp_space = interp_space.lower() + if interp_space == 'hsv': + s.append(f'{indent}ctf->SetColorSpaceToHSV();') + elif interp_space == 'lab': + s.append(f'{indent}ctf->SetColorSpaceToLab();') + elif interp_space == 'ciede2000': + s.append(f'{indent}ctf->SetColorSpaceToLabCIEDE2000();') + elif interp_space == 'diverging': + s.append(f'{indent}ctf->SetColorSpaceToDiverging();') + elif interp_space == 'step': + s.append(f'{indent}ctf->SetColorSpaceToStep();') + else: + s.append(f'{indent}ctf->SetColorSpaceToRGB();') + else: + s.append(f'{indent}ctf->SetColorSpaceToRGB();') + + scale = parameters['color_map_details'].get('interpolationtype', None) + if scale: + scale = scale.lower() + if scale == 'log10': + s.append(f'{indent}ctf->SetScaleToLog10();') + else: + s.append(f'{indent}ctf->SetScaleToLinear();') + else: + s.append(f'{indent}ctf->SetScaleToLinear();') + s.append('') + + if parameters['NaN'] is not None: + color = ', '.join(list(map(str, parameters['NaN']))) + s.append(f'{indent}ctf->SetNanColor({color});') + + if parameters['Above'] is not None: + color = ', '.join(list(map(str, parameters['Above']))) + s.append(f'{indent}ctf->SetAboveRangeColor({color});') + s.append(f'{indent}ctf->UseAboveRangeColorOn();') + + if parameters['Below'] is not None: + color = ', '.join(list(map(str, parameters['Below']))) + s.append(f'{indent}ctf->SetBelowRangeColor({color});') + s.append(f'{indent}ctf->UseBelowRangeColorOn();') + s.append('') + + space = parameters['color_map_details'].get('space', None) + if space: + space = space.lower() + for i in range(0, len(parameters['data_values'])): + color = ', '.join(list(map(str, parameters['color_values'][i]))) + idx = parameters['data_values'][i] + if space == 'hsv': + s.append(f'{indent}ctf->AddHSVPoint({idx}, {color});') + else: + s.append(f'{indent}ctf->AddRGBPoint({idx}, {color});') + s.append('') + + if table_size is not None: + s.append(f'{indent}ctf->SetNumberOfValues({table_size});') + else: + s.append(f'{indent}ctf->SetNumberOfValues({len(parameters["data_values"])});') + + if discretize: + s.append(f'{indent}ctf->DiscretizeOn();') + else: + s.append(f'{indent}ctf->DiscretizeOff();') + s.append('') + + s.append(f'{indent}return ctf;') + s.append('}') + s.append('') + + print('\n'.join(s)) + + +if __name__ == '__main__': + file, discretise, size, generate = get_program_parameters(sys.argv) + main(file, discretise, size, generate) diff --git a/src/Python/Utilities/ColorMapToLUT_XML.md b/src/Python/Utilities/ColorMapToLUT_XML.md new file mode 100644 index 0000000000000000000000000000000000000000..fae07b138a004f0338ebb9160c2c340468aec5af --- /dev/null +++ b/src/Python/Utilities/ColorMapToLUT_XML.md @@ -0,0 +1,21 @@ +### Description + +Generate a VTK colormap from an XML description of a colormap. + +A cone is rendered to demonstrate the resultant colormap. C++ and Python functions can also be generated which implement the colormap. These can be copied into [ColorMapToLUT.cxx]() or [ColorMapToLUT.py]() or into your own code. + +This program was inspired by this discussion: [Replacement default color map and background palette](https://discourse.paraview.org/t/replacement-default-color-map-and-background-palette/12712), and, the **Fast** colormap from this discussion is used as test data here. + +A good initial source for color maps is: [SciVisColor](https://sciviscolor.org/) -- this will provide you with plenty of XML examples. + +**Note:** + +- The XML parser is [lxml](https://lxml.de/) +- Currently the parsing only works for colormaps with no Section key. + +Further information: + +- [VTK Examples - Some ColorMap to LookupTable tools]() +- [How to export ParaView colormap into a format that could be read by matplotlib](https://discourse.paraview.org/t/how-to-export-paraview-colormap-into-a-format-that-could-be-read-by-matplotlib/2436) +- [How to export ParaView colormap into a format that could be read by matplotlib?](https://discourse.paraview.org/t/how-to-export-paraview-colormap-into-a-format-that-could-be-read-by-matplotlib/2394) +- [Color map advice and resources](https://discourse.paraview.org/t/color-map-advice-and-resources/6452/4) diff --git a/src/Python/Utilities/ColorMapToLUT_XML.py b/src/Python/Utilities/ColorMapToLUT_XML.py new file mode 100755 index 0000000000000000000000000000000000000000..f0bf47fe83f1b0e08b04736732dce91f0b45b242 --- /dev/null +++ b/src/Python/Utilities/ColorMapToLUT_XML.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 + +import sys +from pathlib import Path + +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingOpenGL2 +from lxml import etree +from vtkmodules.vtkCommonColor import vtkNamedColors +from vtkmodules.vtkFiltersCore import vtkElevationFilter +from vtkmodules.vtkFiltersSources import vtkConeSource, vtkSphereSource +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderWindow, + vtkRenderWindowInteractor, + vtkRenderer +) +from vtkmodules.vtkRenderingCore import ( + vtkDiscretizableColorTransferFunction, +) + + +def get_program_parameters(argv): + import argparse + description = 'Take an XML description of a colormap and convert it to a VTK colormap.' + epilogue = ''' + A color transfer function in C++ or Python can be optionally generated. + ''' + parser = argparse.ArgumentParser(description=description, epilog=epilogue, + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument('file_name', help='The path to the XML file e.g Fast.xml.') + parser.add_argument('-d', action='store_true', dest='discretize', help='Discretize the colormap.') + parser.add_argument('-n', dest='table_size', default=None, type=int, + help='Specify the size of the colormap.') + parser.add_argument('-g', dest='generate_function', default=None, + help='Generate code for the color transfer function,' + ' specify the desired language one of: Cxx, Python.') + + args = parser.parse_args() + return args.file_name, args.discretize, args.table_size, args.generate_function + + +def main(file_name, discretize, table_size, generate_function): + if file_name: + fn_path = Path(file_name) + if not fn_path.suffix: + fn_path = fn_path.with_suffix(".xml") + if not fn_path.is_file(): + print('Unable to find: ', fn_path) + return + else: + print('Please enter a path to the XML file.') + return + parameters = parse_xml(fn_path) + # Do some checks. + if len(parameters['data_values']) != len(parameters['color_values']): + sys.exit('The data values length must be the same as colors.') + if parameters['opacity_values'] is not None: + if len(parameters['opacity_values']) != len(parameters['color_values']): + sys.exit('The opacity values length must be the same as colors.') + + if generate_function is not None: + generate_function = generate_function.lower() + available_languages = {k.lower(): k for k in ['Cxx', 'Python']} + available_languages.update({'cpp': 'Cxx', 'c++': 'Cxx'}) + if generate_function not in available_languages: + print(f'The language: {generate_function} is not available.') + tmp = ', '.join(sorted([lang for lang in set(available_languages.values())])) + print(f'Choose one of these: {tmp}.') + return + else: + language = available_languages[generate_function] + else: + language = None + + ctf = make_ctf(parameters, discretize, table_size) + if language is not None and language in ['Cxx', 'Python']: + if language == 'Python': + generate_ctf_python(parameters, discretize, table_size) + else: + generate_ctf_cpp(parameters, discretize, table_size) + + colors = vtkNamedColors() + colors.SetColor('ParaViewBkg', 82, 87, 110, 255) + + sphere = vtkSphereSource() + sphere.SetThetaResolution(64) + sphere.SetPhiResolution(32) + + cone = vtkConeSource() + cone.SetResolution(6) + cone.SetDirection(0, 1, 0) + cone.SetHeight(1) + cone.Update() + bounds = cone.GetOutput().GetBounds() + + elevation_filter = vtkElevationFilter() + elevation_filter.SetLowPoint(0, bounds[2], 0) + elevation_filter.SetHighPoint(0, bounds[3], 0) + elevation_filter.SetInputConnection(cone.GetOutputPort()) + # elevation_filter.SetInputConnection(sphere.GetOutputPort()) + + mapper = vtkPolyDataMapper() + mapper.SetInputConnection(elevation_filter.GetOutputPort()) + mapper.SetLookupTable(ctf) + mapper.SetColorModeToMapScalars() + mapper.InterpolateScalarsBeforeMappingOn() + + actor = vtkActor() + actor.SetMapper(mapper) + # actor.GetProperty().SetDiffuseColor(colors.GetColor3d('bisque')) + + # Visualize + ren = vtkRenderer() + ren_win = vtkRenderWindow() + ren_win.AddRenderer(ren) + iren = vtkRenderWindowInteractor() + iren.SetRenderWindow(ren_win) + + style = vtkInteractorStyleTrackballCamera() + iren.SetInteractorStyle(style) + + ren.AddActor(actor) + ren.SetBackground(colors.GetColor3d('ParaViewBkg')) + + ren_win.SetSize(640, 480) + ren_win.SetWindowName('ColorMapToLUT_XML') + + ren_win.Render() + iren.Start() + + +def parse_xml(fn_path): + """ + Parse the XML file of a colormap. + + Check out: https://sciviscolor.org/colormaps/ for some good XML files. + :param fn_path: The path to the XML file. + :return: The parameters for the color map. + """ + with open(fn_path) as data_file: + xml_doc = etree.parse(data_file) + data_values = list() + color_values = list() + opacity_values = list() + nan = None + above = None + below = None + + s = xml_doc.getroot().find('ColorMap') + if s is not None: + color_map_details = dict(s.attrib) + else: + color_map_details = None + sys.exit('The attribute "ColorMap" is not found.') + for s in xml_doc.getroot().findall('.//Point'): + data_values.append(s.attrib['x']) + color_values.append((s.attrib['r'], s.attrib['g'], s.attrib['b'])) + opacity_values.append(s.attrib['o']) + s = xml_doc.getroot().find('.//NaN') + if s is not None: + nan = (s.attrib['r'], s.attrib['g'], s.attrib['b']) + s = xml_doc.getroot().find('.//Above') + if s is not None: + above = (s.attrib['r'], s.attrib['g'], s.attrib['b']) + s = xml_doc.getroot().find('.//Below') + if s is not None: + below = (s.attrib['r'], s.attrib['g'], s.attrib['b']) + return {'path': fn_path.name, 'color_map_details': color_map_details, 'data_values': data_values, + 'color_values': color_values, 'opacity_values': opacity_values, 'NaN': nan, 'Above': above, 'Below': below} + + +def make_ctf(parameters, discretize, table_size=None): + """ + Generate the discretizable color transfer function + :param parameters: The parameters. + :param discretize: True if the values are to be mapped after discretization. + :param table_size: The table size. + :return: The discretizable color transfer function. + """ + + ctf = vtkDiscretizableColorTransferFunction() + + interp_space = parameters['color_map_details'].get('interpolationspace', None) + if interp_space: + interp_space = interp_space.lower() + if interp_space == 'hsv': + ctf.SetColorSpaceToHSV() + elif interp_space == 'lab': + ctf.SetColorSpaceToLab() + elif interp_space == 'ciede2000': + ctf.SetColorSpaceToLabCIEDE2000() + elif interp_space == 'diverging': + ctf.SetColorSpaceToDiverging() + elif interp_space == 'step': + ctf.SetColorSpaceToStep() + else: + ctf.SetColorSpaceToRGB() + else: + ctf.SetColorSpaceToRGB() + + scale = parameters['color_map_details'].get('interpolationtype', None) + if scale: + scale = scale.lower() + if scale == 'log10': + ctf.SetScaleToLog10() + else: + ctf.SetScaleToLinear() + else: + ctf.SetScaleToLinear() + + if parameters['NaN'] is not None: + color = list(map(float, parameters['NaN'])) + ctf.SetNanColor(*color) + + if parameters['Above'] is not None: + color = list(map(float, parameters['Above'])) + ctf.SetAboveRangeColor(*color) + ctf.UseAboveRangeColorOn() + + if parameters['Below'] is not None: + color = list(map(float, parameters['Below'])) + ctf.SetBelowRangeColor(*color) + ctf.UseBelowRangeColorOn() + + space = parameters['color_map_details'].get('space', None) + if space: + space = space.lower() + for i in range(0, len(parameters['data_values'])): + color = list(map(float, parameters['color_values'][i])) + idx = float(parameters['data_values'][i]) + if space == 'hsv': + ctf.AddHSVPoint(idx, *color) + else: + ctf.AddRGBPoint(idx, *color) + + if table_size is not None: + ctf.SetNumberOfValues(table_size) + else: + ctf.SetNumberOfValues(len(parameters["data_values"])) + + if discretize: + ctf.DiscretizeOn() + else: + ctf.DiscretizeOff() + + return ctf + + +def generate_ctf_python(parameters, discretize, table_size=None): + """ + Generate a function do the ctf. + + :param parameters: The parameters. + :param discretize: True if the values are to be mapped after discretization. + :param table_size: The table size. + :return: The discretizable color transfer function. + """ + indent = ' ' * 4 + + comment = f'{indent}#' + if 'name' in parameters['color_map_details']: + comment += f' name: {parameters["color_map_details"]["name"]},' + if 'creator' in parameters['color_map_details']: + comment += f' creator: {parameters["color_map_details"]["creator"]},' + comment += f' file name: {parameters["path"]}' + + s = ['', f'def get_ctf():', comment, f'{indent}ctf = vtkDiscretizableColorTransferFunction()', ''] + + interp_space = parameters['color_map_details'].get('interpolationspace', None) + if interp_space: + interp_space = interp_space.lower() + if interp_space == 'hsv': + s.append(f'{indent}ctf.SetColorSpaceToHSV()') + elif interp_space == 'lab': + s.append(f'{indent}ctf.SetColorSpaceToLab()') + elif interp_space == 'ciede2000': + s.append(f'{indent}ctf.SetColorSpaceToLabCIEDE2000()') + elif interp_space == 'diverging': + s.append(f'{indent}ctf.SetColorSpaceToDiverging()') + elif interp_space == 'step': + s.append(f'{indent}ctf.SetColorSpaceToStep()') + else: + s.append(f'{indent}ctf.SetColorSpaceToRGB()') + else: + s.append(f'{indent}ctf.SetColorSpaceToRGB()') + + scale = parameters['color_map_details'].get('interpolationtype', None) + if scale: + scale = scale.lower() + if scale == 'log10': + s.append(f'{indent}ctf.SetScaleToLog10()') + else: + s.append(f'{indent}ctf.SetScaleToLinear()') + else: + s.append(f'{indent}ctf.SetScaleToLinear()') + s.append('') + + if parameters['NaN'] is not None: + color = ', '.join(parameters['NaN']) + s.append(f'{indent}ctf.SetNanColor({color})') + + if parameters['Above'] is not None: + color = ', '.join(parameters['Above']) + s.append(f'{indent}ctf.SetAboveRangeColor({color})') + s.append(f'{indent}ctf.UseAboveRangeColorOn()') + + if parameters['Below'] is not None: + color = ', '.join(parameters['Below']) + s.append(f'{indent}ctf.SetBelowRangeColor({color})') + s.append(f'{indent}ctf.UseBelowRangeColorOn()') + s.append('') + + space = parameters['color_map_details'].get('space', None) + if space: + space = space.lower() + for i in range(0, len(parameters['data_values'])): + color = ', '.join(parameters['color_values'][i]) + idx = parameters['data_values'][i] + if space == 'hsv': + s.append(f'{indent}ctf.AddHSVPoint({idx}, {color})') + else: + s.append(f'{indent}ctf.AddRGBPoint({idx}, {color})') + s.append('') + + if table_size is not None: + s.append(f'{indent}ctf.SetNumberOfValues({table_size})') + else: + s.append(f'{indent}ctf.SetNumberOfValues({len(parameters["data_values"])})') + + if discretize: + s.append(f'{indent}ctf.DiscretizeOn()') + else: + s.append(f'{indent}ctf.DiscretizeOff()') + s.append('') + + s.append(f'{indent}return ctf') + s.append('') + + print('\n'.join(s)) + + +def generate_ctf_cpp(parameters, discretize, table_size=None): + """ + Generate a function do the ctf. + + :param parameters: The parameters. + :param discretize: True if the values are to be mapped after discretization. + :param table_size: The table size. + :return: The discretizable color transfer function. + """ + indent = ' ' * 2 + + comment = f'{indent}//' + if 'name' in parameters['color_map_details']: + comment += f' name: {parameters["color_map_details"]["name"]},' + if 'creator' in parameters['color_map_details']: + comment += f' creator: {parameters["color_map_details"]["creator"]},' + comment += f' file name: {parameters["path"]}' + + s = ['', f'vtkNew<vtkDiscretizableColorTransferFunction> getCTF()', '{', comment, + f'{indent}vtkNew<vtkDiscretizableColorTransferFunction> ctf;', ''] + + interp_space = parameters['color_map_details'].get('interpolationspace', None) + if interp_space: + interp_space = interp_space.lower() + if interp_space == 'hsv': + s.append(f'{indent}ctf->SetColorSpaceToHSV();') + elif interp_space == 'lab': + s.append(f'{indent}ctf->SetColorSpaceToLab();') + elif interp_space == 'ciede2000': + s.append(f'{indent}ctf->SetColorSpaceToLabCIEDE2000();') + elif interp_space == 'diverging': + s.append(f'{indent}ctf->SetColorSpaceToDiverging();') + elif interp_space == 'step': + s.append(f'{indent}ctf->SetColorSpaceToStep();') + else: + s.append(f'{indent}ctf->SetColorSpaceToRGB();') + else: + s.append(f'{indent}ctf->SetColorSpaceToRGB();') + + scale = parameters['color_map_details'].get('interpolationtype', None) + if scale: + scale = scale.lower() + if scale == 'log10': + s.append(f'{indent}ctf->SetScaleToLog10();') + else: + s.append(f'{indent}ctf->SetScaleToLinear();') + else: + s.append(f'{indent}ctf->SetScaleToLinear();') + s.append('') + + if parameters['NaN'] is not None: + color = ', '.join(parameters['NaN']) + s.append(f'{indent}ctf->SetNanColor({color});') + + if parameters['Above'] is not None: + color = ', '.join(parameters['Above']) + s.append(f'{indent}ctf->SetAboveRangeColor({color});') + s.append(f'{indent}ctf->UseAboveRangeColorOn();') + + if parameters['Below'] is not None: + color = ', '.join(parameters['Below']) + s.append(f'{indent}ctf->SetBelowRangeColor({color});') + s.append(f'{indent}ctf->UseBelowRangeColorOn();') + s.append('') + + space = parameters['color_map_details'].get('space', None) + if space: + space = space.lower() + for i in range(0, len(parameters['data_values'])): + color = ', '.join(parameters['color_values'][i]) + idx = parameters['data_values'][i] + if space == 'hsv': + s.append(f'{indent}ctf->AddHSVPoint({idx}, {color});') + else: + s.append(f'{indent}ctf->AddRGBPoint({idx}, {color});') + s.append('') + + if table_size is not None: + s.append(f'{indent}ctf->SetNumberOfValues({table_size});') + else: + s.append(f'{indent}ctf->SetNumberOfValues({len(parameters["data_values"])});') + + if discretize: + s.append(f'{indent}ctf->DiscretizeOn();') + else: + s.append(f'{indent}ctf->DiscretizeOff();') + s.append('') + + s.append(f'{indent}return ctf;') + s.append('}') + s.append('') + + print('\n'.join(s)) + + +if __name__ == '__main__': + file, discretise, size, generate = get_program_parameters(sys.argv) + main(file, discretise, size, generate) diff --git a/src/Testing/Baseline/Cxx/Utilities/TestColorMapToLUT.png b/src/Testing/Baseline/Cxx/Utilities/TestColorMapToLUT.png new file mode 100644 index 0000000000000000000000000000000000000000..055b93c5a6e64e306392b64d1160579e40cc0447 --- /dev/null +++ b/src/Testing/Baseline/Cxx/Utilities/TestColorMapToLUT.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4249f193946b010a87449b8594701ca350ce71515dd14eee6795fd1aeab5993a +size 17274 diff --git a/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT.png b/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT.png new file mode 100644 index 0000000000000000000000000000000000000000..055b93c5a6e64e306392b64d1160579e40cc0447 --- /dev/null +++ b/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4249f193946b010a87449b8594701ca350ce71515dd14eee6795fd1aeab5993a +size 17274 diff --git a/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT_JSON.png b/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT_JSON.png new file mode 100644 index 0000000000000000000000000000000000000000..055b93c5a6e64e306392b64d1160579e40cc0447 --- /dev/null +++ b/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT_JSON.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4249f193946b010a87449b8594701ca350ce71515dd14eee6795fd1aeab5993a +size 17274 diff --git a/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT_XML.png b/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT_XML.png new file mode 100644 index 0000000000000000000000000000000000000000..055b93c5a6e64e306392b64d1160579e40cc0447 --- /dev/null +++ b/src/Testing/Baseline/Python/Utilities/TestColorMapToLUT_XML.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4249f193946b010a87449b8594701ca350ce71515dd14eee6795fd1aeab5993a +size 17274 diff --git a/src/Testing/Data/Fast.json b/src/Testing/Data/Fast.json new file mode 100644 index 0000000000000000000000000000000000000000..74c42a77c2ed52e9211728e0b1b4d1afb15447b5 --- /dev/null +++ b/src/Testing/Data/Fast.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39274855486e9c407ddf42f386916ec0fdc38fa3e847008107833d9ef87ddf51 +size 729 diff --git a/src/Testing/Data/Fast.xml b/src/Testing/Data/Fast.xml new file mode 100644 index 0000000000000000000000000000000000000000..eda8879d54a0bbb5af94b767d391bd23d168e893 --- /dev/null +++ b/src/Testing/Data/Fast.xml @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78b3074991f0d47b6967466c089b54d516c82b0eefe2b57e13b1124d1915a6df +size 1019