AvizoCodeBook

Share Embed Donate


Short Description

Download AvizoCodeBook...

Description

Avizo Code Book

How to use Avizo efficiently and how to extend it This document is a contribution of Hauke Bartsch, Dr. rer. nat. Updated April 17, 2010

Contents

I.

Avizo basics

9

1. Introduction 2. Avizo modules 2.1. Select a gray value range and extract a 2.2. Volume rendering for RGB images . . 2.3. Analysing volumetric densities . . . . 2.4. Displaying segmentation results . . . . 2.5. Align two data sets using ObliqueSlice

11

sub-volume . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

II. Avizo scripting

13 13 14 15 15 16

19

3. Introduction 21 3.1. Remote controlling Avizo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.2. Displaying your logos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.3. Enable the reload button . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 4. Compute modules 4.1. A standard compute . . . . . . . . . . 4.2. A real example . . . . . . . . . . . . . 4.3. Multiple output objects of a script . . 4.4. Iterating over objects in the workpool 4.5. Annotations in 3D . . . . . . . . . . . 4.6. Transformations on Data Files . . . . 4.7. Refinement of surfaces . . . . . . . . . 4.8. Simplify a surface . . . . . . . . . . . . 4.9. Apply image filter . . . . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

25 25 25 28 30 30 32 33 35 35

5. File load and save 5.1. Write your own data sets . . . . . . . . 5.1.1. Write a SpreadSheet object . . . 5.2. Read your own data set . . . . . . . . . 5.2.1. Read a Surface from Trajectories 5.3. Plot some 1d curves . . . . . . . . . . . 5.4. Load data objects . . . . . . . . . . . . 5.4.1. Create a cluster object . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

45 45 46 47 49 52 53 58

5

Contents 5.4.2. Create a label field . . . . . . . . . . . . . . 5.4.3. Remove a slice from a loaded volume . . . . 5.4.4. Create a scalar field . . . . . . . . . . . . . 5.4.5. Create a line set . . . . . . . . . . . . . . . 5.4.6. Write a lineset out to the console window . 5.4.7. Add a data value to each point of a line set 5.5. Save data objects . . . . . . . . . . . . . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

59 60 61 62 66 67 69

6. General Information 6.1. Memory consumption of data and display . . . . . . . . . 6.2. Set the voxel size and the bounding box . . . . . . . . . . 6.3. Querying environment variables in Avizo . . . . . . . . . . 6.4. Start external processes . . . . . . . . . . . . . . . . . . . 6.5. Display a progress bar . . . . . . . . . . . . . . . . . . . . 6.6. Generate numbered file names . . . . . . . . . . . . . . . . 6.7. DICOM units . . . . . . . . . . . . . . . . . . . . . . . . . 6.8. DICOM scaling - HU values and how they are represented 6.9. Reading in DICOM directories without DICOMDIR . . . 6.10. Force a re-computation . . . . . . . . . . . . . . . . . . . . 6.11. Add information to data files . . . . . . . . . . . . . . . . 6.12. Add a button next to “Load Data” . . . . . . . . . . . . . 6.13. Add function keys . . . . . . . . . . . . . . . . . . . . . . 6.14. Announce new modules with .rc files . . . . . . . . . . . . 6.15. The current time . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

71 71 71 72 72 72 73 73 74 75 75 76 78 80 81 84

7. Editors 7.1. Invoke 7.2. Invoke 7.2.1. 7.3. Invoke 7.4. Invoke 7.5. Invoke 7.6. Invoke 7.7. Invoke 7.8. Invoke

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

85 85 85 86 87 87 88 88 88 89

the Crop Editor . . . . . . . . . the Segmentation Editor . . . . Automatic erosion and dilation the Parameter Editor . . . . . the Transform Editor . . . . . . the Camera Path Editor . . . . the Surface Editor . . . . . . . the slice aligner . . . . . . . . . the Landmark Editor . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . .

. . . . . . . . .

. . . . . . .

. . . . . . . . .

. . . . . . .

. . . . . . . . .

. . . . . . . . .

III. C++ Code examples 8. General Information 8.1. First steps . . . . . . . 8.2. Compiling a package . 8.3. Display a progress bar 8.4. Accessing environment 8.5. Display the file dialog

6

91 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . variables and the . . . . . . . . . .

. . . . . . . . . . . . . . . registry . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

93 93 93 94 94 95

Contents 8.6. Accessing other modules . . . . . . . . . . . . . . 8.6.1. Timing modules . . . . . . . . . . . . . . 8.6.2. Create an existing module . . . . . . . . . 8.6.3. Working with ports . . . . . . . . . . . . 8.7. Textual input and output . . . . . . . . . . . . . 8.8. Debug messages (enable/disable) . . . . . . . . . 8.9. Executing Tcl commands . . . . . . . . . . . . . 8.10. Change the voxel size . . . . . . . . . . . . . . . 8.11. The camera position . . . . . . . . . . . . . . . . 8.12. Updating the viewer . . . . . . . . . . . . . . . . 8.13. Multi-threading . . . . . . . . . . . . . . . . . . . 8.14. LargeDiskdata . . . . . . . . . . . . . . . . . . . 8.15. Generating warning messages and simple dialogs 8.16. Adding additional information to fields . . . . . . 8.17. Utility functions . . . . . . . . . . . . . . . . . . 8.18. Utilizing Analytical Expressions . . . . . . . . . . 8.19. Documentation . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

97 97 98 98 101 101 102 102 103 104 104 110 111 112 112 113 114

9. Compute modules 9.1. Adding another field as input port . . . . . . . . 9.1.1. Correct behaviour for saving networks . . 9.1.2. Adding an almost infinite number of ports 9.2. Adding a region of interest input . . . . . . . . . 9.3. User defined tcl commands – the parse function . 9.4. Save a network file . . . . . . . . . . . . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

115 117 119 119 121 121 122

. . . . . . . . . .

125 . 125 . 126 . 126 . 128 . 128 . 130 . 134 . 140 . 141 . 143

10.Data import and export 10.1. AmiraMesh as a general purpose file format 10.2. Read data sets . . . . . . . . . . . . . . . . 10.2.1. Formating your own data . . . . . . 10.2.2. Convert binary to ascii data . . . . . 10.2.3. Read in Curvilinear Fields . . . . . . 10.2.4. Work with offline data . . . . . . . . 10.2.5. Read in time dependent data . . . . 10.3. Multi-channel fields . . . . . . . . . . . . . . 10.4. Surface data . . . . . . . . . . . . . . . . . . 10.4.1. Write a surface . . . . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

11.LabelFields 149 11.1. Exporting LabelFields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 12.LineSets 151 12.1. Reading a line set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 12.2. Drawing a line set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153

7

Contents 13.Field access 155 13.1. Access stored values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 13.2. Access interpolated positions . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 13.3. Access coordinates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 14.Data Clusters 159 14.1. Reading in a cluster object . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 14.2. Generating a new cluster object . . . . . . . . . . . . . . . . . . . . . . . . . . 159 14.3. Adding a clusterView and a vertexView . . . . . . . . . . . . . . . . . . . . . 161 15.Surfaces 163 15.1. A simple surface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 15.2. Quadro-lateral surfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 15.3. Vector fields attached to a surface . . . . . . . . . . . . . . . . . . . . . . . . 165 16.Image filter 167 16.1. Using a pre-defined filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 16.2. Image filters implemented by shaders . . . . . . . . . . . . . . . . . . . . . . . 168 17.Colormaps

175

18.SpreadSheets

177

19.Graphical interfaces 19.1. Screen aligned text output . . . . . . . . . . . . . . 19.2. Repeated events in time . . . . . . . . . . . . . . . 19.3. Generate geometries by Open Inventor . . . . . . . 19.4. Plot textures in 3D . . . . . . . . . . . . . . . . . . 19.5. Get the actual mouse position . . . . . . . . . . . . 19.6. Specify the type of connection of a module to some 19.7. Writing Interfaces . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . data . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

179 179 181 182 182 185 185 186

20.Use of Avizo build-in libraries 20.1. mclib . . . . . . . . . . . . . . . . . . . 20.1.1. Dynamic Arrays . . . . . . . . 20.1.2. The flood-fill algorithm . . . . 20.1.3. Line integration in vector fields

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

189 189 189 189 190

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

21.Use of external libraries 193 21.1. Working with Numerical Recipes . . . . . . . . . . . . . . . . . . . . . . . . . 193 21.2. Matlab stuff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 21.3. Working with TinyXML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200

8

Part I. Avizo basics

9

1. Introduction This book is a collection of Tcl scripts and C++-programs that should help people to successfully develop their own Avizo modules. Whereas for the first half, the tcl examples you can use the standard Avizo, the second half of the book requires the Avizo XPand extension its license and a C++ compiler. This text was written as a collection of notes to particular topics that primarily are a personal reference. It does not make any assumptions about completeness or covering every aspect of the Avizo environment. The text does not have to be read sequentially. You should be familiar with Avizo already and pick out chapters based on your needs to develop a specific module. There is a focus here on source code examples and you should be able to copy them out of the document1 .

1

In order to get rid of the line numbers at the end of the lines hold down the ALT-key when selecting the text in Acrobat Reader. This will allow you to select a rectangular region excluding potential line numbers.

11

1. Introduction

12

2. Avizo modules This chapter is a collection of case examples that happend to be common hotline problems. Reading those should give you a little inside in the flexibility you gain by combining different tools available in Avizo.

2.1. Select a gray value range and extract a sub-volume A common worklist may require to extract a sub-volume from an initial data set. This may happen in order to do a display of a specific region of interest only without other other nearby object cluttering the field of view. In this example we will assume that the object of interest can be defined by a grayvalue range and a seed point. First we will generate a LabelField which will contain the information about the region of interest (see Step 1). Second, we will use this LabelField to extract the region of interest from the original data set (see Step 2). Voxel by voxel a LabelField contains a natural number to identify a tissue type. There are many different tools in Avizo to define such a data set and the process doing so is shortly called segmentation (see for instance LabelField, LabelVoxel, and CorrelationPlot).

Step 1 Load in your data set by using File→load and select your data. Attach a Labelling→LabelField to your data set. This will only be possible if your data set contains byte, short, or unsigned short values. In case you have color data or floating point data you need to convert the data first into the above data types (see Channelworks and CastField). Avizo will now do two things now. First, it will create a LabelField data set which will be visible right below your orginial data’s icon in the workspace. Directly afterwards Avizo will open the Segmentation Editor on this newly created LabelField. Select in the Segmentation Editor the magic wand tool. In the two text fields left select a useful data range for your region of interest. Moving the mouse over the screen you can see the data value underneath the cursor. Select now with the mouse a point inside your region of interest. Starting from this position a region growing algorithm will select all pixel that are connected and are in the data value range. You can adjust the settings until only the region of interest is drawn in a transparent red color. Now select the 3D checkbox and repeat clicking into the same general area in the current slice. The region growing will be repeated for the whole stack of images. If you click on the button labeled with a + you will add the currently selected voxel in all slices to the currently highlighed material in the material list (top left).

13

magic wand

2. Avizo modules Step 2

expressions

Given the original data and the LabelField defined above you can extract a sub-volume from the data by using the Arithmetic module. Attach a Compute→Arithmetic module to the original data. Now right-mouse-click in the small rectangle in front of the name of the Arithmetic icon and select the InputB section. Move the line that appears towards the LabelField data set and left click this icon to connect the LabelField as an additional input to the Arithmetic module. The first attached object will be called in the Arithmetic expression field A the second connected data set will be called B. With the expression we can now execute a arithmetic-logical operation on the pair of voxels from both data sets. Especially we would like to extract from A the values for which B is our region of interest. The result of this operation will be a new data object which combines the two inputs. Enter in the expression field (B==1)*A. This will extract the gray values from InputA where the LabelField (InputB) has a value of 1. By default the background in a LabelField has a value of 0 and all other materials get successive values starting from 1 up to a value of 255. If there is one material defined its value will be 1. The multiplication in the expression field is working on two numbers, the first one is the return value of (B==1) which is either true (1) or false (0). The second value is the gray value in the original value itself. This way we will multiply the values in the region of interest by 1, which leaves them unchanged and we will multiply the values in all other materials and the background by 0 which effectively replaces all those gray values in A by 0. The background value in the new data set will be 0 which is usually a good choice but in general one would like to be flexible to select another background value in case that 0 is for example a valid value in the region of interest. In this case you can change the expression into: (B==1)*A+(B!=1)*42 which will set the background value to a value of 42 while keeping the original values in the first material.

2.2. Volume rendering for RGB images The general volume rendering for RGB images may show an opaque cube instead of the expected transparent i.e. semi-transparent colors. The reason for this is there is a alpha channel required for volume rendering. In most rgb(a) images, the alpha channel is set to 1, meaning that all voxels are opaque. For semi-transparent rendering you need to set the alpha channel of the images. This can be done very flexibly with the modules →Compute→Arithmetic, →Compute→ChannelWorks and →Compute→ColorCombine. In most cases however, a simple TCL command will do what you need. The command is alphaAverage [min] [max] and it calculates an alpha channel that parallels the luminance of the image. The description of the command in the Online User’s Guide says: alphaAverage [min] [max]. Sets the alpha value of all voxels equal to the luminance 0.3*R + 0.59*G + 0.1*B, i.e., brighter objects become more opaque. If min and max are specified the alpha values are scaled so that they fill this range. On default min and max are 0 and 255, respectively. This is a command that can be applied to any RGBA field in Avizo and produces an alpha channel, where every bright voxel is opaque while the dark voxels become transparent. The

14

2.3. Analysing volumetric densities min value in the argument list of alphaAverage may be greater than the the max value, resulting in inverted transparency ramp.

2.3. Analysing volumetric densities Densities in three dimensional space arrise for example in the context of segmentation. Once a segmentation is performed and it extracts a specific material one can ask questions about how to describe the distribution of the material in space. Either the material is a single object or it may also be distributed in some way throughout the volume. A single object or a small group of object can be sufficiently described by the properties computed by the MaterialStatistics module. It will allow us to get the number of connected objects and their mean positions and volumes. For a distributed object the list of mean positions and volumes is less informative. It is better to describe the density inside the volume by for example plotting the density of a projection of the data volume onto a plane. Wherever we have a high density in the volume we will end up with a large value. Start with exporting one label in into a seperate label field. This is not a requirement for the next steps but we will assume that there is only a single non-background material in the label field. Find out what is the number of material you are interested in. This is defined by the order in which the materials are listed in the Segmentation Editor. The first material will always be the background material (Exterior) and has the number 0. Your material will be at a position 1 or higher. Attach a Arithmetic module to your label field and enter the following expression A==1 where 1 represents the position of your material in the list. After clicking on the DoIt button the resulting object will be a normal uniform scalar field instead of a label field so we could convert it back into a label field but we would have to do this again later so we will not care about the data type right now. Now change the voxel size of the object to 1 in the direction which you want to analyze. This will assure that each slice is at an integer multiple position of the slice before. By multiplying each voxel of our material (1) with its position in the data stack we can therefore produce a sorted list of materials which correspond with their material number with their position in the stack. Connect an Arithmetic module to our label field and enter the expression A*x in order to generate such a field. The variable x represents the location of each voxel in the x direction. The resulting label field will contain as many materials (numbers) as there are slices in the data set. Now convert this data set back into a label field. This can be done by the CastField module which will present the label field option for any input object which is of type byte. On the label field you can apply MaterialStatistics and by using the ’by slice’ methods you can get the volume for each slice. Plotting the volume over its slice position you will get a curve which scales linearily with the density in your data projected to the yz plane of your data set.

2.4. Displaying segmentation results The result of a segmentation is a new data set called a ’label field’. This label field contains for each voxel in space its material. It also contains informations about the names of

15

2. Avizo modules materials and about the colors used to display these materials. In order to display the labels one can use a slicing module like OrthoSlice. Used on a label field it will display the labels as different levels of gray even if other colors are specified in the parameter section of the label field. In order to get the correct display one needs to generate a custom colormap which can be used by OrthoSlice. This colormap can be created by a console command of the label field called makeColormap. After attaching the created colormap to the OrthoSlice and selection of the colormap mapping type each material will be displayed in the colors specified by the label field. Here is a resource file which adds this feature as a new display module for label fields. The name of the module is OrthoSliceLabel and it will appear in the Display section after right-mouse click on a label field. Copy the text into a file with the extension .rc and save it in your Avizo*/share/script-object/ directory. module -name "OrthoSliceLabel" \ -package "hxscriptobj" \ -primary HxUniformLabelField3 \ -category "Display" \ -proc { $PRIMARY makeColormap; $PRIMARY fire; set cm [lindex [all] end] set os [[create HxOrthoSlice] setLabel "OrthoSlice"] $os data connect $PRIMARY $os mappingType setValue 2 $os colormap setValue $cm $os fire }

2.5. Align two data sets using ObliqueSlice Two data sets can be aligned by a single view using the display module ObliqueSlice. The module allows for arbitrary rotations (enable the ’rotate’ checkbox). ApplyTransform is a module that can be connected to the data and additionally to an existing ObliqueSlice module. It will use the transformation defined by the ObliqueSlice module in order to re-orient the data. In order to use this for alignment do the following: a) load the reference (1) and the to be aligned data set (2) b) attach an OrthoSlice module to 1 c) attach an ObliqueSlice module to 2 which you rotate so that it matches the image seen in the OrthoSlice module on 1 d) press the Apply button on ApplyTransform

16

2.5. Align two data sets using ObliqueSlice The resulting data set will contain the transformation you are looking for. In order to get the resampled data using this transform simply use the console command ’applyTransform’ after the name of the result data object. In order to test the alignment one can attach Ortho- or ObliqueSlice modules to the two data sets and move them in sync. The synchronization can be handled by a small script that uses the fact that the display module will notify its downstream objects (the script) of any change in its orientation. # Amira-Script-Object v3.0 $this script hide $this proc constructor {} { $this newPortConnection con HxObliqueSlice } $this proc compute {} { set os1 [$this con source] if { $os1 == "" } { return }

set plane [$os1 getPlane] set oss [all HxObliqueSlice] foreach u $oss { if { $u == $os1 } { continue; } $u setPlane [lindex $plane 0] [lindex $plane 1] [lindex $plane 2] [lindex $plane 3] [lindex $pla $u fire } }

17

2. Avizo modules

18

Part II. Avizo scripting

19

3. Introduction Scripting of Avizo is done by writing text files in the Tcl1 language. Tcl is known as an easy to learn language with not enough restrictions to be useful for large projects (and its to slow for complex computations as well). A language with more restrictions is useful to reduce the number of error you can make and to allow an efficient bug finding. The magic cookies capabilities of Avizo rely on a special header that has to be included in all script files # Amira-Script-Object V3.0

Once this line is added to a file Avizo will recognize it as a script object and after loading it it will be added to the object pool with a blue icon. If you encounter any problems at this point, like Avizo being not able to load a text file with this header line, make sure that the coding type of the text is correct, that is if the line has the correct line ending character(-s). Script objects are only one way of adding functionality to Avizo. There is also a startupinit file which Avizo executes each time it is loaded. Adding Tcl-command to this file you can extend Avizo by new behavior like key-bindings. The startup file is called Avizo.init and it is located in Avizo’s share/resources/ directory. An additional way of adding new functionality is by editing the resource files of Avizo. Those files describe for example what readers/writers are bound to which file extensions or magic cookies. It is very easy to add for example a new file extension like .jpgs2 to the reader that reads JPEG files.

3.1. Remote controlling Avizo There are different ways of doing something like this in Avizo. First of all, (i) you can start Avizo with a script file as a parameter. In this file you have to write your commands to be executed. The Avizo script language is Tcl. For doing something like loading the medical/surf.hx file and displaying it you can do this manually in Avizo and save your work with File→SaveNetwork. The resulting file contains the Tcl-commands needed to re-create your network (data and display modules). This is the best way of learning Avizo’s Tcl language. 1

Usually there are two languages named side by side one is Tcl the other Tk. Avizo only understands Tcl. Because of this you will not be able to create your own user interface using the Tk language. Avizo provides its own interface for that. 2 This file extension was invented to describe stereo images. Basically two images side by side are saved as one JPG coded image. If you want to display two images as a single stereo image use the: viewer setBackgroundImage -stereo image1.tif image2.tif command. Series of tif images should be displayed by the MovieMaker. In order to create high quality stereo images do single snapshots while using ’viewer rotate 5 u’.

21

3. Introduction You can add to this file the command to save an image3 in, for example, tiff format by adding the line viewer 0 snapshot [-stereo][-offscreen x y][-alpha][-tiled x y] "c:/myimage.tif"

The viewer contains other features as well like the ability to save the current scene in an .iv or .wrl file viewer 0 saveScene [-binary] [-root] file.iv viewer 0 saveSceneAsVRML file.wrl [1|2]

or to count the current frame rate (in frames per second) viewer performance [-n]

In later versions of Avizo you can also call viewer

showFramesPerSecond 1

to show the current frame rate in the upper right corner of the screen. (ii) Another way of remote controlling Avizo is by using the Avizo build in socket connection. You have to start an Avizo process and enter in the console window ’app -listen’. Avizo will answer that it is listening to port 7171 (default). You can now call Avizo by executing: Avizo -cmd ”echo hello” -host your.host.na.me -port 7171 and the command in cmd will be executed in the other Avizo. Writing a short script file #!/bin/sh Avizo -cmd "$*" -host your.host.na.me -port 7171

you can send arbitrary commands this way to Avizo. If you want to control Avizo from you own programs you can send commands directly to the socket. Each command has to be preceeded by a unsigned char defining the length of the overall command. Note that messages send this way have a maximum length of 255 characters. You cannot use the -cmd line to provide a tcl command at the startup of your local Avizo only the remote machines will see your command. In case you just want to provide some tcl command line argument that is executed at the startup of Avizo you can use the following trick. Define a command line argument using -tclargs. This will generate a global tcl variable called argv (and argc), but it will not execute them automatically. In order to toggle the execution of the argument you need to change the Avizo.init file. Add the following to the end of the file: 3

A screenshot can also be done by the keyboard shortcut Ctrl-C or Ctrl-Insert. The image will be copied into the clipboard thus can be read into any other program. Please note that images imported in this way will be not compressed. Other keyboard shortcuts include the cursor keys which shift the displayed image up/down or left/right and the spacebar which is equivalent to the ViewAll viewer button.

22

3.1. Remote controlling Avizo

if { [info exists argv] } { if { [string length $argv] > 0 } { eval $argv } }

Now if you start Avizo with ’Avizomain.exe -tclargs ”echo HI”’ whatever tcl command is between the double-quotes will be executed. (iii) There is also a way to start Avizo without its graphical user interface. This is interesting if you want to use Avizo for computations only. You can start Avizo with the command line option -no gui and a script file and it will execute that script. But this way you will not get image written by the snapshot command because this uses a screen-grab mechanism which is not available if Avizo is started without its gui. Please note that you have to be careful in starting a lot of Avizo processes this way like from a batch file. The call to Avizo will return instantly because a new process is spawned. A lot of running Avizo processes will depleed your machines memory soon. One way to wait for the current Avizo to finish is to first put a ’quit’ command in the executed script file and second to wait till the Avizo process is finished. Using Windows you can use a shell script with START /w Avizomain.exe job1.hx

this will execute Avizo in foreground mode and so wait till Avizo is finished. Using a linux environment like cywin you could also do: # start the first Avizo process ... while [ ‘ps -al | grep Avizomain | wc -w‘ -ne 0 ]; do sleep 1; done # start the second Avizo process _after_ the first ...

1 2 3 4 5

(iv) In order to get a remotely running Avizo to display its windows on a local machine you can use a program like vnc. Set some environment variables like OIV REMOTERENDERER=ON and OIV REMOTERENDER DISPLAY=:0.0. Make sure that the vnc window is using a 24-bit color display (start its server with -depth 24). (v) Opening a socket connection from another program one can also remotely enter Tcl commands. Here an example using Tcl (in Avizo use ’app listen 3000’ to open the port) > % % %

tclsh 1 proc send { c } { set s [socket localhost 3000]; puts $s $c; flush $s; close $s 2 } send "echo \"First Command\"" 3 send "echo \"Second Command\"" 4

23

3. Introduction

3.2. Displaying your logos Another example how to use the Inventor file description is if you want to insert a logo in your animations. This here is the standard file that is loaded if we want to display the Avizo and VSG logo during our presentations: #Inventor V2.1 ascii Annotation { OrthographicCamera { viewportMapping LEAVE_ALONE } Translation { translation -1 -1 0 } Image { filename "Avizo.gif" } Translation { translation 2 0 0 } Image { filename "logo.gif" horAlignment RIGHT } }

If you want to display your logos you should be interested that they do not disappear if you load another network, or more specifically that it does not get removed after a call to remove all. This can be achieved by defining a variable $this setNoRemoveAll 1

in the constructer of the script object. A similar setting in C++ would set a HxObject property called noRemoveAll.

3.3. Enable the reload button For the development of script objects the re-write test cycle can be very time consuming. To speed up the reload of the changed script there is a special port which can be displayed for each Avizo script object. To enable the port you have to add the following line to your Avizo script object (preferable at the beginning of the file after the header line): $this script show

The standard behavior of this feature was changed and it is now switched off by default. You should remember to switch it off once the script is finished developing in order to decrease the amount of ports displayed by the file.

24

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

4. Compute modules 4.1. A standard compute There are some parts that appear several times if you are working with script objects. One is that you want to do something only if the user was hitting the DoIt button, another that you would like to have a connected data set. Here is an example how this initial script would look like: # Amira-Script-Object V3.0 $this proc constructor {} { $this newPortButtonList doit 1 $this doit setLabel 0 "DoIt" }

1 2 3 4 5 6

$this proc compute {} { if { ![$this doit wasHit 0] } { return }

7 8 9 10 11

set inputData [$this data source] if { $inputData == "" } { echo "Please connect to a data object" return; } ...

12 13 14 15 16 17

There is a variable which indicates in which directory on disk the current script was loaded from SCRIPTDIR. Note that this is a tcl global variable and it is by default hidden in any procedure. You will have to add a global SCRIPTDIR to the beginning of the procedure in order to make it known to tcl before you can use it.

4.2. A real example Lets write a script which is working like a compute module. It will have a input data set and it will produce an output after a set of operations. We will assume that we have a data set which was read in as short values while in fact the data is represented as unsigned short. The conversion will use HxArithmetic and CastField to do the transformation. Here is the whole script

25

4. Compute modules

# Amira-Script-Object V3.0

1 2

# uncomment this line for debugging # $this script show

3 4 5

# create the user interface (a single doit button) $this proc constructor {} { $this newPortButtonList doit 1 $this doit setLabel 0 "DoIt" }

6 7 8 9 10 11

# now the computation $this proc compute {} { # first check if the user did in fact hit DoIt if { ![$this doit wasHit 0] } { return }

12 13 14 15 16 17 18

# now check if there is a input to this script set inputData [$this data source] if { $inputData == "" } { echo "no input connected" return }

19 20 21 22 23 24 25

# all went well so now create an Avizo module CastField set cf [create HxCastField]

26 27 28

# set its label to CastField set cf [$cf setLabel "CastField"]

29 30 31

# connect our input data to its data port $cf data connect $inputData

32 33 34

# set values CastFields parameter section (unsigned short) $cf outputType setValue 2

35 36 37

# now simulate a user button click on CastField’s doit $cf action touch $cf action hit $cf fire

38 39 40 41 42

# get the output from CastField (assume that it worked) set output [lindex [lindex [$cf downStreamConnections] 0] 0]

43 44 45

# create an Arithmetic module and connect both signed and unsigned data set ar [create HxArithmetic] set ar [$ar setLabel "Arithmetic"] $ar inputA connect $output

26

46 47 48 49

4.2. A real example $ar inputB connect $inputData

50 51

# the expression uses the sign of the signed dataset to swap bytes $ar expr0 setValue \"a*(b>0) + (65535+b)*(b file%03d.jpg\n" $i $i); echo $fn; done;

1 2 3 4 5 6 7 8

In this example we used the terminal based http reader lynx to download some files from a distance http directory. Another useful tool for downloading a large number of files is wget.

6.7. DICOM units Avizo versions prior to 5.x use ’cm’ as default unit for DICOM volumes. As DICOM only contains values but not their units this assumption was only correct for a very limited number of volumes. With Avizo version ¿ 5.0 the default unit is now ’mm’ which better reflects the usual units stored in medical images. Due to this change DICOM volumes loaded with a newer Avizo will have a voxel size which is 10 times larger than before. One can change the default unit back to ’cm’ by changing the settings once in the DICOM read dialog and by using its ’Save Settings’ menu entry. This will set a registry value (DicomUnits) which is either 0.01 for centimeters or 0.001 for millimeters.

73

6. General Information

6.8. DICOM scaling - HU values and how they are represented DICOM data contain a native scaled data portion and two values that map these data by a linear mapping in the HU value range. For each slice the slope and intercept are applied to the raw data during load of the data. Lets imagine that for some of the DICOM images the slope value is larger than 1. Here an example for the slope values of a couple of the images (each line is one DICOM slope value): (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053) (0028,1053)

DS DS DS DS DS DS DS DS DS DS DS DS DS DS DS DS DS

[1.000000] [1.000000] [1.000000] [1.000000] [1.000000] [1.078758] [2.876696] [6.141895] [8.581682] [8.892249] [7.231891] [4.301316] [1.969185] [1.000000] [1.000000] [1.000000] [1.000000]

This part of the data could represent a high intensity region like the bladder and all the image data in that region have larger than 1 slope values. When Avizo imports the data is has to scale the raw data of each slice by the slope value (printed above). As the raw pixel data could have values close to 40, 000 a slope value of 8.9 would scale the data to 8.9 ∗ 40, 000 which is a value that cannot be represented anymore by an unsigned short (16bit) value. This causes Avizo to open a dialog box with three choices “Adjust scaling” “Ignore scaling” and “Clamp data”. The user can select one of them which influences how the data is represented in the software. “Adjust scaling” : For each slice the slope value is scaled so that the maximum value for all slices is mapped to the maximum intensity that can be represented by the data type. This produces a correctly looking result, but with data values that are scaled down. “Ignore scaling” : This ignores the scaling of each slice. The result is a stack in which only the raw DICOM data is displayed. For your data this results in some slices with too low intensities, those are the once that would using the scaling be displayed correctly. “Clamp data” : This applies the scaling of each slice. The slices with too high values will clamp at the highest intensities. This produces a correct looking images with correct scaling,

74

6.9. Reading in DICOM directories without DICOMDIR but the details in the high intensity region are lost. In this case that s in the bladder, which should not be of great interest. There are two good options in this case. Selecting the “Clamp data” option is easiest as the data is read in with correct scaling for all the slices where the intensities are not too high. The second option involves a little bit more work. The problem is caused by using a 16bit data format for image data which has a scaling that cannot be represented by 16bit. Avizo can convert the data to 32bit but this has to be done by hand. The data would be read in using “Ignore scaling” and each slice would be written out as a raw data file. The corresponding slope value needs to be extracted from the DICOM data and each slice needs to be converted to a 32bit type with the correct scaling. Saved back to disk these images can be loaded a second time, as 32bit with correct scaling. A process like this could be automated using an Avizo script, but that would be non-trivial and time consuming.

6.9. Reading in DICOM directories without DICOMDIR DICOM files can be distributed in many sub-directories. In this case it can be useful to collect them all in a flat structure. The following script is using the dcmdump (from the dcmtk package) to find out the series instance uid of each file found in the current directory. It will generate a directory with the series instance uid as name and copy the files into these directories. #!/bin/bash # This script will sort a directory tree for DICOM files and copy # them into a new flat(-ter) structure with directories named after # the series instance uid of each file. # This script is using dcmdump from the dcmtk package.

1 2 3 4 5 6

count=0 for u in ‘find . -xtype f‘ do # find out the file’s SIUID dirname=‘dcmdump $u | grep "(0020,000e)" | cut -d[ -f 2 | cut -d] -f 1‘ if [ ! -d $dirname ]; then mkdir $dirname fi count=‘expr $count + 1‘ echo $count cp $u ${dirname}/${count}.dcm done

7 8 9 10 11 12 13 14 15 16 17 18

6.10. Force a re-computation In a script sometimes a computation has to be performed in a loop. In order to invoke the same actions on a module by scripting compared to the appropriate mouse-clicks in its

75

6. General Information user interface (DoIt button etc.) you will have to do the following two calls additionally to setting the value of the port for {set i 22} {$i < 245} {incr i} { Isosurface threshold setValue $i Isosurface doIt touch Isosurface fire }

1 2 3 4 5

The touch command simulates the mouse click and the fire command will execute this action. If you need to work with ports that expose multiple buttons you can select the appropriate button by setValue 1 where whichButton is a number starting from 0 for the first button in order. Here an example Avizo XScreen action setValue 2 1 Avizo XScreen fire

which will activate the third button (2) in the action port of an Avizo XScreenmodule.

6.11. Add information to data files The parameter editor allows to view and add parameters to any Avizo data object, i.e. a green icon. Those parameters are saved together with the data in AmiraMesh files and can therefore be used to annotate data files. Parameters can be organized by a hierarchical structure. Directories are called ’bundles’. So you can generate a new directory by using the ’newBundle’ method. data.am parameters newBundle DICOM data.am parameters DICOM setValue "Patient name" "anonymous"

Here we added also a parameter value pair into this directory. If the parameters are of a specific type you have to state that explicitly while generating the new parameter. For example on ca add a parameter G0018-11A0 and assign to it the following character string as its value: body part thickness [UL]: 65. If we now add a parameter G0018-1531 with the value detector secondary angle [DS]: -21.5542 both will be recognised as either unsigned long (UL) or decimal string (DS). If now value representation is given, and the tag is not included in Avizo’s list of known DICOM tags (AvizoBase/dicom/dicom.h), the value representation defaults to OB (other byte). In order to print all the parameters available for a data file a recursive function like the following can be used: # Avizo Script

1 2

# assume that the data file is called lobus.Labels

76

3

6.11. Add information to data files set dataset lobus.Labels

4 5

# # This function will recursively print out all parameters. # Arguments are the name of the data set (dataset) the path # to a group of arguments (path) and an initial level (level). # proc printAllBelow { dataset path level } { # get some spaces infront of the lines to represent level set spaces [format "%[expr ${level}*3]s" " "] # how many entries for this parameter section? set cmd "$dataset $path size" set numEntries [eval $cmd] # for each of the entries now print out all sub-entries for { set i 0 } { $i < $numEntries } { incr i } { # get the name of the i’s entry set cmd "$dataset $path index $i" set thisName [eval $cmd] # test if this entry is a bundle or a simple key set cmd "$dataset $path $thisName isBundle" # if its a key print it, else recurse again if { ![eval $cmd] } { set cmd "$dataset $path $thisName getValue" set thisValue [eval $cmd] echo "$spaces $thisName = $thisValue" } else { echo "$spaces Bundle $thisName:" # go one level deeper printAllBelow $dataset "$path $thisName" [expr $level+1] } } } # this is the initial call to the function # which will print out every key/value under parameters printAllBelow $dataset " parameters " 0

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

Here is another function that will print out the commands needed to copy a parameter tree from one object A to another object B. The part of the tree to be copied can be specified as well as the destination. This function is very similar to the one above. Instead of performing the move it will create a list of commands that can be executed to perform the copy operation. set createCMD "" # # Collect the build commands to move parameters pathA in object A # to pathB in object B. # After calling this function createCMD contains the commands # needed to perform the copy operation (use eval to perform the copy # operation).

1 2 3 4 5 6 7

77

6. General Information proc collectParameters { A pathA B pathB } { global createCMD

8 9 10

set cmd "$A $pathA size" set numEntries [eval $cmd] for { set i 0 } { $i < $numEntries } { incr i } { set cmd "$A $pathA index $i" set thisName [eval $cmd] set cmd "$A $pathA $thisName isBundle" if { ![eval $cmd] } { set cmd "$A $pathA $thisName getValue" set thisValue [eval $cmd] set createCMD "${createCMD}\n$B $pathB setValue $thisName \"$thisValue\"" } else { set createCMD "${createCMD}\n$B $pathB newBundle \"$thisName\"" collectParameters $A "$pathA $thisName" $B "$pathB $thisName" } } } # call the function with two data objects (lobus.Labels and artischoke) # copy all parameters from A to B collectParameters lobus.Labels " parameters " artischoke " parameters " echo $createCMD # eval $createCMD

For an OrthoSlice this can be used to store contrast and brightness variables specific for this data file. If one uses the ContrastControl function on the OrthoSlice one can come up with a setting for the minimum and maximum value of the transfer function setup (the data window fields). These values can then be stored in the data file because they define the brightness and the contrast used to display the image. Save the values in a variable called DataWindow. Add as values the two number you read out from the Data Window port with a space seperating them. The next time you attach an OrthoSlice module to this data object the corresponding colormap will be initially set to the two provided values. Here is how you add this in a tcl script. data.am parameters setName dataWindow data.am parameters setValue dataWindow "22 254"

6.12. Add a button next to “Load Data” Buttons next to the “Load Data” button are called macrobuttons and they can be defined by the user. Here is an example which implements a macro button for loading an input data file, attaching a CastField module to it, and converting the data to an unsigned short. One has to edit two files:

78

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

6.12. Add a button next to “Load Data”

\share\resources\Avizo.init \share\resources\macrobuttons.rc

At the bottom of Avizo.init add the following Tcl code: # The funtion that is executed after pressing the button proc loadConvert16bit {} { set script [[create HxScriptObject] setLabel "foo"] $script newPortFilename filename $script filename setMode 1 $script select $script filename exec if [ catch { load [$script filename getFilename] } datafile ] { remove $script return } if { $datafile != "" } { set cast [create HxCastField] $cast data connect $datafile $cast select $cast fire # # Specify outputType here: # # unsigned char (byte) = 0 # signed short (16-bit) = 1 # unsigned short (16-bit) = 2 # signed int (32-bit) = 3 # float = 4 # double = 5 # LabelField = 6 # $cast outputType setValue 2 $cast fire $cast action setValue 0 $cast fire set newData [lindex [lindex [$cast downStreamConnections] 0] 0] if { $newData == "" } { return } $newData fire $newData select $newData fire $newData master disconnect $newData fire remove $cast remove $datafile $newData deselect }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

79

6. General Information remove $script

45

}

46

In the macrobuttons.rc file, add the following line at the end: macroButton -add "Load 16-bit..." -color mediumaquamarine -proc "loadConvert16bit"

With these changes, when starting up Avizo one should get another button labelled ”Load 16-bit...” next to the standard ”Open Data...” button. Clicking on it, the Avizo Load file dialog will be displayed, allowing to select an input file. The file being loaded, a CastField module being attached, and an output file being generated. At the end, only the new output file is added to the Pool.

6.13. Add function keys Function keys can be added by defining tcl procedures that have a specific name. Here an example: proc onKeyF1 {} { echo "hello" }

Allowed are only the ’F’-keys but you can use any of the three Shift, Ctrl, or Alt keys additionally. Here a more complex example which defines two functions that increase or decrease the eye balance for stereo (OpenGL raw stereo): set balance 0.7 set offset 0.7 proc onKeyF8 {} { global balance offset set balance [expr $balance echo balance is $balance viewer 0 setStereo -m 0 -b } proc onKeyShiftF8 {} { global balance offset set balance [expr $balance echo balance is $balance viewer 0 setStereo -m 0 -b }

1 2 3 4

+ 0.1]

6

$balance $offset

7 8 9 10

- 0.1]

11 12

$balance $offset

There is another special case for defining keys in Avizo. This is for the 2D plot windows. In this case a definition looks like this:

80

5

13 14

6.14. Announce new modules with .rc files

proc onPWKeyF4 {} { global thePlot

1 2 3

# works only if there is a plot window if { ![ info exists thePlot ] } { echo "There is no plot window" return }

4 5 6 7 8

...

9

A more complex example of the use of onPWKey can be found in the hxdataprobe.rc files which is in the share/resources directory of Avizo.

6.14. Announce new modules with .rc files This example .rc files add a new entry to the context menu of Ortho- and ObliqueSlice in order to set the brightness and contrast of the data. After saving the data file these values will always be used later on. This tcl-code has to be copied to the Avizo-x.x/share/resource/ directory in order to be available after the next start of Avizo. ############################################################# # .rc provides a popup menu entry for setting the data window #############################################################

1 2 3 4

# register a menu entry that will set the colormap min/max values # as the DataWindow in the dataset connected to the module module -name "Set DataWindow" \ -primary "HxObject" \ -check { [__providesDataRange $PRIMARY] } \ -category "Main" \ -proc { set range [__getDataRange $PRIMARY] if [llength $range] { set data [__getFirstData $PRIMARY] $data parameters setValue "dataWindow" $range set x "data window for $data has been set\nto range" append x "\[[lindex $range 0]:[lindex $range 1]\]" theMsg warning $x $data touch } }

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

Here are some additional functions that are used in the above code. # check if a module has a (visible) port of a certain type, and return its name proc __firstPortOfType {type module} {

1 2

81

6. General Information foreach port [$module allPorts] { if {[$module $port getTypeId] == $type} { if [$module $port isVisible] { return $port } } } return "" } # return first data object connected to a module, or show message proc __getFirstData {module} { set dataport [__firstPortOfType HxConnection $module] if {$dataport == ""} { theMsg error "no connection port found $module" "OK" } else { set data [$module $dataport source] if {$data == ""} { theMsg error "no data connected to $module" "OK" } else { return $data } } return "" }

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

# query the data range from a given module proc __getDataRange {module} {

28 29 30

set type [$module getTypeId]

31 32

if { ($type == "HxOrthoSlice") || ($type == "HxObliqueSlice") } { # which one is active: linear range or colormap? set mapping [$module mappingType getValue] if {$mapping == 0} { # linear range is active set min [$module linearRange getValue 0] set max [$module linearRange getValue 1] } elseif {$mapping == 1} { # histogram mode theMsg error "cannot determine data range in histogram mode" "OK" return "" } else { # colormap mode set min [$module colormap getMinValue] set max [$module colormap getMaxValue] } } else { # any other module: look for the first colormap port set port [__firstPortOfType HxPortColormap $module] if {$port == ""} {

82

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

6.14. Announce new modules with .rc files theMsg error "found no colormap port in $module" "OK" set min [$module $port getMinValue] set max [$module $port getMaxValue]

53 54 55

}

56

}

57 58

return [list $min $max]

59

} # check if a module is suited for data range definition proc __providesDataRange {module} { set type [$module getTypeId] if { ($type == "HxOrthoSlice") || ($type == "HxObliqueSlice") } { return 1 } set port [__firstPortOfType HxPortColormap $PRIMARY] if {$port != ""} { return 1 } echo "nix is" return 0 } echo "method setDataWindow registered."

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

New modules can also be created by customizing existing modules. This is also done in the *.rc files. Here an example that defines a new SurfaceView1 module. Using the existing module we define new initial setting as for example a different transparency mode: module -name "HaukesSurfaceView" \ -primary "HxSurface HxSurfaceScalarField" \ -class "HxDisplaySurface" \ -category "Main Display" \ -proc {$this drawStyle setValue 4; $this baseTrans setValue 0.8} \ -dso "libhxsurftools.so"

1 2 3 4 5 6

The key point is to extend the standard rc-definition of a module by a -proc section that can be filled with tcl-calls. Another example is the ’Create→Landmarks (2 sets)’ which is using the same approach to instanciate a Landmark object which contains two sets of points. As you may have noticed in the ’category’ section you can define where your module should appear in the menu initially. The default is that modules are generated by rightclicking in the user interface on the modules that they use as input. This object oriented appearence can be broken by using the special category ’Data’. Modules with this setting will be presented in the Create→Data menu of the main control window. This makes sense 1

A good thing to know about the SurfaceView module is the fact that there is a tcl-command line option called ’selectVisibleOnly’ which can be used to change the mode of selection. Using Draw one can punch holes in a surface without removing the triangles in patches underneed.

83

6. General Information for objects that do not require any input data for their operation as for example a module which defines an analytical vector field.

6.15. The current time The current time can be printed out using: clock format [clock seconds]

84

7. Editors 7.1. Invoke the Crop Editor The crop editor can be attached to a data object with set CE [create HxImageCrop] $CE attach $image

Some commands of the crop editor are available directly on the data sets. This includes the flip, swap and the crop options. # flips the data around the x-Axis (1 is y-, 2 is z-Axis) lobus.am flip 0 # swap dimensions will re-arrange the order 0,1,2 into the given one lobus swapDims 1 0 2 # crop the background of a volume with a threshold lobus.am crop -auto 54 # crop the image by given values (imin imax jmin jmax kmin kmax) lobus.am crop 20 99 20 99 20 40 # if the new dimensions are bigger specify a value for filling lobus.am crop -10 500 -10 500 -5 200 42

7.2. Invoke the Segmentation Editor In Avizo versions prior to 5.2 you can invoke the Segmentation Editor automatically for a label-field by executing: create HxGiEditor gi attach mydata-labels

(where mydata-labels is a prior loaded label set) in the Avizo console window (or script). HxGiEditor is the name of the class which is the Segmentation Editor. Its object name in the Avizo object pool is ’gi’. By ’attach’ we assign the Segmentation Editor to be working on the ’mydata-labels’ labelfield. Since Avizo 5.2 one has to use the more consistent way of assigning the editor to a variable before using it: set gi [create HxGiEditor] $gi attach mydata-labels

85

7. Editors In the following sections we will assume that ’gi’ is replaced with $gi if Avizo 5.2 or later is used. There are some options that can be invoked in the Segmentation Editor using the gi Tcl-interface. Smoothing for example is done by: gi setSlice N gi smooth 3

(smoothes the slice number N with a Gaussian kernel of size 3) or gi smoothAll

which smoothes all slices. There are some more commands available like ’fillBones’ and ’removeIslands3d’. You get a list of them by executing > gi allPorts attach blowShrink clipGeom detach experimentalBrushMode experimentalLassoMode fillBone fillBoneAll fire getControllingData getLabel getProjectionBox getTypeId getVar getViewerMask hasVar help numParam paste proc redo removeIslands removeIslands3d render selectMaterial set1ViewerMode set4ViewerMode setControllingData setLabel setMaxAlpha setOrientation setSlice setTool setVar setViewerMask smooth smoothAll unclipGeom undo unsetVar >

in the Avizo console window. Please note, that in order to work some functions like the orientation test will require a material called “Exterior”.

7.2.1. Automatic erosion and dilation The Segmentation Editor allows us to use some of its build-in functionaly from Tcl. One example is the opening and closing of structures. This involves the repeated application of erosion and dilation on a specific material of a label field. Here is an example: # assume lobus.labels.am is loaded create HxGiEditor gi attach lobus.labels.am # select the first non-background material (Exterior is 0) gi selectMaterial2 1 # two times dilation gi growSelection3D gi growSelection3D # one time erosion gi shrinkSelection3D # now assign the new selection to material 1

86

1 2 3 4 5 6 7 8 9

7.3. Invoke the Parameter Editor gi add 1 # remove the module from display gi detach

7.3. Invoke the Parameter Editor The following two lines will create a Parameter Editor and show the parameters of an input object whose name is stored in the variable data. set pe [create HxParameterEditor] $pe attach $data

7.4. Invoke the Transform Editor In a similar way than the Segmentation Editor also the Transform Editor is represented by an (hidden) object in the Avizo workspace. It needs to be generated and attached to a data object. The transform editor is generated by: create HxTransformEditor xe xe attach mydata.am xe fire

. The current settings for the transformation of a data set can also be printed out by using the console command datafile.am getTransform

which will result in a list of 16 numbers that represent the usual 4 × 4 matrix for transformations containing rotations, shifts and scalings. In order to copy a transformation from one object to another you can use this construct. otherdatafile.am setTransform 1 0 0 ...

If you want to reset the transformation and the Transform Editor is still open you can get access to the buttons by transformEditorReset hit transformEditorReset touch transformEditorReset send 1

where nB is the number of the button, e.g. All would be 0, Translation is 1, Rotation is 2 and Scale is 3. The action buttons are working similar. In this case you use transformEditorAction instead of transformEditorReset.

87

7. Editors

7.5. Invoke the Camera Path Editor Similar to the examples above you can create a the camera path editor and attach it to a camera path by # create the camera path first set cp [create HxKeyframeCameraPath] # now create the editor set cpe [create HxKeyframeCameraPathEditor] $cpe attach $cp $cp fire

1 2 3 4 5 6 7

# now we can set the current camera position with viewer 0 setCameraPosition 0 0 0 viewer 0 setViewDirection 1 0 0

8 9 10 11

# and save this location in the camera path $cp portKCPEButtons setValue 0 1 $cp portKCPEButtons send $cp fire

7.6. Invoke the Surface Editor create HxSurfEdit surfEdit attach lobus.surf

7.7. Invoke the slice aligner To perform an automatic slice alignment use the following framework: set set $la $la $la $la $la $la

data YourDataObjectIconName la [create HxAlignSlices] data connect $data select action setValue 0 1 fire setAlignMode 0 alignAll

Now you can perform a resampling of the data and save them as a new 2D image series $la action setValue 1 1 $la fire set newData [lindex [lindex [$la downStreamConnections] 0 ] 0] $newData save "Raw Data 2D" c:/newFileName####.raw

88

12 13 14 15

7.8. Invoke the Landmark Editor

7.8. Invoke the Landmark Editor Given a landmark set you can create a landmark editor object with set le [create HxLandmarkEditor] $le setNumSets 2 $le attach Landmarks Landmarks fire

Landmarks can be added using ’appendLandmark’ (adds a landmark for each set at the specified coordinates) and moved using ’setPoint’ ( ). Before moving the landmark the editor has to switch to the correct mode using “Landmarks landmarkEditorMode setValue 2” (for moving points).

89

7. Editors

90

Part III. C++ Code examples

91

8. General Information 8.1. First steps In order to start a project in Avizo you should always start with the Development Wizard. It will generate the directory structures needed, a first templated version of your project and the make files for your project for both Unix and Windows based systems. Sometimes you will need to link your project against a new library. This happens if you, for example, want to export a spreadsheet object as additional output. Spreadsheets are defined in the hxspreadsheet library so you need to link your project against this library. Add the new library to the Package file in the LIBS section. The file is generated by the Development Wizard in your project directory. The ’build system’ option in the Development Wizard will generate new versions of the build files which contain the library.

8.2. Compiling a package On Linux by default (as well as on the other systems) a debug version of your module will be generated. In order to produce the final version of the module without the debug information you need to call (for the unix systems) gmake MAKE CFG=Optimize on Windows systems you can recompile after selecting the Release solution configuration. In order to generate a 64bit version of your library you need to set the environment variable AVIZO64 to 1. You can check if the library was correctly build for 64bit by using the command line tool file. There is also a variable defined with this name in the start script that you need to set to 1 so that the 64bit version of the libraries are called. In order to check if a library contains a specific function one can use the linux tools ’nm’ and ’c++filt’. In the following call we look for a specific function: nm /root/Avizo-plugins/lib/arch-LinuxAMD64-Debug/libexample.so | \ c++filt | grep myfunction

More information about a dll is obtained by the objdump command. It will list the type of the executable (32 or 64 bit) and also all dependent libraries (objdump -x somelib.dll | less). In order to find out where Avizo is looking for your module you can type into the console of Avizo (before you start your module) dso verbose 1

Now Avizo will display some more information about the directories it is looking for additional libraries like your the library which contains your package.

93

8. General Information In order to have your debug-mode module visible in Avizo you need to start Avizo with the command line option -debug.

8.3. Display a progress bar The progress bar can be used to display the progress of a computation and to allow the user to break out of a loop using to much computational time. #include ... theWorkArea->startWorking("Doing a lot..."); for(int i=0;isetProgressValue(i*1.0/10000.0); theWorkArea->setProgressInfo("still working..."); if(theWorkArea->wasInterrupted()){ theWorkArea->stopWorking(); return; } # place your code here ... } theWorkArea->stopWorking();

8.4. Accessing environment variables and the registry Environment variables can be queried as follows const char* AvizoDir = getenv("AVIZO_LOCAL"); if (getenv("AVIZO_USE_ADVANCED_ALGORITHMS")) magic = true; else magic = false;

In order to read a global variables in the Avizo workspace (as defined by the tcl set command) use const char *value = theInterpreter->getVar("someVariableName");

. You can also generate and query local variables which belong to an existing module. This way modules of the same type can have different values for this variable. Define such a variable belonging to an object ’moduleName’ by for example moduleName setVar myKernelSize 42 and query them inside another module with:

94

1 2 3 4 5 6 7 8 9 10 11 12 13 14

8.5. Display the file dialog

McString res; if(getTclMemberVar("myKernelSize", res)) kernelSize = atof(res); // kernelSize has now the value of the variable

If a module needs to store settings across Avizo sesions one can use the registry of the operating system. Old versions of Avizo used a mechanism in HxResource. #include ... // store values in the registry char str[256]; sprintf(str, "%d", portPosition.getValue(0)); HxResource::regSetValue("MyModule_positionX", str);

1 2 3 4 5 6 7

// read values from the registry McString str; if(HxResource::regGetValue("MyModule_positionX", str)) { portPosition.setValue(0,atoi(str)); } else { // use the default value portPosition.setValue(0,42); }

8 9 10 11 12 13 14

With newer versions of Avizo the settings manager should be used instead. #include ... // store values in the registry char str[256]; sprintf(str, "%d", portPosition.getValue(0)); theSettingsMgr->setCustom("MyModule_positionX", str);

1 2 3 4 5 6 7

// read values from the registry McString str; if(theSettingsMgr->getCustom("MyModule_positionX", str)) { portPosition.setValue(0,atoi(str)); } else { // use the default value portPosition.setValue(0,42); }

8 9 10 11 12 13 14

8.5. Display the file dialog The normal way to get a file dialog in your program is to use the corresponding port HxPortFilename. It provides a button and a text field. You can also call the load/save dialog on your own.

95

8. General Information

#include ... McString file; if (!filename) { HxFileDialog fn; fn.setDialogUsage (HxFileDialog::LoadFile); fn.setTitle ("Load a file on disc"); fn.postAndWait (); McFilename f (fn.getFileName ()); if (f.doesExist ()) { theMsg->printf("error: could not create file"); } }

1 2 3 4 5 6 7 8 9 10 11 12 13

There is also a global object theFileDialog which represents the file dialog. Use this one if your dialog should keep the values for the directory from the last visit. The example above also shows the usage of McFilename which contains methods for testing if a file exists, is readable by the current user or methods to seperate the basename from the root and the extension of the filename. In order to read in a number of files automatically we can also use the McFilenameCollector class. Especially the static function collect is useful to get a list of filenames from a given directory. Here an example that looks into the share/models/ directory of Avizo and tries to find files ending with .iv. Found files are entered into a HxPortMultiMenu. McString r = HxResource::getRootDir(); r += "/share/models/"; // which files in here are .iv? McDArray fileNames; fileNames.resize(0); /// if there would be some leftover filenames McFilenameCollector::collect(fileNames, "*.iv", r); // extend the first drop down menu and add the basenames portMenu.setNum(0,fileNames.size()); for(int i = 0;ibasename()); }

1 2 3 4 5 6 7 8 9 10 11 12

But now back to the Filename dialog which is the topic of our section. theFileDialog->setDialogUsage(HxFileDialog::LoadFileList); if (theFileDialog->postAndWait() == 0) { McString buf; // will contain a list of filenames for (int i=0; igetFileCount(); i++) { buf += ’"’; buf += theFileDialog->getFileName(i); buf += "\" ";

96

1 2 3 4 5 6 7

8.6. Accessing other modules }

8

}

9

8.6. Accessing other modules If you want to control other modules currently loaded in Avizo you can get a list of them by using theObjectPool. Here an example that collects all the float sliders in all the modules currently loaded and prints out their value. First let’s iterate over all objects in the workspace and see if any of them has a HxPortFloatSlider: #include ... for (int i=0; inodeList.size(); i++){ HxObject *obj = theObjectPool->nodeList[i]; for (int j=0; jgetNumPorts(); j++){ HxPort *port = obj->getPort(i); if(!port->isVisible()) // do not use them continue; HxPortFloatSlider *slider = dynamic_cast(port); if(!slider) // its not a float slider continue; // now do something with the port theMsg->printf("the current value is: %g", ((HxPortFloatSlider *)port)->getValue()); } }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Note that you get the list of all visible objects by calling instead of nodeList theObjectPool→visibleList. Another way to get only the visible objects is to call theObjectPool→nodeList[i].iconVisible(). Oddly enough the value returned by calling getLabel() on a port is a char *. For all modules it is a McString object which needs to be converted into a character pointer: theMsg->printf("%s: %s", theObjectPool->nodeList[i].getLabel().toString(), theObjectPool->nodeList[i].getPort(0).getLabel());

8.6.1. Timing modules Sometimes it is useful to know how long a specific call took to execute. For this purpose the following construct measures time in seconds: #include ...

1 2

97

8. General Information clock_t t1 = clock();

3 4

// Start work dosomething();

5 6 7

clock_t t2 = clock(); float time = (float)(t2-t1)/CLOCKS_PER_SEC; theMsg->printf("Total time it took: %6.2f sec", time);

8 9 10

8.6.2. Create an existing module All the Avizo objects can be created by any other module. Here an example on how to create an OrthoSlice module from a compute module. #include ... HxOrthoSlice *os = (HxOrthoSlice *)HxResource::createObject("HxOrthoSlice"); if(os) { os->setLabel("OrthoSlice"); theObjectPool->addObject(os); os->portData.connect(portData.source()); // connect to my own input os->fire(); }

1 2 3 4 5 6 7 8 9

8.6.3. Working with ports The name ports summarizes the interface ports and the connections of a module. The order in which the ports are defined in the header file of the C++ class defines the order in which the ports will be listed in the interface. Whereas the names of objects and ports can be queried by getLabel() the classTypeId needs to be queried with getName(). Here some examples on how to get informations out of the ports of a given object: for (int j=0; j< object->getNumPorts(); j++) { HxPort* port = object->getPort(j); if (!port->isVisible()) // ignore all invisble ports continue;

1 2 3 4 5

theMsg->printf("%s has port %s of type %s", object->getLabel().getString(), port.getLabel(), port.getClassTypeId().getName());

6 7

Here now some examples of querying the values of ports. This always depends on the type of port. if(port->isOfType(HxPortFloatSlider::getClassTypeId()){ HxPortFloatSlider *p = (HxPortFloatSlider *)port;

98

1 2

8.6. Accessing other modules theMsg->printf(" min: %g, max: %g, current value: %g", p->getMinValue(), p->getMaxValue(), p->getValue());

3 4 5 6

}

7 8

if(port->isOfType(HxPortIntSlider::getClassTypeId())){ HxPortIntSlider *p = (HxPortIntSlider *)port; theMsg->printf(" min: %d, max: %d, current value: %d", p->getMinValue(), p->getMaxValue(), p->getValue()); }

9 10 11 12 13 14 15 16

if(port->isOfType(HxPortColormap::getClassTypeId())){ HxPortColormap *p = (HxPortColormap *)port; float min,max; min = max = 0; p->setLocalMinMax(min,max); theMsg->printf(" local range min: %d, max: %d", min, max); }

17 18 19 20 21 22

if(port->isOfType(HxPortText::getClassTypeId())){ HxPortText *p = (HxPortText *)port; theMsg->printf(" current value: %s", p->getValue()); }

23 24 25 26 27

if(port->isOfType(HxPortFilename::getClassTypeId())){ HxPortFilename *p = (HxPortFilename *)port; theMsg->printf(" current value: %s", p->getValue()); }

28 29 30 31 32

if(port->isOfType(HxPortRadioBox::getClassTypeId())){ HxPortRadioBox *p = (HxPortRadioBox *)port; for(int k=0;kgetNum();k++){ theMsg->printf(" toggle%d: %s", k, p->getLabel(k)); } theMsg->printf(" currently highligted: %s (%d)", p->getLabel(p->getValue()),p->getValue()); }

33 34 35 36 37 38 39 40 41 42

if(port->isOfType(HxPortToggleList::getClassTypeId())){ HxPortToggleList *p = (HxPortToggleList *)port; for(int k=0;kgetNum();k++){ theMsg->printf(" toggle%d: %s (%s)", k, p->getLabel(k), p->getValue(k)==1?"on":"off"); } }

43 44 45 46 47 48 49 50

if(port->isOfType(HxPortIntTextN::getClassTypeId())){ HxPortIntTextN *p = (HxPortIntTextN *)port;

51 52

99

8. General Information for(int k=0;kgetNum();k++){ theMsg->printf(" intText%d: %s %d [%d..%d]", k, p->getLabel(k), p->getValue(k), p->getMinValue(),p->getMaxValue()); } }

53 54 55 56 57 58 59

if(port->isOfType(HxPortFloatTextN::getClassTypeId())){ HxPortFloatTextN *p = (HxPortFloatTextN *)port; for(int k=0;kgetNum();k++){ theMsg->printf(" floatText%d: %s %g [%g..%g]", k, p->getLabel(k), p->getValue(k), p->getMinValue(),p->getMaxValue()); } }

60 61 62 63 64 65 66 67 68

if(port->isOfType(HxPortMultiMenu::getClassTypeId())){ // now some specific values for that port HxPortMultiMenu *p = (HxPortMultiMenu *)port; for(int k=0;kgetNumMenus();k++){ McString label = ""; for(int o=0;ogetNum(k);o++){ label += " \""; label += p->getLabel(k,o); label += "\""; } theMsg->printf(" MultiMenu \"%s\" (%d): %s", p->getLabel(k), k, label.getString()); theMsg->printf(" active: %d(%s)", p->getIndex(k), p->getLabel(k,p->getIndex(k))); } } }

Note, if you try to access the interface of a module which was just created it may be that its ports are not yet defined. This happens for example in a reader which may generate some display modules attached to the data. In this case you can force the interface of the module to be created if you select the object: object->select(); // now interface with the module like setting a pin object->deselect();

Another case is when you like to find a specific module in the workspace in order to attach yourself to it. This is used to automate the setup of modules that require more than one input. Usually the inputs share a common part of their filename, like starting with the same name but ending with -labels.am instead of .am. Here is a piece of code that is best put into an update()/indexupdate module.

100

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

8.7. Textual input and output

if(portData.isNew()){ // if we get connected // guess the name of the other module McString name; setResultName(name, portData.source()->getName(), "_EVALS"); if(theObjectPool->findObject(name)){ // include Amira/HxObjectPool.h portEVALS.connect(theObjectPool->findObject(name)); } }

1 2 3 4 5 6 7 8

8.7. Textual input and output To produce textual output from inside Avizo modules you have to use the theMsg->printf("bla %d", 100)

functionality known from the C language. You can also use the C++ equivalent which looks like this theMsg->stream() eval("echo \"tcl command\""); theMsg->printf("Value returned by eval: %s", someStr); ... Tcl_Interp* inter = theInterpreter->getTclInterp(); // set a tcl variable char t[256]; sprintf(t, "%d", 30 + 12); theInterpreter->setVar("bla", t, TCL_GLOBAL_ONLY); // ask the variable for its value char* ampl = (char*)theInterpreter->getVar("bla"); theMsg->printf("Value of tcl variable bla is: %s", ampl);

or to remove the variable again Tcl_UnsetVar(inter,"bla",TCL_LEAVE_ERR_MSG | TCL_GLOBAL_ONLY);

8.10. Change the voxel size The voxel size is defined by bounding box that a number of voxel occupy. So we can change the voxel size by changing the data’s bounding box (and leaving the dimensions the same). So lets ask the field for the values of the bounding box and its dimension.

102

1 2 3 4 5 6 7 8 9 10 11 12

8.11. The camera position

const int *dims = field->lattice.dims(); float bb[6]; field->getBoundingBox(bb);

The voxel size (for uniform scalar fields) is given as: /*voxelsize for */ x= ((bb[1]-bb[0])/(dims[0]-1); /*voxelsize for */ y= ((bb[3]-bb[2])/(dims[1]-1); /*voxelsize for */ z= ((bb[5]-bb[4])/(dims[2]-1);

In order to change the voxel size to xn, yn, zn we have to have a new bounding box which is: float nbb[6]; nbb[0] = bb[0]; nbb[2] = bb[2]; nbb[4] = bb[4]; nbb[1] = (dims[0]-1)*xn + bb[0]; nbb[3] = (dims[1]-1)*yn + bb[2]; nbb[5] = (dims[2]-1)*zn + bb[4];

and now the new bounding box is in nbb. field->coords()->setBoundingBox(nbb);

Please note that this will let the lower right corner of the bounding box be fixed. Your data will therefore grow or shrink in a specific direction rather than keep its mean position.

8.11. The camera position The camera is an object of viewer. Here an example: #include #include #include ... SoCamera *camera = theController->viewer(0)->getCamera(); float rad; SbVec3f axis; SbVec3f pos = camera->position.getValue(); camera->orientation.getValue().getValue(axis, rad); float focalDistance = camera->focalDistance.getValue(); float nearDistance = camera->nearDistance.getValue();

1 2 3 4 5 6 7 8 9 10 11

Also if there are some values to be saved/changed if one is using a perspective or some other values if one is using an orthogonal camera. Using an SoPerspectiveCamera one has to ask for its ’heightAngle’. The same value from an SoOrtographicCamera is called ’height’ (see Inventor/nodes/SoPerspectiveCamera.h and Inventor/nodes/SoOrthographicCamera.h).

103

8. General Information

8.12. Updating the viewer In order to give the user an impression of the status of the current computation it is useful to update the display during the computation every once in a while. This might be done by calling the viewers render function and telling Avizo that the data has been changed. In this case Avizo will send an update command to all connected modules in the work area which in turn will update their computations and visualizations. #include #include #include ... // field could be lobus.am HxUniformScalarField3 *field = (HxUniformScalarField3 *)portData.source(); if(!field){ theMsg->printf("error: no data object connected"); return; } // we will update this viewer HxViewer *viewer = theController->getCurrentViewer();

1 2 3 4 5 6 7 8 9 10 11 12 13

const int *dims = field->lattice.dims(); // get the dimensions of lobus.am float value[1]; // we will assume scalar values // loop and update the data theWorkArea->startWorking("Computing..."); for(int i=0;isetProgressValue((1.0*i)/10000.0); value[0] = (rand()/(pow(2,15)-1.0)) * 255.0; // generate a value // and pic an arbitrary position in the field to update with value[0] field->lattice.set( (int)(rand()/(pow(2,15)-1) * (dims[0]-2)), (int)(rand()/(pow(2,15)-1) * (dims[1]-2)), (int)(rand()/(pow(2,15)-1) * (dims[2]-2)), &value[0]); if((i % 10) == 0){ // update only every 10th iteration field->touch(); // tell Avizo that the data has changed field->fire(); viewer->render(); // now update the first viewer } } theWorkArea->stopWorking();

This solution will update the viewer window during the computation but it will not allow the user to interact. In order to perform a computation in the background you need to read on to the next section in which we introduce a multi-threaded approach.

8.13. Multi-threading Threads are used in Avizo in the following way:

104

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

8.13. Multi-threading

#include ... myModule::MyThread::MyThread(myModule *cp, int param){ me = cp; // remember the calling object something = param; // remember some parameters } myModule::MyThread::~MyThead(){ } void myModule::MyThead::run(){ me->DoSomething(something); }

1 2 3 4 5 6 7 8 9 10 11

A thread can be called from this code using MyThread *mt1 = new MyThread(this,1); MyThread *mt2 = new MyThread(this,2); mt1->start(); mt2->start(); while(!mt1->wait(300)); while(!mt2->wait(300));

1 2 3 4 5

Here is the definition of the MyThread class. You can copy something like this into your module and call it similar to the code above. class MyThread : public HxThread { private: MyThread(){}; public: MyThread( myModule *me, int direction ); ~MyThread();

1 2 3 4 5

virtual void

run(); // abstract from base class

6 7

int something; // my player object hxConePlot * coneplot; };

8 9 10 11

There is more to threading than just starting and stopping some background jobs. We also need to communicate between the threads. There is a whole lot of stuff that could be told here but for the moment just look up things like mutex.lock, mutex.unlock, and wait.wakeOne. Especially the lock and unlock commands should be used if you write some global variables in the threads. Here is now a full example program doing three things, one is it it using three threads to generate data, do computation of the data (a smoothing) and it saves the data as LargeDiskData. ///////////////////////////////////////////////////////////////// /*

1 2

105

8. General Information * Avizo example source to run three seperate threads for * computation, display and online data aquisition. (Hauke) */ ///////////////////////////////////////////////////////////////// #include #include #include #include // those are for the compute part (convolution) #include #include #include

3 4 5 6 7 8 9 10 11 12 13 14 15

#include

16 17

// a timer event in the background doing nothing so far #include

18 19 20

HX_INIT_CLASS(hxMajoTest2,HxCompModule)

21 22

hxMajoTest2::MyThread::MyThread(hxMajoTest2 *cp, int val){ SoDB::threadInit(); // Inventor scene graph ready, // start Avizo with -mt in order to make it thread save onlineData = cp; type = val; }

23 24 25 26 27 28 29

#define #define #define #define #define

lockthread threadmutex->lock();//SoDB::writelock(); unlockthread threadmutex->unlock();//SoDB::writeunlock(); sleepread 1000 sleepcompute 1000 sleepupdate 1000

30 31 32 33 34 35

hxMajoTest2::MyThread::~MyThread(){}

36 37

void hxMajoTest2::MyThread::run(){ switch(type) { case MyThread::READ_THREAD: onlineData->readThread(); break; case MyThread::COMPUTE_THREAD: onlineData->computeThread(); break; case MyThread::DISPLAY_THREAD: onlineData->displayThread(); break; default: theMsg->printf("error: unknown job type"); } }

38 39 40 41 42 43 44 45 46 47 48 49 50

// its actually not reading by generating data here void hxMajoTest2::readThread(){

106

51 52

8.13. Multi-threading // reading data from disk theMsg->printf("thread reading..."); while(!output) HxThread::msleep(500); theMsg->printf("ok, we got the output");

53 54 55 56 57 58

// now fill it every so often with some new data while(!0){ HxThread::msleep(sleepread); const int *dims = output->lattice.dims(); // pick a slice to fill int whichSlice = (int)(rand()/((float)pow(2.0,15.0)-1.0)* (dims[2]-1.0));

59 60 61 62 63 64 65 66

// use the mutex to lock an area in the code which // we have to handle alone lockthread; int k = whichSlice; for(int j=0;jprintf("updated the data..."); }

67 68 69 70 71 72 73 74 75 76 77 78

}

79 80

// do a smoothing on the data every some seconds void hxMajoTest2::computeThread(){ // wait till we get the output data object while(!output) HxThread::msleep(5000); // do some computation on the data... like convolve with a filter while(!0){ HxThread::msleep(sleepcompute); smooth(output); // convolution with a Gaussian theMsg->printf("data smoothed..."); } }

81 82 83 84 85 86 87 88 89 90 91 92 93

// very simple just update the display at certain intervals void hxMajoTest2::displayThread(){ // update the display modules theMsg->printf("thread display..."); while(!output){ HxThread::msleep(500); } theMsg->printf("ok, we got the output"); while(!0){

94 95 96 97 98 99 100 101 102

107

8. General Information HxThread::msleep(sleepupdate); lockthread; output->touch(); // update display modules output->fire(); theMsg->printf("updated display..."); unlockthread; }

103 104 105 106 107 108 109

}

110 111

// do some advanced computation void hxMajoTest2::smooth(HxUniformScalarField3 *data){ const int *dims = data->lattice.dims(); unsigned char *imgBuffer1 = (unsigned char *)malloc( dims[0]*dims[1]*dims[2]*sizeof(double)); McTypedPointer *i1Ptr = new McTypedPointer(imgBuffer1, McPrimType::mc_double); McTypedData3D *i1 = new McTypedData3D(dims[0],dims[1],dims[2],*i1Ptr); // copy data into field lockthread; memcpy((double *)i1Ptr->data, data->lattice.dataPtr(), dims[0]*dims[1]*dims[2]*sizeof(double)); unlockthread;

112 113 114 115 116 117 118 119 120 121 122 123 124 125

ImGaussFilter3D *filter = new ImGaussFilter3D(); int kernelsize[] = {3,3,3}; float sigma[] = {0.6,0.6,0.6}; filter->setParams(kernelsize,sigma); filter->apply3D(i1,NULL);

126 127 128 129 130

lockthread; memcpy(data->lattice.dataPtr(), (double *)i1Ptr->data, dims[0]*dims[1]*dims[2]*sizeof(double)); unlockthread; free(imgBuffer1); }

131 132 133 134 135 136 137

void oneShot(void * data, SoSensor* sensor){ hxMajoTest2* onlineData = (hxMajoTest2*)data; theMsg->printf("time step in oneShotCB %ld", ((hxMajoTest2 *)data)->counter++); }

138 139 140 141 142 143

void hxMajoTest2::startThreads(void *data){ MyThread * mt1 = new MyThread( this, MyThread::READ_THREAD ); MyThread * mt2 = new MyThread( this, MyThread::COMPUTE_THREAD ); MyThread * mt3 = new MyThread( this, MyThread::DISPLAY_THREAD ); mt3->start(); mt2->start(); mt1->start(); theMsg->printf("threads are startet..."); }

144 145 146 147 148 149 150 151

hxMajoTest2::hxMajoTest2() :

108

152

8.13. Multi-threading HxCompModule(HxUniformScalarField3::getClassTypeId()), portSize(this, "size", 3), portAction(this,"action")

153 154 155

{

156

portSize.setLabel(0,"x"); portSize.setLabel(1,"y"); portSize.setLabel(2,"z"); portSize.setValue(0,100); portSize.setValue(1,100); portSize.setValue(2,100);

157 158 159 160 161 162 163

portAction.setLabel(0,"DoIt");

164 165

oneShotCallback = new SoTimerSensor(oneShot, this); oneShotCallback->setBaseTime(0.0); oneShotCallback->setInterval(2.0); oneShotCallback->schedule();

166 167 168 169 170

counter = 0; output = NULL; // init, threads will wait for this one

171 172 173

// we will use this Mu_tually Ex_clusive object to handle the single // threads work on the output data object threadmutex = new HxMutex();

174 175 176

}

177 178

hxMajoTest2::~hxMajoTest2() { }

179 180 181 182

void hxMajoTest2::compute() { if (!portAction.wasHit()) { return; } startThreads(this);

183 184 185 186 187 188 189

const int dims[] = {100,100,100}; output = dynamic_cast(getResult()); if( output && !output->isOfType(HxUniformScalarField3::getClassTypeId()) ) { output = 0; } if( !output ){ output = new HxUniformScalarField3(dims,MC_DOUBLE); }

190 191 192 193 194 195 196 197 198 199

output->setLabel("output"); setResult(output);

200 201

109

8. General Information }

202

8.14. LargeDiskdata Here is an example on how to write data as a LargeDiskData to disk and how to read such data back in. friend class LatticeManager; void hxOnlineData::compute() { if (!portAction.wasHit()) return;

1 2 3 4 5 6

float bbox[6]; const int *dims; int nDataVar = 1;

7 8 9 10

// the data volume on disk that we will use to store the data char *parameter = "c:/bla.data"; HxRawAsExternalData *newlda = NULL; if(!HxResource::loadData(parameter, "AmiraMesh as LargeDiskData")){ theMsg->printf("error: could not find the file \"%s\"", parameter); return; } newlda = dynamic_cast(theObjectPool->nodeList.last()); if(!newlda){ theMsg->printf("error: last object is not RawAsExternalData in object pool"); return; } newlda->getBoundingBox(bbox); dims = newlda->dims();

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

// remember that we load this as large disk data McString loadCommand; loadCommand.printf("load -amLDD %s", parameter); newlda->setLoadCmd(loadCommand, 1);

27 28 29 30 31

// now to read the memory int numSlices = 1; HxExternalData::BlockRequest request(newlda);

32 33 34 35

// loop over all the slices for(int slices=0; slices < dims[2]; slices += numSlices){ int num = numSlices; if(slices + num > dims[2]){ // last block problem num = dims[2] - slices; }

110

36 37 38 39 40 41

8.15. Generating warning messages and simple dialogs request.setSize(dims[0],dims[1], num); request.setOrigin(0,0,slices); if(!newlda->getBlock(&request)){ theMsg->printf("error: could not get data block"); } for(int i=0;iprintf("data is %g", newlda[i]); }

42 43 44 45 46 47 48 49

}

50 51

// now an example how to write from memory to disk const int blockSize[3] = {64, 64, 64}; // lets write a standard block void* block = mcmalloc (sizeof(char) * nDataVar * blockSize[0] * blockSize[1] * blockSize[2]); McRawData::memset (McTypedPointer (block, McPrimType::mc_uint8), 0, blockSize[0] * blockSize[1] * blockSize[2]);

52 53 54 55 56 57 58

int p[3]; for (p[2] = 0; p[2] < dims[2]; p[2] += blockSize[2]) { for (p[1] = 0; p[1] < dims[1]; p[1] += blockSize[1]) { for (p[0] = 0; p[0] < dims[0]; p[0] += blockSize[0]) { int subSize[3];

59 60 61 62 63 64

for (int i = 0; i < 3; i++) { subSize[i] = dims[i] - p[i]; if (blockSize[i] < subSize[i]) { subSize[i] = blockSize[i]; } } request.setBlockOrigin(subSize); request.setDataOrigin(subSize); request.data = block; request.setBlockSize(blockSize); request.setDataSize(blockSize); if( newlda->putBlock(&request) < 0 ) { theMsg->printf("error set external data"); }

65 66 67 68 69 70 71 72 73 74 75 76 77 78

}

79

}

80

}

81

}

82

8.15. Generating warning messages and simple dialogs #include ... if(HxMessage::question("Do you realy want to quit?", "ok", "stop that", 0, 1, 1) == 1) { ...

1 2 3 4

111

8. General Information 5

if (HxMessage::warning ("You selected to display more than 5000 spheres.\n" "Avizo may get very slow.\n" "Do you want to continue?", "Yes", "No", 0, 1) == 1) {...

8.16. Adding additional information to fields The AmiraMesh file format allows to attach any number of parameters to the data in a file. Those parameters are used for example to hold DICOM information or parameters that where used to generate the data icon. In general you will want to document what was done to the data. Here is an example: HxParameter* fftParam = new HxParameter(paramHalfComplex,realSize); fftParam->setFlag(HxParamBase::NO_SETVALUE, 1); fftParam->setFlag(HxParamBase::NO_RENAME, 1); fftParam->setFlag(HxParamBase::NO_DELETE, 1); result->parameters.insert(fftParam);

See also chapter 11 on page 149 for a more complex example involving sub-groups of parameter settings. Parameters can be copied from one file to the next. output->parameters.copy(lineset->parameters);

In order to find a specific parameter bundle you can call it by its name. The following code fragment adds a bundle called LaplacianEigenmaps at the end of the already existing set LineSetDataValues. HxParamBundle* descr = output->parameters.bundle("LineSetDataValues", 1); if(!descr) return; // something is wrong HxParamBundle* val = descr->bundle ("LaplacianEigenmaps", 1); val->set ("LaplacianEigenmaps", descr->nBundles());

Or you can ask for values from a bundle by findString, findColor, findNum, findReal. const char *str = 0; val->findString("LaplacianEigenmaps", str); // str now contains the value of the key LaplacianEigenmaps

8.17. Utility functions Most utility functions are stored in mclib. This package contains some classes that are frequently used in Avizo. A prominent example is the dynamic array class McDArray.

112

6 7 8

8.18. Utilizing Analytical Expressions Additionally to the usual append and insert functions the array can also be sorted. Assume that you have a structure called Component and an array of such structures that you want to sort. The sorting should be done based on a specific field in each component called inputValue. Define first a static compare function that we can use to sort our array. int compare(HxShapeAnalysis::Component const& t1, HxShapeAnalysis::Component const& t2) { if(t1.inputValue < t2.inputValue) return -1; if(t2.inputValue < t1.inputValue) return 1; return 0; } ... McDArray bla; ... bla.sort(compare); // now the array is sorted

8.18. Utilizing Analytical Expressions Expressions are used in Avizo mainly inside the Arithmetic module. It is used to perform (nearly) arbitrary local computations on the values of fields. You can use it for example to calculate a gamma correction on an image (1/exp(gamma)) or to do a exposure limitation (1 − exp(−x ∗ 0.5)) or in the case of LabelFields to extract one or more volume parts based on their material index. Its main limitation is that you can only use it for operations locally to a single voxel. With other interfaces like tcl or MATLAB (CalculusMatlab) you could also do computations like finite differences between neighboring voxels. In order to utilize the analytical expressions you will have to define variables and set their value. After that you can interprete a string which will result in its value. Arithmetic uses this in order to calculate a new field given the current values as variables in the expression to be evaluated. #include #include #include ... McDArray var; // in .h Anna *epxr; ... var = 0; // in constructor expr = 0; var.append(new AnVarnode("u", 0)); var.append(new AnVarnode("v", 0));

1 2 3 4 5 6 7 8 9 10 11

In a compute module we can evalutate an expression by:

113

8. General Information

Anna* oldExpr = expr; // now use the field to set a value const char *expressionString = portText.getValue(); // a HxPortText expr = AnConverter::strToAnna(expressionString, var.size(), var); if (!expr) { expr = oldExpr; theMsg->printf("HxAnnaScalarField3: failed\n"); } else if (oldExpr) delete oldExpr;

1 2 3 4 5 6 7 8 9 10

// now set the variables to their actual values var[0]->setValue(42.0); // u var[1]->setValue(10.0); // v

11 12 13 14

if(expr) // and evaluate the expression value = expr->value();

8.19. Documentation Once written you can add a documentation for your Avizo module. For a loaded module in Avizo you can call the command createDocFile in the Avizo console window. This will create a .doc file (not Microsoft Word but docygen) and png-image files for each port. Move the files to your ${AVIZO LOCAL}/src/mypackage/doc directory. Now edit the .doc file and fill in the information for all the ports, the input connections and the general description. Now you can generate the documentation in its final html format by executing at the command prompt (not the Avizo console) doc2html -a

The executable can be found in your Avizo bin directory. If you move the resulting files to the ${AVIZO ROOT}/share/doc/usersguide/ directory your help will be called and displayed in the Avizo help window if you hit the question sign in the Avizo work window. In the final doc (doxygen) file you can also put in links to other documents a \link{HxArbitraryCut}{a link to the cutting module}

will produce such a link. One of the great points about the doxygen is that you can actually put in latex commands to describe math formulas. Avizo contains its own http-server which handels the help pages. Because of this sometimes you will see network activity of Avizo. The server can be started or stopped by httpd {start|quit}

114

15 16

9. Compute modules Avizo compute modules are used to perform computations on one or more input objects. The result is usually saved in an output object created by the module and re-used if accessible. Here is the general framework void hxSomeClass::compute() { if(!portAction.wasHit()) // continue only if the user presses doit return;

1 2 3 4

HxUniformColorField3* field = (HxUniformColorField3*) portData.source(); if(!field) { theMsg->printf("Error: not connected to a color field."); return; }

5 6 7 8 9 10

float bb[6]; field->getBoundingBox(bb); /// < physical size const int *dims = field->lattice.dims(); /// < number of voxel

11 12

Once the input is accessible we can create the output object our re-use a priviously conneced module: // int dims[3] = { 100, 100, 100 }; 1 HxUniformScalarField3 *output = dynamic_cast(getResult()); 2 3

if( output && !output->isOfType(HxUniformScalarField3::getClassTypeId()) ) { output = 0; } if( output ){ const int* outdims = output->lattice.dims(); if( dims[0] != outdims[0] || dims[1] != outdims[1] || dims[2] != outdims[2] || field->primType() != output->primType() ){ output = 0; } } if( !output ){ output = new HxUniformScalarField3(dims,MC_DOUBLE); } // now copy the bounding box from the input to the result output->lattice.coords().setBoundingBox(bb);

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

If the dimension of the output changes we will create a new data object. The old data object will be still in the workspace. You can try to remove it but in this case all the

115

9. Compute modules attached display modules will be removed from the workspace as well (which is not good style). Nevertheless here is the code: if(!output || tmpdims[0] != outdims[0] || tmpdims[1] != outdims[1] || tmpdims[2] != outdims[2]){ removeFormerResult = 1; ... // after the generation of the data if(removeFormerResult){ HxObject* obj = getResult(); if(obj){ theObjectPool->removeObject(obj); } } setResult(output); output->setLabel("OutputObject");

1 2 3 4 5 6 7 8 9 10 11 12 13

In modules derived from HxCompModule you can use the setResult call to register the data in the object pool. A probably sequence looks like this output->composeLabel(inputData->getName(),"_modified"); setResult(0,output);

For multiple outputs increase the 0 to 1 and so on. If you use several result objects special care has to be taken to allow a correct saving and restoring of network files. If for example the output can be re-created from the module then the module should state so in the canCreateData function. Here an example which ’guesses’ the output object port based on the icon name: /** this is a virtual function */ int HxExtractEigenvalues::canCreateData(HxData* data, McString& createCmd) { McString dn(data->getName()); bool res = false; if(dn.matches("*_evec1")){ // only for this one we will create the outputs createCmd.printf("{%s} action hit; {%s} fire; {%s} getResult 0\n", getName(), getName(), getName()); res = (HxCompModule::getTouchTime(0) == data->getTouchTime()); } if(dn.matches("*_evec2")){ // here we assume that the output exists already createCmd.printf("{%s} getResult 1\n", getName()); res = (HxCompModule::getTouchTime(1) == data->getTouchTime()); } if(dn.matches("*_evec3")){ createCmd.printf("{%s} getResult 2\n", getName()); res = (HxCompModule::getTouchTime(2) == data->getTouchTime()); } if(dn.matches("*_evals")){

116

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

9.1. Adding another field as input port createCmd.printf("{%s} getResult 3\n", getName()); res = (HxCompModule::getTouchTime(3) == data->getTouchTime());

19 20

} return res;

21 22

}

23

If you do not derive your object from HxCompModule and you still want to generate an output object you will have to use theObjectPool->addObject(output);

. The object will be placed in the workspace but no blue line will indicate that your module generated the data.

9.1. Adding another field as input port The special input port called portData can be taught to accept multiple types of input data. So your program could connect at one time to a lattice at another time to a surface or cluster. The list of classes that a port can be connected to is specified two times. In the .rc file of your module you need to add the classes to the ’-primary’ value (a list of C++ class names separated by spaces). In your class definition file you can add the new type to the constructor as in: portData.addType(HxCluster::getClassTypeId());

// add a cluster as pot. input

Now it is important to decide at one point in your program if the input is one or the other type. This can be done with a construct like the following: HxUniformScalarField3* field = static_cast (portData.source(HxUniformScalarField3::getClassTypeId ())); HxCluster *cluster = static_cast (portData.source(HxCluster::getClassTypeId ())); if(field){ // we have a scalar field as input ... } else { // we have a cluster as input ... }

Another way to extend the module is to use dedicated ports for different tasks. This example shows how to add more inputs to a compute module. The module in fact has several inputs that appear as ports (somethimes with and sometimes without a user interface). Usually in compute modules you will get the input data by a call to portData.source(). To add additional input ports you can use the HxConnection class.

117

9. Compute modules

// in the .h file HxConnection portColorField;

1 2 3

// in the .cpp files class constructor someClass::someClass() :HxCompModule(HxField3::getClassTypeId()), portColorField (this,"Colorfield",HxField3::getClassTypeId()), { ...

4 5 6 7

This module can be connected to a HxField3 object (general lattice) because its derived from the HxCompModule and therefore has a build-in port called portData. The data can be obtained by calling (HxField3*)portData.source(). Because it has defined a HxConnection it can also be connected to a ColorField (usually to provide additional color values per voxel). HxField3 *colorfield = (HxField3 *)portColorField.source( HxField3::getClassTypeId()); HxLocation3 *locColorField = 0; int locnDataVar = 0; if(!colorfield) return;

1 2 3 4 5 6 7

locColorField = ((HxField3 *) portColorField.source())->createLocation(); locnDataVar = colorfield->nDataVar(); float *return = (float *)malloc(sizeof(float)*locnDataVar);

8 9 10 11 12

locColorField->set(1,1,1); colorfield->eval(locColorField,result); for(int i=0;iprintf("Value in Colorfield at point 1,1,1,(%d) is %g\n", i, result[0]);

13 14 15 16 17

We added here also an example how to query data from the connected field. Because we used a more general class like HxField3 we may encounter different kinds of grid-types attached to our module. The use of HxLocation3 allows us to abstract from all those different cases. Instead we ’re-sample’ the data and can obtain values for any point we like (HxLocation3 uses a by default a tri-linear interpolation method to return appropriate values). The resource file for this project looks like this: module -name "someClass" \ -primary "HxField3" \ -class "someClass" \ -category "Display" \ -package "hxsomeClass"

118

1 2 3 4 5

9.1. Adding another field as input port The category section reflects the context menu after right-mouse-click on the data item. If there is no primary section, i.e., the module will not be connected to an input data object, category will be the section in the create menu.

9.1.1. Correct behaviour for saving networks Your module needs to tell Avizo what output it needs to save during a ’network save’ process. This will reduce the number of modules that your network needs to store if data can be created on the fly instead of beeing copied to disc. This is done by the method savePorts. Usually the method savePorts of the parent class should be called in order to save all standard ports of the interface. But, in case that you have defined some tcl-commands and would like to save by them status information of your module you will need to add to the function. void HxMyModule::savePorts(FILE* fp){ fprintf(fp, "{%s} setHiddenVariable %d\n", getName(), 42); fprintf(fp, "{%s} fire\n", getName()); HxCompModule::savePorts(fp); // saves all standard ports }

1 2 3 4 5

In order to tell the Avizo runtime what outputs can be created and which outputs needs to be save seperately use a command like: int MyModule::canCreateData(HxData* data, McString& createCmd) { createCmd.printf("{%s} compute; {%s} getResult\n", getName(), getName()); return (resultTouchTime == data->getTouchTime()); }

1 2 3 4 5

The createCmd definition will be used to create the modules output, thus should return the name of the connected output object.

9.1.2. Adding an almost infinite number of ports Of course you cannot add an infinite number of input ports, because it would be a waste of resources. But we can add 1 more that is actually used - at every point in time. Here is an example: int freeportthere = 0; for(int i=0;isource() == NULL){ freeportthere = 1; break; } else { connectionPorts[i]->rename("closedPort"); } }

1 2 3 4 5 6 7 8

119

9. Compute modules if(!freeportthere){ // this memory will never get freed new HxConnection(this,"freePort",HxObject::getClassTypeId()); }

9 10 11

As you can see this code has the slight problem that the new connection object will only get free’ed if we close our application but not if we close the module. Also we have to be careful now to allow the module to be saved as a network file. The network file lists the connections present at the time that the network was saved which includes the connection generated in, for example the update() method. During reading of the network file update() will not be called after each data set is connected, thus some ports might not exist (not yet created) during the network load. In order to allow the module to behave correctly we can introduce another tcl command that specifies the number of ports the module should expose. We will call this function before we connect the data items. Here is the definition of the tcl-command in the parse() function of our module. } else if ( CMD2("createMorePorts", "createMorePorts") ) { 1 ASSERTARG(3); 2 int howManyMore = atoi(argv[2]); 3 if(howManyMore < 0) 4 howManyMore = 0; 5 char str[256], str2[256]; 6 for(int i = 0; i < howManyMore; i++){ 7 // first the HxConnection 8 sprintf(str, "S%d", (int)connectionPorts.size()); 9 HxConnection *t = new HxConnection(this,str,HxLattice3::getClassTypeId()); 10 t->addType(HxStackedScalarField3::getClassTypeId()); 11 portConnectionList.append(t); 12 13

// now the interface (three scalars) sprintf(str2, "G%d", (int)connectionPorts.size()-1); HxPortFloatTextN *tt = new HxPortFloatTextN(this, str2, 3); tt->setValue(0,1.0); // others are by default 0 portGradientList.append(tt); tt->hide(); // enable this port if there is a connected object }

14 15 16 17 18 19 20

Now this function needs to be called when we save a network. We can overwrite the savePorts function: void HxComputeTensor::savePorts(FILE* fp) 1 { 2 fprintf(fp, "{%s} createMorePorts %d\n", getName(), connectionPorts.size()-8);3 fprintf(fp, "{%s} fire\n", getName()); 4 5

HxCompModule::savePorts(fp); // do your usual work }

120

6 7

9.2. Adding a region of interest input

9.2. Adding a region of interest input Regions of interest give the user more control over your module. To extend a given project to the use of regions of interest is very easy. // in the .h file #include ... HxRoiInterface.h ... HxConnection portROI;

1 2 3 4 5 6 7

// in the constructor someClass::someClass(): HxCompModule(HxUniformScalarField3::getClassTypeId()), portROI (this,"ROI",HxRoiInterface::getClassTypeId()), ...

8 9 10 11 12

The values from the region of interest (portROI) can be accessed like this (in compute()): float bb[6]; field->getBoundingBox(bb); HxRoiInterface* roi = (HxRoiInterface *) portROI.source(HxRoiInterface::getClassTypeId()); if (roi) { // if we found a region of interest, use its bounding box roi->getRoi(bb); // instead of the original bounding box } // now we have to restrict our computation to the bb

1 2 3 4 5 6

The main point here is to show that the compute module has to be changed only minimally to add the required functionality in case a region of interest if present.

9.3. User defined tcl commands – the parse function The parse function defines additional tcl-commands to Avizo modules. It is executed whenever the user calls the function on the command line in the console window. int hxLua::parse( Tcl_Interp * t, int argc, char ** argv ){ char *cmd = argv[1];

1 2 3

if (CMD("exec")){ ASSERTARG(3); // automatic error message if no 2 arguments are given theMsg->printf("execute this: \"%s\"",argv[2]); } else HxCompModule::parse(t,argc,argv); // everything else is // handeled by the global compute module

4 5 6 7 8 9 10

return TCL_OK;

11

}

12

121

9. Compute modules Please note that the theMsg call will not result in proper values returned in the Avizo console window (tcl). In order to return tcl values you will have to use one of the following functions: Tcl_VaSetResult(t,"%d",splineOrder);

or for a list of return values: for(int i=0;iregisterFileType(); 15 theFileDialog->registerFileType("Avizo script", ".hx"); 16 theFileDialog->registerFileType("Avizo script and data files (pack & go)",17".hx"); const char* format = theFileDialog->getFileType(); /// < this crashes! 18 if (format && strstr(format,"data files")) // pack & go 19 saveFlags |= HxObjectPool::SAVE_LOADABLE; 20 } 21 theObjectPool->saveState( m_networkPath, dataDir, saveFlags ); 22

123

9. Compute modules

124

10. Data import and export 10.1. AmiraMesh as a general purpose file format AmiraMesh can save an arbitrary number of fields into a single file. It will keep all the bounding box information and additional parameter sections in the files as well. AmiraMesh am; /* lets save the following two fields into the AmiraMesh file McDArray mean; // this is a simple linear array McDArray weights; // this is 2D */

1 2 3 4 5

// a location will keep the information about the number of voxels 6 AmiraMesh::Location *loc1 = new AmiraMesh::Location("meanD", mean.size()); 7 am.insert(loc1); 8 // each location has a data section attached 9 AmiraMesh::Data* d1 = new AmiraMesh::Data("mean", loc1, 10 McPrimType::mc_float, 1, mean.dataPtr()); // note that ’1’ here refers to 11 // scalar data in contrast to 12 // 3 which would be vector data 13 am.insert(d1); 14 15

int weightDim[2]; // the size of the 2D array 16 weightDim[0] = weights.size(); weightDim[1] = weights[0].size(); 17 AmiraMesh::Location *loc4 = new AmiraMesh::Location("weightD", 2, weightDim); 18 am.insert(loc4); 19 // for the data we need a linear memory area so we have to copy the data over 20 float *d4linear = (float *)malloc(sizeof(float)*weightDim[0]*weightDim[1]); 21 for(int i=0;ifindLocation("meanD"); if(loc1 && loc1->nDim() != 1){ theMsg->printf("error reading meanD: meanD is not there or not 1D"); return; } const int* meanDims = loc1->dims(); AmiraMesh::Data* meanData = am->findData("meanD", HxFLOAT, 1, "mean"); if(!meanData){ theMsg->printf("error: could not read mean array"); return; } mean.resize(0); // remove the old stuff mean.append(meanDims[0],(float *)(meanData->dataPtr()));

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

// read in the weights array 18 AmiraMesh::Location* loc4 = am->findLocation("weightD"); 19 if(loc4 && loc4->nDim() != 2){ 20 theMsg->printf("error reading weightD: weightD is not there or not 2D"); 21 return; 22 } 23 const int* weightsDims = loc4->dims(); 24 AmiraMesh::Data* weightsData = am->findData("weightD", HxFLOAT, 1, "weights");25 if(!weightsData){ 26 theMsg->printf("error: could not read weights array"); 27 return; 28 } 29 weights.resize(weightsDims[0]); // resize to new size 30 for(int i=0;idataPtr())[i*weightsDims[1]], 33 sizeof(float)*weightsDims[1]); 34 } 35 delete am; 36

10.2. Read data sets 10.2.1. Formating your own data Here is a common question regarding reading in of density data: > I have a data file that contains [ix iy iz scalar] in sequence, > ( (ix=1,ngrid_x), iy=1,ngrid_y), iz=1,ngrid_z) > E.g. > ix iy iz scalar

126

1 2 3 4

10.2. Read data sets > 1 1 1 .32772E-12 > 2 1 1 .72553E-05 > 3 1 1 .30395E-04 > ....... > > I’d like to load it and visualize it in 3D in Avizo.

5 6 7 8 9 10

Perhaps the simplest way to read in this kind of data is to add an Avizo header to the file. As we can see the data is sorted so that the x-index is the fastest running. This is also the standard format for Avizo’s internal data structure. So we can remove the first three columns from the file (the ones containing the coordinates) and Avizo will assume the correct locations for our data. This leaves only the data values in their order. Now we add the following header: # AmiraMesh 3D ASCII 2.0

1 2

define Lattice ngrid_x ngrid_y ngrid_z

3 4

Parameters { BoundingBox -1 1 -1 1 -1 1 CoordType "uniform" }

5 6 7 8 9

Lattice { float Data } @1

10 11

@1 .32772E-12 .72553E-05 ...

12 13 14 15

You will have to change the names ngrid x,y, and z in this example into the actual values for the number of points in x, y, and z direction in order be able to load the file. We could also have used binary data which takes up less space. In this case only the header signature needs to be changed to # AmiraMesh 3D BINARY 2.0

Conversion between ASCII and BINARY data can be performed in Avizo by the build-in program aminfo. For example type in the Avizo console window aminfo -a outputfile.am inputfile.am to save the input file in ascii format. To read in a vector field header we need to change the header into the following way: # AmiraMesh ASCII 1.0 define Lattice 30 30 30

127

10. Data import and export Lattice { float[3] Coordinates } = @1 Parameters { CoordType "uniform", BoundingBox 0 1 0 1 0 1 } @1 1 1 1 2 1 1 ...

10.2.2. Convert binary to ascii data Included in Avizo is a program which is called aminfo. Its also available from the home page of Avizo. On the command line it can be called by Usage: aminfo [-a outfile|-b outfile] Prints ascii header of an AmiraMesh file. Convert AmiraMesh to ASCII/BINARY format. -a writes whole file in ascii format. -b writes whole file in binary format.

It is important to do (i) the order of the arguments correct AND (ii) to be in the correct directory. Alternatively one can also use explicit path names. So assume you have a file called Result.am in the current working directory (use the ’pwd’ command in the Avizo console window to make sure you where you are, otherwise use ’cd’ to go where the data resides on disk). Now: aminfo -a ASCIIResult.am Result.am

converts the file Result.am into an equivalent ascii coded file ASCIIResult.am. This may take a while because of the usually very large file size of ascii files. Please note that if you switch the last two arguments nothing will happen (also no error message will be produced). If you want to convert a series of files, like all files in the current working directory you can use a tcl loop like the following which converts all files with the extension .am into their ascii file equivalent. foreach u [glob *.am] {aminfo -a ASCII$u $u }

10.2.3. Read in Curvilinear Fields Curvilinear fields are specified as lists of data plus lists of coordinates of the data. Here is an example file for a vector field defined on a curvilinear grid: # AmiraMesh 3D ASCII 2.0

1 2

define Lattice 2 2 2

128

3

10.2. Read data sets 4

Parameters { CoordType "curvilinear" }

5 6 7 8

Lattice { float[3] Data } @1 Lattice { float[3] Coordinates } @2

9 10 11

# Data section follows @1 -1 1 1 -1 -1 1 1 1 1 1 -1 1 -1 1 -1 -1 -1 -1 1 1 -1 1 -1 -1

12 13 14 15 16 17 18 19 20 21 22

@2 1 2 1 3 0 2 0 3 1 2 1 3 0 2 0 3

23

0 0 0 0 1 1 1 1

24 25 26 27 28 29 30 31

In this file we define a cube of size 2 × 2 × 2 with grid positions x, y, z where x ∈ [0..1], y ∈ [2..3], and z ∈ [0..1]. The vectors attached to each grid position are pointing into the center of the cube. Each entry in the list represents either a coordinate or a data entry for a single point in space. The two lists are sorted, i.e., the first three elements in the data section are the vector components at the coordiante specified by the first three numbers in the coordiante section. Basically you can use the HxRegVectorField3 class and specify the coordinates as HxCurvilinearCoord3. Here is an example source code, you will have to add the extra code for the actual read-in of the values from the file. This example shows only how to fill the internal Avizo structures. #include #include #include #include #include



1 2 3 4 5 6

READCURVELINEARFIELD_API int readCurvelinearField(const char* filename) {

7 8

129

10. Data import and export FILE* fp = fopen(filename,"rb"); if (!fp) { theMsg->ioError(filename); return 0; }

9 10 11 12 13

int dims[3]; dims[0]=42; dims[1]=42; dims[2]=42; int npoints = dims[0]*dims[1]*dims[2]; // number of points

14 15 16

// space for the xyz coordinates (!add check for memory) float* xyz = (float *) malloc(3*npoints*sizeof(float));

17 18 19

// fill the coordinate values int count = 0; for(int k=0;klattice.dataPtr(); for(int i=0;i QxPreferenceDialog::memoryLimitMB){ // ask for the dialog about how to read the data QxDataConvertDialog convertDlg; McFilename fname(filename); McString dirPath; fname.dirname(dirPath); convertDlg.setDestFileDirectory(dirPath.getString()); bool failed = false; if(convertDlg.exec() == QDialog::Rejected) { return 0; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

howToRead = convertDlg.getChoice(); if(howToRead == 0){ // VolumeViz sprintf(out, "%s", convertDlg.getDestFilePath().ascii()); }

19 20 21 22

}

23

VolumeViz: The following example shows how to read in a file from disk which consists out of some header part and the raw data. McString *convertRawDataWithHeaderToLDM(const char *filename, int *dims, float *bbox, McPrimType type, int isLittleEndian, int header) { McFilename fn(filename); McString inBaseName(fn.basename()); McString lstFilenameStr; lstFilenameStr += fn.getString(); int dotPos = inBaseName.index(".", 0); int i; char* ibn = inBaseName.getString(); for (i = 0; i < dotPos; i++) lstFilenameStr += ibn[i]; lstFilenameStr += ".lst";

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

McFilename outFilenameStr(filename); outFilenameStr.replaceSubStr(outFilenameStr.extension(), ".lda"); char *out = outFilenameStr.getString();

16 17 18

131

10. Data import and export 19

FILE* fp = fopen(lstFilenameStr.getString(), "w"); if (!fp) { theMsg->printf("Could not create list file in %s", lstFilenameStr.getString()); return NULL; // error! } HxVolumeDataObject volDataObj; bool isDataTypeOK = true; int dataType; switch (type) { case McPrimType::mc_uint8 : dataType = 0; break; case McPrimType::mc_int16 : dataType = 5; break; case McPrimType::mc_uint16 : dataType = 1; break; case McPrimType::mc_int32 : dataType = 6; break; case McPrimType::mc_float : dataType = 10; break; default : isDataTypeOK = false; break; } if(isDataTypeOK == false){ theMsg->printf("error: could not recognize the data type"); return NULL; }

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

int ret = 0; ret = volDataObj.convertToLDA(filename, out, bbox[0], bbox[2], bbox[4], bbox[1], bbox[3] , bbox[5], dataType, dims[0], dims[1], dims[2], header);

54 55 56 57 58 59

if(!ret) return NULL; // error else return new McString(out); }

Please note that there is also a second function convertToLDA() which gets a control file from disk. This file contains the information needed to do the conversion, bounding box information etc. This version of convertToLDA() apparently only works with series of image

132

60 61 62 63 64

10.2. Read data sets files (known to the VolumeViz extension). So for loading single raw data files the above mentioned way is working. In order to detect if the current machine is using little or big endian one can use this handy two liner int isLittleEndian = 1; isLittleEndian = *((char *)&isLittleEndian);

LargeDiskData: Here an example which is reading in blocks of data from disk. The data is mapped to a memory area where it can be changed or visualized. float bbox[6]; const int *dims; int nDataVar = 1;

1 2 3 4

// the data volume on disk that we will use to store the data char *parameter = "c:/bla.data"; HxRawAsExternalData *newlda = NULL; if(!HxResource::loadData(parameter, "AmiraMesh as LargeDiskData")){ theMsg->printf("error: could not find the file \"%s\"",parameter); return; } newlda = dynamic_cast (theObjectPool->nodeList.last()); if(!newlda){ theMsg->printf("error: last object is not RawAsExternalData"); return; } newlda->getBoundingBox(bbox); dims = newlda->dims();

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

// remember that we load this as large disk data McString loadCommand; loadCommand.printf("load -amLDD %s", parameter); newlda->setLoadCmd(loadCommand, 1);

21 22 23 24 25

// now to get the data into memory int numSlices = 1; HxExternalData::BlockRequest request(newlda);

26 27 28 29

// loop over all the slices for(int slices=0; slices < dims[2]; slices += numSlices){ int num = numSlices; if(slices + num > dims[2]){ // last block problem num = dims[2] - slices; } request.setSize(dims[0],dims[1], num); request.setOrigin(0,0,slices); if(!newlda->getBlock(&request)){

30 31 32 33 34 35 36 37 38

133

10. Data import and export theMsg->printf("error: could not get data block"); } for(int i=0;iprintf("data is %g", newlda[i]); } }

39 40 41 42 43 44 45

// now an example how to write from memory to disk const int blockSize[3] = {64, 64, 64}; // lets write a standard block void* block = mcmalloc (sizeof(char) * nDataVar * blockSize[0] * blockSize[1] * blockSize[2]); McRawData::memset (McTypedPointer (block, McPrimType::mc_uint8), 0, blockSize[0] * blockSize[1] * blockSize[2]);

46 47 48 49 50 51 52

int p[3]; for (p[2] = 0; p[2] < dims[2]; p[2] += blockSize[2]) { for (p[1] = 0; p[1] < dims[1]; p[1] += blockSize[1]) { for (p[0] = 0; p[0] < dims[0]; p[0] += blockSize[0]) { int subSize[3]; for (int i = 0; i < 3; i++) { subSize[i] = dims[i] - p[i]; if (blockSize[i] < subSize[i]) { subSize[i] = blockSize[i]; } } request.setBlockOrigin(subSize); request.setDataOrigin(subSize); // combined as setOrigin request.data = block; request.setBlockSize(blockSize); request.setDataSize(blockSize); if( newlda->putBlock(&request) < 0 ) { theMsg->printf("error in setting external data"); } } } }

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

10.2.5. Read in time dependent data Time dependent data is handled in Avizo by the TimeSeriesCtrl module. // generate the control and add it to the workspace HxDynamicSeriesCtrl* ctrl = new HxDynamicSeriesCtrl; McDArray timeSteps(numOfVolumes); char ctrlfn[1024]; sprintf(ctrlfn, "%s-Ctrl", McFilename::basename(filename)); ctrl->setLabel(ctrlfn); timeSteps.resize(0); theObjectPool->addObject(ctrl);

1 2 3 4 5 6 7 8

134

10.2. Read data sets // now interate over all the data files for(int numVols = 0; numVols < numOfVolumes; numVols++){ long skip = numVols * (dims[0]*dims[1]*dims[2]*sizeOfNf); // read in a single volume (magic function) HxLattice3 *lattice = readData(qualifiedFn, dataformatstr, "xfastest", typestr, 1, dims[0], dims[1], dims[2], bbox[0], bbox[1], bbox[2], bbox[3], bbox[4], bbox[5], skip); if(!lattice){ // a fail-save mechanism that adds the current directory McFilename *fn = new McFilename(files[i]); fn->dirname(qualifiedFn); qualifiedFn += filename; lattice = readData(qualifiedFn, dataformatstr, "xfastest", typestr, 1, dims[0], dims[1], dims[2], bbox[0], bbox[1], bbox[2], bbox[3], bbox[4], bbox[5], skip); } if(!lattice){ theMsg->printf("error: could not read \"%s\"", qualifiedFn.getString()); return 0; } HxUniformScalarField3 *obj = new HxUniformScalarField3(lattice);

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

// now lets append all the parameters and their values to the file // we have build before a dynamic array ’valuePairs’ that // contains all the information HxParamBundle* bundle = obj->parameters.bundle("INTERFILE", 1); char linestr1[1024]; char linestr2[1024]; for(int pa=0; painsert(new HxParameter(linestr1,linestr2)); } McString loadCommand; loadCommand.printf("load -interfile %s", qualifiedFn.getString()); obj->setLoadCmd(loadCommand,1);

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

timeSteps.appendSpace(1); char fnn[1024]; sprintf(fnn, "%s%04d", McFilename::basename(filename), numVols); obj->setLabel(fnn); timeSteps.last().objects.append(obj);

45 46 47 48 49

} ctrl->init(timeSteps); // this is important to let the ctrl know whats // going on

50 51 52

Here is another example that is generating a class DynamicScalarField which helps in creating the appropriate load command for the time series. #include

1 2

135

10. Data import and export #include #include #include #include #include #include #include #include



3 4 5 6 7 8 9 10 11

#define MAX_LENGTH 4096 #define MAX_FIELDS 1024

12 13 14

HX_INIT_CLASS(RajaHxSurface,HxSurface)

15 16

class HxDynamicScalarField : public HxDynamicSeriesCtrl {

17 18

public: HxDynamicScalarField() { }

19 20 21

/* This method should write a Tcl command which creates the object. In this case the data file is read. The reader creates this object, the surface, and the surface fields. */ virtual int saveCreation(FILE* fp, const char*, int) { if (fp) { fprintf(fp, "load -m %s\n", filename.dataPtr()); } return 0; // Indicates, that no autosaving is needed }

22 23 24 25 26 27 28 29 30 31 32

// The name of the data file is stored here McString filename; };

33 34 35 36

RAJAM_API int RajaRead(int n, const char** files){ int i,j,k; for (i=0; iioError(files[i]); return 0; } char buffer[MAX_LENGTH]; fgets(buffer,MAX_LENGTH,fp); // Skip first line (Title) fgets(buffer,MAX_LENGTH,fp); // Get field infos int start = 23; // We just skip the begining of this line... int end; int nFields = 0; char fieldName[MAX_FIELDS][128]; while (start < strlen(buffer)) {

136

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

10.2. Read data sets end = start+1; while (buffer[end] != ’,’ && end < strlen(buffer)) end++; strncpy(fieldName[nFields],&buffer[start+2],end-start-3); fieldName[nFields][end-start-3] = 0; start = end; nFields++;

53 54 55 56 57 58

} fieldName[nFields-1][strlen(fieldName[nFields-1])] = 0; theMsg->printf("#Fields = %d\n",nFields);

59 60 61 62

int nPoints=0, nTriangles=0; // Get number of points and triangles fgets(buffer,MAX_LENGTH,fp); sscanf(buffer,"ZONE T=\"TRIANGLES\", N=%d, E=%d, F=FEPOINT, ET=TRIANGLE",&nPoints, &nTriangles); theMsg->printf("#Points %d #triangles %d\n",nPoints,nTriangles);

63 64 65 66 67 68 69

RajaHxSurface* surface = new RajaHxSurface; // create new surface surface->addMaterial("Inside",0); // add some materials surface->addMaterial("Outside",1);

70 71 72 73

HxSurface::Patch* patch = new HxSurface::Patch; surface->patches.append(patch); // add patch to surface patch->innerRegion = 0; patch->outerRegion = 1;

74 75 76 77 78

surface->points.resize(nPoints); surface->triangles.resize(nTriangles);

79 80 81

McDArray timeSteps(nFields);

82 83

McDArray data(nFields); for (j = 0; j < nFields; j++) { HxSurfaceScalarField* field = (HxSurfaceScalarField*) HxSurfaceScalarField::create(surface, HxSurfaceScalarField::OnNodes, 1); timeSteps[j].objects.append(surface); timeSteps[j].objects.append(field); field->setLabel(fieldName[j]); data[j] = field->dataPtr(); }

84 85 86 87 88 89 90 91 92 93 94

for (j=0; jpoints[j]; fscanf(fp, "%f %f %f", &p[0], &p[1], &p[2]); for (k = 0; k < nFields; k++) { fscanf(fp,"%f",&data[k][j]); } }

95 96 97 98 99 100 101 102

137

10. Data import and export fgets(buffer,MAX_LENGTH,fp); for (j=0; jtriangles[j]; tri.points[0] = idx[0]-1; // indices should start at zero tri.points[1] = idx[1]-1; tri.points[2] = idx[2]-1; tri.patch = 0; }

109 110 111 112 113 114 115

// Add all triangles to the patch patch->triangles.resize(nTriangles); for (j=0; jtriangles[j] = j;

116 117 118 119 120

surface->setLabel(McFilename::basename(files[i]));

121 122

fclose(fp);

123 124

HxDynamicScalarField *dynScalar = new HxDynamicScalarField; // Let the module know the filename dynScalar->filename = files[i]; dynScalar->setLabel("DynData"); dynScalar->init(timeSteps);

125 126 127 128 129 130

theObjectPool->addObject(dynScalar);

131 132

surface->setFields(&data); } return 1; }

133 134 135 136

Sometimes it is very inefficient to use HxDynamicSeriesCtrl. Especially if the number of time steps is very large operations on the object pool become very slow. In this case it is better to derive directly from the data object and extend its capabilities. Note that using this approach the automatic memory handling (caching) of HxDynamicSeriesCtrl is not available so data needs to fit into memory. class HxMySurfaceScalarField : public HxSurfaceScalarField { public:

1 2 3 4

HxMySurfaceScalarField(HxSurface * surface, Encoding encoding, void *data)5 : HxSurfaceScalarField(surface, encoding, data), 6 portTime(this, "Time") 7 { 8

138

10.2. Read data sets portTime.setMinMax(0,1); portTime.setIncrement(1); myTimePoints.resize(0); numPoints = 0;

9 10 11 12

}

13 14

// makes a copy of the data, use can free data afterwards 15 bool addTimePoint(float *data, int nums, double time){ 16 float *tmp = (float *)malloc(sizeof(float)*nums); 17 if(!tmp){ 18 theMsg->printf("error: not enough memory"); 19 return false; 20 } 21 if(numPoints > 0 && nums != numPoints){ 22 theMsg->printf("error: trying to add %d points, but had 23 %d points previous nums, numPoints); 24 free(tmp); 25 return false; 26 } 27 numPoints = nums; 28 memcpy(tmp, data, sizeof(float)*nums); 29 myTimePoints.append(tmp); 30 myTimeTimes.append(time); 31 portTime.setMinMax(0,myTimePoints.size()-1); 32 return true; 33 } 34 35

void update() { portTime.setMinMax(0,myTimePoints.size()-1); // set the pointer for the actual data int step = portTime.getValue(); // where we should look for the closest data item in time to the current time // (myTimeTimes as lookup into myTimePoints) left as an exercise to the user

36 37 38 39 40 41 42

if(step < 0 || step >= myTimePoints.size()){ theMsg->printf("step %d does not exist (%d..%d)", step, 0, myTimePoints.size()-1); return; } // now copy the current data over memcpy(this->dataPtr(), myTimePoints[step], numPoints); touch();

43 44 45 46 47 48 49 50

}

51 52

HxPortTime portTime; protected:

53 54 55

McDArray myTimePoints; McDArray myTimeTimes; mclong numPoints;

56 57 58

139

10. Data import and export };

59

Using the above class we will end up with a field that contains scalar values per triangle and exposes a time slider to the user that switches the data between the different points in time.

float *data = (float *)malloc(sizeof(float)*surface->triangles.size()); 1 if(!data){ 2 theMsg->printf("Error: not enough memory"); 3 return 0; 4 } 5 fread(v_array, nfloat, totalPoints, fp); 6 for (int i = 0; i < totalPoints; i++) { 7 data[2*i+1] = data[2*i] = v_array[i]; 8 } 9 HxMySurfaceScalarField* ssf = new HxMySurfaceScalarField(surface,HxSurfaceScalarField::On 10 ssf->addTimePoint(data,totalPoints*2,0); // add time point 0 11 ssf->setLabel(McFilename::basename(filefield)); 12 13

theObjectPool->addObject(ssf);

14 15

for (int j = 1; j < nTimes; j++) { 16 theWorkArea->setProgressValue((float)j/(float)nTimes); 17 if(theWorkArea->wasInterrupted()){ 18 theWorkArea->stopWorking(); 19 break; 20 } 21 // read the time variable 22 // read the data values from a file 23 fread(v_array, nfloat, totalPoints, fp); 24 for (int i = 0; i < totalPoints; i++) { // add the data for two triangles 25 ssf->dataPtr()[2*i+1] = ssf->dataPtr()[2*i] = v_array[i]; 26 } 27 ssf->addTimePoint(ssf->dataPtr(), totalPoints*2, time); 28 // add the data to the array 29 } 30 ssf->update(); 31 if(v_array) 32 free(v_array); 33

10.3. Multi-channel fields Multi-Channel fields are data objects that define a set of (single channel) data objects as being connected to each other. Display modules will recognize this connection and allow you do switch on and off channels or to use a specific color encoding each channel. #include #include

140

1 2

10.4. Surface data #include ... HxUniformScalarField3 *field = (HxUniformScalarField3 *)portData.source(); if(!field){ theMsg->printf("error: no data connected"); return; } // generate a multi-channel object and make appear in the object pool HxMultiChannelField3 *mcf = new HxMultiChannelField3(); mcf->setLabel("MultiChannelFieldObject"); theObjectPool->addObject(mcf);

3 4 5 6 7 8 9 10 11 12 13 14

// now add three channels HxUniformScalarField3 *field1 = new HxUniformScalarField3(&field->lattice); HxSpatialData::registerData(field1,"field1"); field1->parameters.setColor(&McColor(1,0,0)[0]); field1->setLabel("field1"); field1->portMaster.connect(mcf);

15 16 17 18 19 20 21

HxUniformScalarField3 *field2 = new HxUniformScalarField3(&field->lattice); HxSpatialData::registerData(field2,"field2"); field2->parameters.setColor(&McColor(0,1,0)[0]); field2->setLabel("field2"); field2->portMaster.connect(mcf); // this connects the first channel

22 23 24 25 26 27

HxUniformScalarField3 *field3 = new HxUniformScalarField3(&field->lattice); HxSpatialData::registerData(field3,"field3"); field3->parameters.setColor(&McColor(0,0,1)[0]); field3->setLabel("field3"); field3->portMaster.connect(mcf);

28 29 30 31 32 33

// to get access to a multi-channel field (its lattice) HxLattice3 *lat1 = &mcf->getChannel(0)->lattice;

34 35

10.4. Surface data Surfaces are described minimally as two lists, one list of 3D coordinates - the coordinates of each vertex - and one list of triangles each described by the three indexes of its vertexes. Here is an example how such a file might look like on disk: # HyperSurface 0.1 ASCII

1 2

Parameters { Materials { Exterior { Id 1 } Green {

3 4 5 6 7 8

141

10. Data import and export Color 0.21622 0.8 0.16 }

9 10

} BoundaryIds { name "BoundaryConditions" } Filename "C:/data/trivialSurface.surf" }

11 12 13 14 15 16 17

Vertices 8 1.000000 0.666667 0.666667 0.500000 1.000000 0.500000 0.500000 1.000000 0.000000 0.000000 0.000000 1.000000 0.500000 0.000000 0.523810 0.523809 NBranchingPoints 0 NVerticesOnCurves 0 BoundaryCurves 0 Patches 1 { InnerRegion Green OuterRegion Yellow BoundaryID 0 BranchingPoints 0

18

0.500000 1.000000 0.000000 0.000000 0.500000 0.500000 1.000000 0.500000

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

Triangles 7 3 1 8 4 3 8 6 4 8 5 6 8 7 5 8 2 7 8 1 2 8 }

37 38 39 40 41 42 43 44 45

It might be useful to be able to attach data to each of the surface vertices. These data could represent the curvature at each vertex or the vertex normals (see the compute modules GetCurvature and SurfaceNormals). Here is an example on such a file that contains vectors at each node. If both files are loaded and the number of nodes in both files are the same Avizo will connect them with a black line indicating that the data values can be visualized on the locations stored in the surface object. # AmiraMesh 3D ASCII 2.0

1 2

nNodes 8

3 4

142

10.4. Surface data Parameters { ContentType "SurfaceField", Encoding "OnNodes" }

5 6 7 8 9

NodeData { float[3] values } @1

10 11

# Data section follows @1 0.28616 -0.953862 0.0908864 0.672649 -0.704687 -0.225741 -0.206371 -0.966829 -0.150505 -0.577045 -0.60496 -0.548674 0.325057 -0.325058 -0.888074 -0.316176 -0.347793 -0.882651 0.772042 -0.456859 -0.441849 0.182139 -0.805094 -0.56449

12 13 14 15 16 17 18 19 20 21

10.4.1. Write a surface In order to write out a surface you only need to construct a writer class. Here an example for writing out an Avizo surface to Matlab. #include #include #include #include



1 2 3 4 5

SAVESURFACETOMATLAB_API int saveSurfaceToMatlab(HxSurface* data, const char* filename) { FILE* fp = fopen(filename,"wb"); if (!fp) { theMsg->ioError(filename); return 0; }

6 7 8 9 10 11

/* Write data into file ... */ fprintf(fp, "%% this is a matlab file which can be loaded by\n"); fprintf(fp, "%% load \"%s\"\n\n", filename); fprintf(fp, "surface = struct(’vertices’, [");

12 13 14 15 16

int np = data->getNumPoints(); int nt = data->triangles.size(); McVec3f* c = data->getCoords(); for (int i=0 ; igetPoint(), v3->getPoint() } ; const SbMatrix mm = action->getModelMatrix(); SbVec3f vx[3]; for(int j = 0; j < 3; j++) { mm.multVecMatrix(vtx[j],vx[j]); } // see if the point is already in the coord3 list int cc[3]; cc[0] = -1; cc[1] = -1; cc[2] = -1;

144

10.4. Surface data bool newPoints[] = { true, true, true }; // initially all points are allowed for(int j = 0; j < coord3->point.getNum(); j++){ // now look if some points have to be removed if(coord3->point[j][0] == vx[0][0] && coord3->point[j][1] == vx[0][1] && coord3->point[j][2] == vx[0][2] ){ newPoints[0] = false; cc[0] = j; } if(coord3->point[j][0] == vx[1][0] && coord3->point[j][1] == vx[1][1] && coord3->point[j][2] == vx[1][2] ){ newPoints[1] = false; cc[1] = j; } if(coord3->point[j][0] == vx[2][0] && coord3->point[j][1] == vx[2][1] && coord3->point[j][2] == vx[2][2] ){ newPoints[2] = false; cc[2] = j; } } // for each point that is new we have to add an index if(cc[0] == -1) cc[0] = coord3idx++; if(cc[1] == -1) cc[1] = coord3idx++; if(cc[2] == -1) cc[2] = coord3idx++; // only add the points that are new! if(newPoints[0] || newPoints[1] || newPoints[2]) { for(int j=0;jpoint.setNum(coord3->point.getNum() + 1); coord3->point.setValues(cc[j], 1, &vx[j]); } } } int32_t indices[] = { cc[0], cc[1], cc[2], -1 }; int oldsize = ifs->coordIndex.getNum(); ifs->coordIndex.setNum(oldsize + 4); ifs->coordIndex.setValues(oldsize, 4, indices); } ... // generate a geometry (SoSphere) SoSeparator *root = new SoSeparator; SoComplexity *complexity = new SoComplexity; complexity->value.setValue(portComplexity.getValue()); root->addChild(complexity);

145

10. Data import and export root->addChild(new SoSphere); coord3 = new SoCoordinate3; coord3->point.setNum(0); ifs = new SoIndexedFaceSet; ifs->coordIndex.setNum(0); coord3idx = 0; root->ref(); // add a callback that will call the triangle_cb function SoCallbackAction ca; ca.addTriangleCallback(SoShape::getClassTypeId(), triangle_cb, NULL); ca.apply(root); root->unref(); // after this the coord3 and ifs contain the vertex and triangle informations

Enable two-sided lightning Sometimes it is important to enable the two-sided lightning for surfaces. In this case add the following node to your surface part of the scene graph: SoShapeHints* shapehints = new SoShapeHints; shapehints->shapeType.setValue(SoShapeHints::UNKNOWN_SHAPE_TYPE); shapehints->vertexOrdering.setValue(SoShapeHints::COUNTERCLOCKWISE); group->addChild(shapehints);

Based on the coordinates of your surface sometimes you will have to use CLOCKWISE instead of COUNTERCLOCKWISE. Inspect the currently used scene graph Since Avizo version 4.1 a new tool is available that is able to display and also change the currently used scene graph. The tool is called ivTune and is started with the keyboard shortcut Shift-F11. Save surface data for PovRay Here is another example that is writing out the triangles of a surface object per patch. The example is building a file suitable for the ray-tracing program PovRay. First lets clear up a connected surface object. surface->removeDuplicatePoints(1e-4); surface->removeDegenerateTriangles(); surface->removeEmptyPatches(); //surface->recompute(); // will introduce new patches for all non-connected surfaces surface->computeNormalsPerTriangle(); surface->touch();

146

10.4. Surface data Now we can generate some materials. This is only interesting for the later use in the triangle lists. // generate the Materials for(int patch = 0; patch < surface->patches.size(); patch++){ fprintf(fp, "\n#declare Mat%04d = texture {\n\tpigment {\n\t\tcolor rgb " "\n\t}\n\tfinish { surffinish }\n\tnormal { surfnormal }\n}", patch, (1.0*patch+1)/(surface->patches.size()), (1.0*patch+1)/(surface->patches.size()), (1.0*patch+1)/(surface->patches.size())); }

1 2 3 4 5 6 7 8 9 10

Now save all the triangles for each patch and assign them their material. theWorkArea->startWorking("building pov-ray file..."); for(int patch = 0; patch < surface->patches.size(); patch++){ theWorkArea->setProgressValue((1.0*patch)/(surface->patches.size()-1)); fprintf(fp, "\nmesh {\n"); for(int triangles = 0; triangles < surface->patches[patch]->triangles.size(); triangles++){ int currenttriangle = surface->patches[patch]->triangles[triangles]; int p0 = surface->triangles[currenttriangle].points[0]; int p1 = surface->triangles[currenttriangle].points[1]; int p2 = surface->triangles[currenttriangle].points[2];

1 2 3 4 5 6 7 8 9 10 11 12

fprintf(fp,"\n\tsmooth_triangle { " " \n\ttexture{ Mat%04d }\n\t}", surface->points[p0].x, surface->points[p0].y, surface->points[p0].z, surface->normals[p0].x, surface->normals[p0].y, surface->normals[p0].z, surface->points[p1].x, surface->points[p1].y, surface->points[p1].z, surface->normals[p1].x, surface->normals[p1].y, surface->normals[p1].z, surface->points[p2].x, surface->points[p2].y, surface->points[p2].z, surface->normals[p2].x, surface->normals[p2].y, surface->normals[p2].z, patch

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

147

10. Data import and export ); } fprintf(fp, "\n}\n"); // end of mesh{ } theWorkArea->stopWorking();

148

34 35 36 37 38

11. LabelFields Labelfields are the result of a segmentation process. They can be used to either visualize the labels by Surfaces (SurfaceGen1 ) or used to get quantitative measurements (MaterialStatistics ). To generate a LabelField and sets its size we can use the following construct HxUniformLabelField3 *output = dynamic_cast(getResult()); 1 if(output && !output->isOfType(HxUniformLabelField3::getClassTypeId())) 2 output = 0; 3 if(!output){ 4 output = new HxUniformLabelField3(); 5 output->resize(dims[0],dims[1],dims[2]); 6 } 7 // init and generate the labels 8 memset(output->lattice.dataPtr(),0,dims[0]*dims[1]*dims[2]); 9 output->coords()->setBoundingBox(bb); 10 output->composeLabel(field->getName(),"-labels"); 11

In a compute module this will either generate a new LabelField or re-use the outcome of a previous computation (an attached result object). We are using here the bounding box and the name of an attached field. This is likeliy the case if you want to write a module for automatic segmentation. We now have to generate the names and colors for each material. Those values are saved as HxParameters with a specific number of entries. We have to provide an Id and a color for each material. As a speciality we will allow the user of the module to enter labels for each material in a text window (HxPortText portLabels). Using a string tokenizer we can query for the labels or use instead some that are automatically generated. HxParamBundle& materials = *output->parameters.materials(); materials.removeAll();

1 2 3

// we should add a default Exterior material first materials.bundle("Exterior", 1);

4 5 6

McString *labelname = new McString(portLabels.getValue()); labelname->unpad(); // remove starting and trailing blanks McStringTokenizer *tokenizer = new McStringTokenizer(*labelname);

7 8 9 10

for(int i=0; ihasMoreTokens()){ sprintf(str, "%s",(tokenizer->nextToken()).getString()); } else { sprintf(str, "Tissue%d",i); } bundle = materials.bundle(str,1); } float tc[3]; (colors[i])->getValue(tc[0],tc[1],tc[2]); bundle->insert(new HxParameter("Color",3,tc)); bundle->insert(new HxParameter("Id",i)); }

colors contains a list of SbColor (Inventor/SbColor.h) values that we will use as the colors for the different labels in the field. Given the labels we can now fill the structure by unsigned char *outputdata = (unsigned char *)output->lattice.dataPtr(); ... outputdata[pos] = id; ... setResult(output);

11.1. Exporting LabelFields You can save the label field as series of tif, bmp, png etc images. This will output the whole stack as a numbered sequence of monochrome bitmap images. If you need the colors that were defined in the segmentation editor being preserved you need to first issue the command image.labels makeColormap

in the console window (assuming “image” being the name of your stack). Then you attach a Compute→ColorCombine module to the labelfield, right click the colormap window in ColorCombine, select entry ’image.colors’ (if “image” has been the name of your stack) and click DoIt. The newly created ’Combination’ can be saved as tif or bmp. The Segmentation Editor can also display labels as filled regions ontop of the raw data. In order to change the default (outline mode) to the filled mode you need to right click on each material in the material list and select the new draw-style option.

150

12 13 14 15 16 17 18 19 20 21 22 23 24 25

12. LineSets LineSets are used to store connected points in space. HxLineSet* lineSet = new HxLineSet;

1 2

if (lineSet) { lineSet->lines.resize(1); lineSet->points.resize(0); lineSet->lines[0].points.resize(0); lineSet->data.resize(1); // data per point for (int i = 0; i < sampleLocs.size(); i++) { lineSet->points.append(McVec3f(*(sampleLocs[i]))); lineSet->data[0].append(42.0); lineSet->lines[0].points.append(i); } lineSet->setLabel("LineSet"); theObjectPool->addObject(lineSet); }

3 4 5 6 7 8 9 10 11 12 13 14 15

This example will be quite slow because of the potentially many append calls which take some time. A faster way to do the same is here: HxLineSet *myLineset = new HxLineSet(); McVec3f *vertex = new McVec3f[VERTEXNUM]; int *Idx = new int[VERTEXNUM]; for(int i=0;iaddPoints(vertex,VERTEXNUM); delete vertex; myLineset->addLine(VERTEXNUM,Idx); delete Idx; myLineset->setNumDataValues(1); // only correct if the number of // points is already known! float *val = new float[VERTEXNUM]; for(int i=0;igetData(0),val,VERTEXNUM*sizeof(float)); delete val; myLineset->touchMinMax(); // resets the min/max values

22 23 24

12.1. Reading a line set Here an example that fills a label field from the point that are in a line set. Note that this will not trace the lines directly but only show how to map a point found in a line set to an index in a field (in this case a label field).

// output is a label field 1 HxUniformLabelfield *labelField = dynamic_cast(getResult()); 2 // here check if labelField is not empty, generate it etc. 3 4

// ask for the bounding box of the data (xmin, xmax, ymin, ymax, zmin, zmax) float bbox[6]; labelField->getBoundingBox(bbox); // ask for the dimensions const int *dims = labelField->lattice.dims(); // ask for a pointer to the data unsigned char *data = labelField->lattice.dataPtr();

5 6 7 8 9 10 11

// input is a line set HxLineSet *lineSet = (HxLineSet *)portData.source(); if(!lineSet) return; for (int i =0; i < lineSet->lines.size(); i++) { for(int j=0; j < lineSet->lines[i].points.size(); j++) { McVec3f *pos = &lineSet->points[lineSet->lines[i].points[j]]); theMsg->printf("point (%d) of line (%d) is at %g %g %g", j, i, (*pos)[0], (*pos)[1], (*pos)[2]); // find the voxel in the labelField if( (*pos)[0] < bbox[0] || (*pos)[0] > bbox[1] || (*pos)[1] < bbox[2] || (*pos)[1] > bbox[3] || (*pos)[2] < bbox[4] || (*pos)[2] > bbox[5]) continue; // data is outside the volume int x = ((*pos)[0]-bbox[0]) / ( (bbox[1]-bbox[0])-1.0 ) * dims[0]; int y = ((*pos)[1]-bbox[2]) / ( (bbox[3]-bbox[2])-1.0 ) * dims[1]; int z = ((*pos)[2]-bbox[4]) / ( (bbox[5]-bbox[4])-1.0 ) * dims[2]; // now set the value in the label field to 1 (first material) data[ z * (dims[0]*dims[1]) + y * dims[0] + x] = 1; } }

152

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

12.2. Drawing a line set

12.2. Drawing a line set LineSets can also be drawn directly into the Avizo viewer. This is done by the correspondig Inventor nodes. Here an example which is displaying three lines (a local coordinate system). #include #include ... SoSeparator *lines = new SoSeparator; SoLineSet *lineset = new SoLineSet; SoCoordinate3 *coords = new SoCoordinate3; scene->addChild(lines); lines->addChild(coords); lines->addChild(lineset); // now fill the coordinates with three lines each int nPoints = 0; for(int i=0;ipoint.set1Value(nPoints++,a.x,a.y,a.z); // start point coords->point.set1Value(nPoints++,a.x+scale*(evals[mo[i]]*m(0,i)), a.y+scale*(evals[mo[i]]*m(1,i)), a.z+scale*(evals[mo[i]]*m(2,i))); // end point lineset->numVertices.set1Value(i,2); // connect the last two points }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Please note that the numVertices command always needs at least two points per call. For a line which is longer than 2 points you would first add all the points and finaly use a single call to numVertices.set1Value to add all vertices for n points. Similar to SoLineSet there is also a SoPointSet. SoSeparator *points = new SoSeparator; SoPointSet *ps = new SoPointSet; SoCoordinate3 *pcoords = new SoCoordinate3; SoDrawStyle *drawStyle = new SoDrawStyle; drawStyle->pointSize = 4; // a fixed point size

1 2 3 4 5 6

scene->addChild(points); points->addChild(drawStyle); points->addChild(pcoords); points->addChild(ps); int nP = 0; // seedpoints is a McDArray and // contains our point locations pcoords->point.setNum(seedpoints.size()); for(int i = 0;ipoint.set1Value(nP++,seedpoints[i]); } ps->numPoints = nP;

7 8 9 10 11 12 13 14 15 16 17 18

153

12. LineSets In order to draw a nicer looking set of points you could use of course real spheres in an inventor scene graph. But sometimes the amount of geometry generated by this approach would slow down the display greatly. There is a middle way which is to use screen aligned textures instead of the real geometry. Texture draw is very fast on all modern hardware so the framerate with this approach is very good (see HxClusterView’s plate mode for drawing spheres). Here is an example on how to render plates in Avizo. Lets assume that the coordinates of the spheres are in an SbVec3f array called seedpoints. #include ... SoSeparator *scene = new SoSeparator; // scene->removeAllChildren();

1 2 3 4 5

SoSphereSet *ps = new SoSphereSet; ps->renderFunction = SoSphereSet::PLATES; ps->setComplexity(0.2); ps->setNumSpheres(seedpoints.size()); ps->coords.init(seedpoints.size(), seedpoints); ps->newCoords(); ps->nSelected = seedpoints.size(); ps->selected.resize(seedpoints.size()); SbColor col(1.0,0,0); // one color for all for(int k=0;ksetRadius(k, 0.05); ps->setColor(k,col); ps->selectSphere(k,1); } ps->update(); ps->touch(); scene->addChild(ps);

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

showGeom(scene);

154

24

13. Field access There are several ways in which you can read the values from, lets say, a uniform vector field. This works the same even if you work with scalar- complex- or colorfields. Only the number of values per location in space is changing.

13.1. Access stored values The following code is returning the stored field values. Later we will see an example which can evaluate the field on arbitrary position. HxUniformVectorField3 *bla = (HxUniformVectorField3 *)portData.source(); const int *dims = bla->lattice.dims(); // now dims[0], dims[1], and dims[2] contain the number of voxels float *data = (float *)bla->lattice.dataPtr(); // data now points to the data, you can use memcpy etc. data[0] = 42.0; data[1] = 42.0; data[2] = 42.0; // sets the first vector at position 0, 0, 0 to 42, 42, 42

1 2 3 4 5 6 7

An important class to use is HxLattice3. It provides functions that are working with all data types by implicitely casting the parameters and the results into floating point values thus we do not have to provide interfaces to all the different data types. HxLattice3 *lattice = bla->lattice; float result[3]; result[0] = 42; result[1] = 42; result[2] = 42; lattice->set(i,j,k,result);

1 2 3

The above code is setting the value in the field at the index tripel i, j, k to the vector defined in result. There is also an eval(i,j,k,&result 0 ) method that will return the value at index i, j, k. Please note that the index position is in general not directly connected to a position in space. The connected data object bla may be defined on a curvilinear field and therefore there may be arbitrary distances between points in space.

13.2. Access interpolated positions The second way is to use the location class and get an interpolation for arbitrary locations in the field. The locations in this case are not defined in terms of voxel indexes but by a float location in terms of origin of the bounding box and the voxel size.

155

13. Field access

HxUniformVectorField3 *bla = (HxUniformVectorField3 *)portData.source(); if(bla == NULL){ theMsg->printf("error, no data connected"); return; } float bb[6]; bla->getBoundingBox(bb); HxLocation3 *loc = bla->createLocation(); int nDataVar = bla->nDataVar(); // nDataVar should be 3 now float *result = (float*)malloc(sizeof(float)*nDataVar); // space for the values loc->set(42.0, 42.0, 42.0); bla->eval(loc,result); // now we have the vector from location 42,42,42 in result[0..2]

1 2 3 4 5 6 7 8 9 10 11 12

The above example is working but will be slower than needed. Basically the problem is that the set function will take some time in order to calculate the required value. It is more efficient to ask the location class to re-use some part of the computation. This is done by int ret = 0; if(ret) ret = loc->move(42.0, 42.0, 42.0); else ret = loc->set(42.0, 42.0, 42.0); if(ret) bla->eval(loc,result); else theMsg->printf("error: could not read values");

1 2 3 4 5 6 7 8 9

13.3. Access coordinates There is a build-in coordinate system in each data object. Mostly the coordinates are implicit thus information about the origin and the voxel size are sufficient to calculate for each voxel its position in space in ’real world coordinates’. But in case of curvilinear fields the information about the position of each voxel is stored in the field itself. Here is an example which is looking for the type of the attached object and in case its not curvilinear yet it is converting it to a curvilinear field. #include #include ... char *str = "lobus.am"; // HxObject is a basis class of all data objects, so this always works HxObject *obj = theObjectPool->findObject(str); if(!obj){ theMsg->printf("error: unknown data object \"%s\"",str); return 0;

156

1 2 3 4 5 6 7 8 9

13.3. Access coordinates } int kval = int jval = int ival = float xval float yval float zval

10

1; // voxel at this index 1; 1; = 42.0; // will be at this position = 42.0; = 42.0;

11 12 13 14 15 16 17

if(obj->isOfType(HxRegScalarField3::getClassTypeId())){ 18 HxRegScalarField3 *data = dynamic_cast(obj); 19 HxRegScalarField3 *data2 = NULL; 20 // new data in case that we have to generate a curvilinear field 21 const int *dims = data->lattice.dims(); 22 // if we do not have already curvilinear fields we need to create one 23 // we will try to keep its name and remove the former data object 24 if(data->lattice.coords()->coordType() != c_curvilinear){ // we need to create 25 a data // object which is c_curvilinear now 26 int npoints = dims[0]*dims[1]*dims[2]; // number of voxel 27 // space for the xyz coordinates 28 float* xyz = (float *) malloc(3*npoints*sizeof(float)); 29 if(!xyz){ 30 theMsg->printf("error: could not allocate %.2fMB", 31 3*npoints*sizeof(float)/1024.0/1024.0); 32 return 0; 33 } 34 // fill the coordinate values, use defaults 35 int count = 0; 36 for(int kk=0;kkcoords(), 50 xyz, 3*npoints*sizeof(float)); 51 free(xyz); 52 // read in now the values per node 53 int nDataVar = data->lattice.nDataVar(); // number of values per 54 node float *tmp = (float *)data2->lattice.dataPtr(); 55 // copy the contents of the data into data2 56 memcpy(tmp,data->lattice.dataPtr(),sizeof(float)*npoints*nDataVar); 57 char filename[1024]; sprintf(filename,"%s",data->getName()); 58 if (data2){ 59

157

13. Field access theObjectPool->removeObject(data); HxData::registerData(data2, filename); } else return 0; // should never happen } // now change the coordinate because it will be curvilinear! if(data2){ // data 2 contains the data object ((HxCurvilinearCoord3 *)data2->lattice.coords())->coords() [3*((kval*dims[0]*dims[1])+(jval*dims[0])+ival)+0] = ((HxCurvilinearCoord3 *)data2->lattice.coords())->coords() [3*((kval*dims[0]*dims[1])+(jval*dims[0])+ival)+1] = ((HxCurvilinearCoord3 *)data2->lattice.coords())->coords() [3*((kval*dims[0]*dims[1])+(jval*dims[0])+ival)+2] = } else { // the data was already curvilinear ((HxCurvilinearCoord3 *)data->lattice.coords())->coords() [3*((kval*dims[0]*dims[1])+(jval*dims[0])+ival)+0] = ((HxCurvilinearCoord3 *)data->lattice.coords())->coords() [3*((kval*dims[0]*dims[1])+(jval*dims[0])+ival)+1] = ((HxCurvilinearCoord3 *)data->lattice.coords())->coords() [3*((kval*dims[0]*dims[1])+(jval*dims[0])+ival)+2] = } return 0; }

158

60 61 62 63 64 65 66 67

xval;

68 69

yval;

70 71

zval;

72 73 74

xval;

75 76

yval;

77 78

zval;

79 80 81 82

14. Data Clusters 14.1. Reading in a cluster object Data cluster are collections of points in space. There may be any number of data corresponding to each single point in the cluster. The usuall file format to store this type of data understood by Avizo is PSI. #include

1 2

HxCluster* cluster = (HxCluster*) portData.source(); if (cluster && portData.isNew()) { int n = cluster->getNumDataColumns(); portVariable.setNum(1+n); # Amira/HxPortMultiMenu.h portVariable.setLabel(0,"Id"); for (int i=0; idataColumns[i]; portVariable.setLabel(i+1,dc.name.dataPtr()); }

3 4 5 6 7 8 9 10 11

Query the point coordinates: n = cluster->getNumPoints(); float b[6]; cluster->getBoundingBox(b); McVec3f* points = cluster->points.dataPtr(); int* src = cluster->ids.dataPtr();

# how many points? # point coordinates # point ids

Query data per point: HxCluster::DataColumn* dc = &cluster->dataColumns[0]; McPrimType primType = dc->primType; # something like McPrimType::mc_int32 float* src = (float*) dc->data; # value per point

14.2. Generating a new cluster object Modules that are known to Avizo are kept in an internal structure most of which can be accessed by the HxResource module. For example a HxResource::getExecPath() will return the path of the Avizo executable which is below the HxResource::getRootDir() the installation directory of Avizo. HxResource::getDataDir() returns the value of the global variable AVIZO DATA.

159

14. Data Clusters

#include #include #include #include #include #include



1 2 3 4 5 6 7

HX_INIT_CLASS(simpleCluster,HxModule)

8 9

simpleCluster::simpleCluster() : HxModule(){} simpleCluster::~simpleCluster(){}

10 11 12

void simpleCluster::compute(){ HxCluster* cluster = new HxCluster(); cluster->setNumDataColumns(1); cluster->resize(2);

13 14 15 16 17

McVec3f* coords = cluster->getCoords(); coords[0].setValue(0,0,0); coords[1].setValue(10,0,0);

18 19 20 21

float* data = (float*) cluster->dataColumns[0].data; data[0] = 0; data[1] = 1;

22 23 24 25

cluster->computeBounds(); cluster->setLabel("Cluster"); theObjectPool->addObject(cluster); }

26 27 28 29 30

void simpleCluster2::compute(){ HxCluster* cluster = new HxCluster(); cluster->setNumDataColumns(1); cluster->resize(2);

31 32 33 34 35

cluster->points[0] = McVec3f(0,0,0); cluster->ids[0] = 0;

36 37 38

cluster->points[1] = McVec3f(10,0,0); cluster->ids[1] = 1;

39 40 41

float* data = (float*) cluster->dataColumns[0].data; data[0] = 0; data[1] = 1;

42 43 44 45

cluster->computeBounds(); cluster->setLabel("Cluster"); theObjectPool->addObject(cluster); }

160

46 47 48 49

14.3. Adding a clusterView and a vertexView 50

14.3. Adding a clusterView and a vertexView In order to create standard Avizo objects you have to use the HxResource tool. It allows you to specify the object by its name as printed out by the console command iconname getTypeId. Here are two examples for ClusterView and DisplayVertices. HxModule *clusterView = (HxModule*)HxResource::createObject("HxClusterView"); clusterView->composeLabel(field->getName(),"_ClusterView"); theObjectPool->addObject(clusterView); clusterView->portData.connect(cluster); clusterView->fire();

1 2 3 4 5 6

HxModule *vertexView = (HxModule*)HxResource::createObject("HxDisplayVertices"); vertexView->composeLabel(field->getName(),"_VertexView"); theObjectPool->addObject(vertexView); vertexView->portData.connect(cluster); vertexView->fire();

7 8 9 10 11

}

12

161

14. Data Clusters

162

15. Surfaces 15.1. A simple surface Surfaces can contain any number of patches (and contours and edges). This example copies data from a structure cutTriangulated to the patch. // if we already constructed a surface re-use it HxSurface *surface = dynamic_cast(getResult(0)); if(!surface){ surface = new HxSurface(); surface->setLabel("CuttingPlane"); // the icon name } surface->clear(); // remove everything from before

1 2 3 4 5 6 7 8

surface->points.resize(cutTriangulated->numberofpoints); surface->pointType.resize(cutTriangulated->numberofpoints);

9 10 11

// copy the points to the surface for (k = 0; k < cutTriangulated->numberofpoints ; k++){ surface->points[k].setValue( cutTriangulated->pointlist[2*k], cutTriangulated->pointlist[2*k+1], myLineSet->getAverageCoord().z); surface->pointType[k] = HxSurface::INNER; }

12 13 14 15 16 17 18 19 20

HxSurface::Patch* patch = new HxSurface::Patch; patch->outerRegion = surface->addMaterial("Exterior",0); patch->innerRegion = surface->addMaterial("Interior",1); surface->triangles.resize(cutTriangulated->numberoftriangles); patch->triangles.resize(cutTriangulated->numberoftriangles);

21 22 23 24 25 26

for (i = 0; i < cutTriangulated->numberoftriangles; i++){ surface->triangles[i].points[0] = cutTriangulated->trianglelist[i * 3 ]; surface->triangles[i].points[1] = cutTriangulated->trianglelist[i * 3 + 1]; surface->triangles[i].points[2] = cutTriangulated->trianglelist[i * 3 + 2]; surface->triangles[i].patch = 0; patch->triangles[i] = i; } surface->patches.append(patch); // add patch to surface setResult(0,surface);

27 28 29 30 31 32 33 34 35

Please take the above example as a minimally needed framework. You should not leave out

163

15. Surfaces something from the above mentioned steps as for example adding materials. If the data structures are not set correctly the most likely result of working with a surface will be a segmentation fault.

15.2. Quadro-lateral surfaces Quadro-laterial surfaces are surfaces which consist out of quadrats instead of the usual triangles. Internally also these surfaces are rendered by two triangles per quadrat. An Avizo module using this type of surface is HxHeightField. ... Hx2DMesh::Hx2DMesh():HxQuadBase(HxRegScalarField3::getClassTypeId()), ... hideGeom(soRoot); int dim[3] = { 0, 10, 20 };

1 2 3 4 5 6

soQuadSurface->coords.resize(dim[1]*dim[2]); soQuadSurface->quads.resize( (dim[1]-1)*(dim[2]-1) );

7 8 9

soQuadSurface->colorBinding = SoSurface::VERTEX_COLORS; soQuadSurface->colors.resize(dim[1]*dim[2]);

10 11 12

float coords1[3]; // will be the 3d coordinates float value = 0.0; // will be the color value

13 14 15

for(j=0;jcolors[offs+i] = portColormap.getPackedColor(value); } } for(j=0;jcomputeVertexNormals(); handleDrawStyle(); showGeom(soRoot);

164

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

15.3. Vector fields attached to a surface You will have to override the update() function in order to get the colormap port displayed correctly.

15.3. Vector fields attached to a surface In Avizo the vertices of surfaces can have attached data. Scalar data may be visualized as color only the surface but also surface vector fields are used in order to show flow arround some objects for example. Here is an example on how to define such a data object: HxSurface *surface = new HxSurface();

1 2

HxSurface::Patch* patch = new HxSurface::Patch; surface->patches.append(patch); // add patch to surface // this is the default Exterior == 0 patch->outerRegion = surface->addMaterial("Exterior",0); patch->innerRegion = surface->addMaterial("Interior",1);

3 4 5 6 7 8

surface->points.resize(3); surface->triangles.resize(1);

9 10 11

surface->points[0].setValue(0,0,0); surface->points[1].setValue(10,0,0); surface->points[2].setValue(10,10,0);

12 13 14 15

surface->triangles[0].points[0] = 0; surface->triangles[0].points[1] = 1; surface->triangles[0].points[2] = 2; surface->triangles[0].patch = 0;

16 17 18 19 20

patch->triangles.resize(1); patch->triangles[0] = 0;

21 22 23

HxSurfaceVectorField *vecField = new HxSurfaceVectorField(surface,HxSurfaceField::OnNodes); float *vectors = vecField->dataPtr(); vectors[0] = 0; vectors[1] = 0; vectors[2] = 10; vectors[3] = 0; vectors[4] = 0; vectors[5] = 20; vectors[6] = 0; vectors[7] = 0; vectors[8] = 5;

24 25 26 27 28 29 30 31 32 33 34 35 36

HxData::registerData(surface,"HuiSurface"); HxData::registerData(vecField,"HuiSurfaceVector");

37 38

165

15. Surfaces

166

16. Image filter 16.1. Using a pre-defined filter Lets assume we want to use a Gaussian filter on a uniform field. Image filters work on typed objects in the Avizo framework. That is we need to construct an appropriate data object. Here is an example which uses a random field: #include #define srand48(x) srand(x) #define drand48() ((double)rand()/RAND_MAX) ... imgBuffer1 = (unsigned char *)malloc(dims[0]*dims[1]*dims[2]*sizeof(double)); i1Ptr = new McTypedPointer(imgBuffer1,McPrimType::mc_double); i1 = new McTypedData3D(dims[0],dims[1],dims[2],*i1Ptr);

1 2 3 4 5 6 7 8

for(int i=0;idata)[i] = drand48() -0.5;

9 10

Given this data set we can convole it with a Gaussian function: ImGaussFilter3D *filter = new ImGaussFilter3D(); int kernelsize[3]; float sigma[3]; kernelsize[0] = 3; kernelsize[1] = 3; kernelsize[2] = 3; sigma[0] = 1.6; sigma[1] = 1.6; sigma[2] = 1.6; filter->setParams(kernelsize,sigma); filter->apply3D(i1,NULL);

1 2 3 4 5 6

Because we did not supplied a second argument in apply3D the input data will be overwritten. Note, that in case you want to get the data out of an McTypedData2D you will have to use a construct like this: McTypedData2D *image = new McTypedData2D(dims[0],dims[1], McPrimType::mc_double); ImGaussFilter2D *filter = new ImGaussFilter2D(); filter->setKernelSize(12,8); filter->setSigma(8,4); filter->calculateKernel(); filter->apply2D(image,NULL); for(int j=0;jaddress(0,0))[y*dims[0]+x]); }

10 11 12

}

13

Please note the difference in accessing the address.

16.2. Image filters implemented by shaders In order to use shaders you can use the mcgl package. It provides the classes for accessing the OpenGL shaders (example below is in GLSL). Here is an example of a module that is using two images to perform a trivial computation between them. One of them could represent a mask the other the image data or both of them could be data that needs to be combined on a single pixel base. First here are the variables generated in the header file of the module. protected: int

1

iWidth, iHeight;

// The dimensions of our array

unsigned int

_iTexture;

// The texture used as a data array

GLhandleARB GLhandleARB

_programObject; // the program used to update _fragmentShader, _vertexShader;

GLint GLint

_texUnit; _coordUnit;

2 3 4 5 6 7 8

// a parameter to the fragment program // another texture to the fragment program

9 10 11

int make_tex(SHORT *pBuf, int texSize); // texture unit 1 int make_ftex(FLOAT *pBuf, int texSize); // texture unit 0 int CompilerLog(void); // prints error message from comiling the shader

12 13 14

Here are the two shaders, on vertex shader that is doing nothing at the moment and one pixel shader. Both are defined by strings in the program and we will compile them and send them to the graphics board. // Here is the texture vertex shader static const char *cart2polVertSource = { "void main(void)" "{" " gl_TexCoord[0].x = gl_MultiTexCoord0.x;" " gl_TexCoord[0].y = gl_MultiTexCoord0.y;" " gl_Position = ftransform();" "}" };

1 2 3 4 5 6 7 8 9 10

// Here is my texture fragment shader. At the moment its doing nothing but

168

11

16.2. Image filters implemented by shaders // copy the input to the output. Sadly enough not even that is // working. static const char *cart2polFragSource = { "uniform sampler2D texUnit;" // the current texture "uniform sampler2D coordUnit;" // the mask "uniform float minRadius;" "uniform float maxRadius;" "void main(void)" "{" " const float offset = 1.0 / 512.0;" " vec4 c1 = texture2D(texUnit, gl_TexCoord[0].st);" " vec4 c2 = texture2D(coordUnit, gl_TexCoord[0].st);" " gl_FragColor = c1+c2;" "}" };

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

Now we will define the constructor. There we compile and load the shaders and define some variables that we can have access to in the shader itself. HxCorrelateImages::HxCorrelateImages() : HxCompModule(HxUniformScalarField3::getClassTypeId()), portField2( this, "field2", HxUniformScalarField3::getClassTypeId()), portAction( this, "action"){

1 2 3 4 5

portAction.setLabel(0, "DoIt");

6 7

iWidth = iHeight = 512; _programObject = glCreateProgramObjectARB();

8 9 10

// Create the edge detection fragment program _vertexShader = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB); _fragmentShader = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);

11 12 13 14

glShaderSourceARB(_vertexShader, 1, &cart2polVertSource, NULL); glShaderSourceARB(_fragmentShader, 1, &cart2polFragSource, NULL);

15 16 17

glCompileShaderARB(_vertexShader); glCompileShaderARB(_fragmentShader);

18 19 20

glAttachObjectARB(_programObject, _vertexShader); glAttachObjectARB(_programObject, _fragmentShader);

21 22 23

// Link the shader into a complete GLSL program. glLinkProgramARB(_programObject); GLint progLinkSuccess; glGetObjectParameterivARB(_programObject, GL_OBJECT_LINK_STATUS_ARB, &progLinkSuccess); if (!progLinkSuccess){ theMsg->printf("Filter shader could not be linked\n"); CompilerLog(); // print out the error message from the compiler

24 25 26 27 28 29 30 31

169

16. Image filter return; } glUseProgramObjectARB(_programObject); // here or below?

32 33 34 35

// Get location of the sampler uniform _texUnit = glGetUniformLocationARB(_programObject, if(_texUnit == -1) theMsg->printf("error: could not get uniform _coordUnit = glGetUniformLocationARB(_programObject, if(_coordUnit == -1) theMsg->printf("error: could not get uniform

36

"texUnit");

37 38

named (texture)."); "coordUnit");

39 40 41

named (coordinates).");

42 43

// now transmit two variables to the shader, the minimum and maximum radius // for the polar variable r GLint loc1, loc2, loc3; float minRadius = 5; float maxRadius = 200; loc1 = glGetUniformLocationARB(_programObject, "minRadius"); GLERRCHECK; glUniform1fARB(loc1,minRadius); loc2 = glGetUniformLocationARB(_programObject, "maxRadius"); GLERRCHECK; glUniform1fARB(loc2,maxRadius);

44 45 46 47 48 49 50 51 52 53

// disable the shader again glUseProgramObjectARB(0);

54 55 56

GLint maxSamplers; glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS_ARB,&maxSamplers); theMsg->printf("Number of texture image units found: %d", maxSamplers); }

57 58 59

After doing this here is now the compute module. Presented is only the part where we transmit textures to the GPU and start doing the work but not the parts where we generate the data. Here is out of compute() the setup of the screen. We will draw a quad later on that should fill the whole screen (which is of the size of the textures used). clock_t t1 = clock();

1 2

// generate and transmit all templates 3 GLuint *IDS = (GLuint *)malloc(sizeof(GLuint)*dims[2]); 4 int nChan = 4; // we will use bilinear filtering 5 // we need square images 6 int texSize = dims[0]; 7 if(dims[0] != dims[1]){ 8 theMsg->printf("error: image size has to be square (%dx%d)", dims[0], dims[1]); 9 free(IDS); 10 return; 11 } 12 iWidth = iHeight = dims[0]; // should be 256.. but maybe others are working as well 13 14

170

16.2. Image filters implemented by shaders // first setup the display to cover the texture 1 to 1 int vp[4]; glGetIntegerv(GL_VIEWPORT, vp); glViewport(0,0,iWidth,iHeight); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); // set the projection matrix to orthogonal for pixel to texel 1 to 1 mapping glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(-1, 1, -1, 1); // gluPerspective(45,1.0*w/h,1,1000); glMatrixMode(GL_MODELVIEW); glLoadIdentity();

15 16 17 18 19 20 21 22 23 24 25

Now I can generate a texture that will be send to the board by float *pVB = (float *)malloc(sizeof(float)*512*512*3); for(int j = 0; j < 512; j++) { for (int i = 0; i < 512; i++) { pVB[3*(j*512+i)+0] = i/512.0; pVB[3*(j*512+i)+1] = j/512.0; pVB[3*(j*512+i)+2] = j/512.0; } } // now I would like to transfer these coordinates to the CPU GLuint COORD_ID = make_ftex(pVB, texSize); free(pVB);

1 2 3 4 5 6 7 8 9 10 11

The important part is make ftex and this function sets the texture into a texture unit. All textures that we want to use later on in shaders at the same time have to be in different units. So make ftex is using one unit and make tex is using another. Also one of them is defining a float the other a short texture. int HxCorrelateImages::make_tex(SHORT *pBuf, int texSize) {

1 2

GLuint ID; glGenTextures(1, &ID); glActiveTexture(GL_TEXTURE0 + 1); glBindTexture(GL_TEXTURE_2D, ID);

GLERRCHECK;

3 4

GLERRCHECK;

5 6

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 7 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 8 glTexImage2D( GL_TEXTURE_2D, 0, 4, texSize, texSize, 0, GL_RGBA, GL_SHORT, pBuf); 9 GLERRCHECK; 10 11

return ID;

12

} int HxCorrelateImages::make_ftex(FLOAT *pBuf, int texSize) {

13 14 15

GLuint ID; glGenTextures(1, &ID); glActiveTexture(GL_TEXTURE0 + 0);

GLERRCHECK;

16 17

171

16. Image filter glBindTexture(GL_TEXTURE_2D, ID); 18 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 19 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 20 glTexImage2D( GL_TEXTURE_2D, 0, 3, texSize, texSize, 0, GL_RGB, GL_FLOAT, pBuf);21 22

return ID;

23

}

24

We will use make tex now to put in several textures (RGBA textures derived from the input data field). They all end up in the same texture unit because we will work on them one at a time. SHORT *pBuf = new SHORT[texSize*texSize*nChan]; for(int i=0;iprintf("*** GL Error 0x%x\n", glerr ); */ \ if(glerr == GL_INVALID_ENUM) \ theMsg->printf("GL_INVALIDE_ENUM line:%d\n", __LINE__ ); \ if(glerr == GL_INVALID_VALUE) \ theMsg->printf("GL_INVALIDE_VALUE line:%d\n", __LINE__ ); \ if(glerr == GL_INVALID_OPERATION) \ theMsg->printf("GL_INVALID_OPERATION line:%d\n", __LINE__ ); \ if(glerr == GL_STACK_OVERFLOW) \

1 2 3 4 5 6 7 8 9 10 11 12 13

173

16. Image filter theMsg->printf("GL_STACK_OVERFLOW line:%d\n", __LINE__ ); \ if(glerr == GL_STACK_UNDERFLOW) \ theMsg->printf("GL_STACK_UNDERFLOW line:%d\n", __LINE__ ); \ if(glerr == GL_OUT_OF_MEMORY) \ theMsg->printf("GL_OUT_OF_MEMORY line:%d\n", __LINE__ ); }\ while(glGetError() != GL_NO_ERROR);\ } #else #define GLERRCHECK #endif

174

14 15 16 17 18 19 20 21 22 23

17. Colormaps Colormaps are objects in the workspace of Avizo and can be used by more than one module at a time. To define a colormap you can use the following code float colormap[768] = { 0.8905, 0.1429, 0, 0.8905, 0.1524, 0, ... 0.8762, 0, 0.1524}; HxColormap256 *cm = new HxColormap256(250); for(int i=0;isetRGBA(i,colormap[i*3+0],colormap[i*3+1], colormap[i*3+2],0.5); } cm->setLabel(name); cm->setMinMax(-3.1415927, 3.1415927); theObjectPool->addObject(cm);

1 2 3 4 5 6 7 8 9 10 11 12 13

175

17. Colormaps

176

18. SpreadSheets Generate the spreadsheet object: #include ... HxSpreadSheet *ss = new HxSpreadSheet(); ss->composeLabel(data->getLabel().getString(),"PowerStatistics"); ss->addColumn("nmode",HxSpreadSheet::Column::FLOAT); ss->addColumn("power",HxSpreadSheet::Column::FLOAT);

1 2 3 4 5 6

Fill its rows: ss->setNumRows(npow); for(int i=0; icolumns[0].setValue(i,(double)pow[i]/nmode[i]); ss->columns[1].setValue(i,(double)nmode[i]); }

1 2 3 4 5

There are only two data types available for the column in a table, one is the HxSpreadSheet::Column::FLOAT the other one is HxSpreadSheet::Column::STRING. To go through all the columns in a table: for(int i = 0; i < ss->nRows(); i++){ 1 for(int j = 0; j < ss->columns.size(); j++){ 2 if(ss->columns[j].type == HxSpreadSheet::Column::STRING) 3 theMsg->printf("Row %d, value \"%s\"", i, ss->columns[j].stringValue(i)); 4 else 5 theMsg->printf("Row %d, value %g", i, ss->columns[j].floatValue(i)); 6 } 7 } 8

177

18. SpreadSheets

178

19. Graphical interfaces 19.1. Screen aligned text output To present some text into the window in a sort of ’head-up-display’ you can use the OpenGL underlying all draw commands in Avizo. The following code will do the drawing in screen positioned mode so the text will not be attached to an 3D object. But first an small example: 1

SbVec4f region(0,0,1,1);

2 3

HxViewer *viewer = theController->getCurrentViewer(); const SbViewportRegion& viewport = viewer->getViewportRegion(); SbVec2s windwoSize = viewport.getWindowSize(); windowSize[0] = (short)(windowSize[0]/(region[2]-region[0])+0.5); windowSize[1] = (short)(windowSize[1]/(region[3]-region[1])+0.5); float orgx, orgy; orgx = 40; orgy = 40; if(orgxgetSceneGraph(0); root->insertChild(eventCB,0); root->insertChild(callbackR,0); memcpy(currentText, "some text displayed in Avizo viewer", 1024); fontSize = 18;

7 8 9 10 11 12

// in destructor SoSeparator *root = (SoSeparator *)theController->getSceneGraph(0); root->removeChild(eventCB); root->removeChild(callbackR);

13 14 15 16

Now we have to add the following two functions to our code. They get called any time we render the image thus can add something to the scene. In our case we re-position the some text to a fixed position on screen.

179

19. Graphical interfaces

// render function void askForValue::render(SoGLRenderAction* renderAction) { #ifdef HX_TILED_RENDERING SoState* state = renderAction->getState(); SbVec4f region = SoModifyViewVolumeElement::get(state); #else SbVec4f region(0,0,1,1); #endif

1 2 3 4 5 6 7 8 9 10

const SbViewportRegion& viewport = renderAction->getViewportRegion(); SbVec2s windowSize = viewport.getWindowSize();

11 12 13

windowSize[0] = (short)(windowSize[0]/(region[2]-region[0])+0.5); windowSize[1] = (short)(windowSize[1]/(region[3]-region[1])+0.5);

14 15 16

SoCacheElement::invalidate(renderAction->getState());

17 18

float orgx, orgy; orgx = 40; orgy = 40;

19 20

if (orgxsetInterval(2.0); // every two seconds oneShotCallback->schedule(); counter = 0;

19 20 21 22 23 24

19.3. Generate geometries by Open Inventor Open Inventor can read in geometries as .iv files similar to wrml files. You can use this feature to generate Inventor objects (like manipulators) ’on-the-fly’. Here an example for generating a plane (used by GeometryCutter in Avizo): static char* translateGeom = { "Separator {" " ShapeHints { vertexOrdering COUNTERCLOCKWISE }" " Material { diffuseColor 0 0.15 0.3 }" " PickStyle { style UNPICKABLE }" " Coordinate3 { point [ 0 -1 -1, 0 -1 1, 0 1 1, 0 1 -1 ] }" " Normal { vector -1 0 0 }" " NormalBinding { value OVERALL }" " FaceSet { }" " PickStyle { style SHAPE }" " DrawStyle { style INVISIBLE }" " Coordinate3 { point [ 0 -0.8 -0.8, 0 -0.8 0.8, 0 0.8 0.8, 0 0.8 -0.8 ] }" " FaceSet { }" "}" }; ... SoInput in; SoNode (node; in.setBuffer(geometry,strlen(geometry)); SoDB::read(&in,node); tabPlane->setPart("translator",node);

Here tabPlane is a SoTabPlaneDragger, but an addChild in the scenegraph will do nicely (with showGeom).

19.4. Plot textures in 3D Textures plotted in 3D are useful for many display modules. One example is the use of textures instead of geometry for drawing spheres (see plates). (Drawing textures can be much faster than drawing triangles on current hardware.) The following example will arrange squares filled with textures on a surface. You can learn here how to create a texture and how to set its position in 3D space.

182

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

19.4. Plot textures in 3D Note that before being able to reach the surface normals one has to compute them for the surface. A good strategy for this is to first remove duplicate points surface->removeDuplicatePoints(1e-4); surface->computeNormalsPerVertexIndexed( );

After this operation on the surface one can start generating the textures. #include #include #include #include #include #include ... HxViewer *viewer = theController->getCurrentViewer(); if(scene) { // scene is SoSeparator hideGeom(scene); scene->removeAllChildren(); } else { scene = new SoSeparator; } scene->ref();

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// for each image in field for(int i = 0; i < dims[2]; i++) { int max = ...; // some index of a point of the surface

17 18 19 20

// so image i matches best to surface point max McVec3f point = surface->points[max]; McVec3f normal = -surface->normals[max]; // we use the inverted normals because we have one sided // lighting

21 22 23 24 25 26

// What are the coordinates for each image (offset vectors)? // They are normal to the normal of the surface at this position. McVec3f xdir(1,0,0); McVec3f ydir(0,1,0); McVec3f inplane1, inplane2; normal.normalize(); if(normal.dot(xdir)-1.0 < 1e-6) inplane1 = normal.cross(ydir); else inplane1 = normal.cross(xdir); inplane1.normalize(); inplane2 = inplane1.cross(normal); inplane2.normalize(); // create four points that are the corners of the texture McVec3f p0 = -inplane1/2.0 - inplane2/2.0 + point; McVec3f p1 = inplane1/2.0 - inplane2/2.0 + point; McVec3f p2 = inplane1/2.0 + inplane2/2.0 + point;

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

183

19. Graphical interfaces McVec3f p3 = -inplane1/2.0 + inplane2/2.0 + point;

42 43

// now add a separator for this texture to scene SoSeparator *sep = new SoSeparator; scene->addChild(sep);

44 45 46 47

// ok now show a texture at that coordinate 48 SoTexture2 *slice = new SoTexture2; 49 sep->addChild(slice); 50 // attention this only works for byte data in field! 51 slice->image.setValue( SbVec2s(dims[1],dims[0]), 52 1, 53 &((const unsigned char *)(field->lattice.dataPtr()))[i*(dims[0]*dims[1])] );54 55

SoCoordinate3* coord = new SoCoordinate3; sep->addChild(coord); coord->point.set1Value(0, SbVec3f( p0[0], coord->point.set1Value(1, SbVec3f( p1[0], coord->point.set1Value(2, SbVec3f( p2[0], coord->point.set1Value(3, SbVec3f( p3[0],

56 57

p0[1], p1[1], p2[1], p3[1],

p0[2])); p1[2])); p2[2])); p3[2]));

58 59 60 61 62

SoNormal *normalV = new SoNormal; sep->addChild(normalV); normalV->vector.set1Value(0, SbVec3f(normal[0], normalV->vector.set1Value(1, SbVec3f(normal[0], normalV->vector.set1Value(2, SbVec3f(normal[0], normalV->vector.set1Value(3, SbVec3f(normal[0],

63 64

normal[1], normal[1], normal[1], normal[1],

normal[2])); normal[2])); normal[2])); normal[2]));

65 66 67 68 69

SoTextureCoordinate2 *texCoord = new SoTextureCoordinate2; sep->addChild(texCoord); texCoord->point.set1Value(0, SbVec2f( 0 , 0) ); texCoord->point.set1Value(1, SbVec2f( 1 , 0) ); texCoord->point.set1Value(2, SbVec2f( 1 , 1) ); texCoord->point.set1Value(3, SbVec2f( 0 , 1) );

70 71 72 73 74 75 76

SoTextureCoordinateBinding *tBind = new SoTextureCoordinateBinding; sep->addChild(tBind); tBind->value.setValue(SoTextureCoordinateBinding::PER_VERTEX);

77 78 79 80

SoFaceSet *myFaceSet = new SoFaceSet; sep->addChild(myFaceSet); myFaceSet->numVertices.set1Value(0, 4); } showGeom(scene); viewer->render();

184

81 82 83 84 85 86

19.5. Get the actual mouse position

19.5. Get the actual mouse position This example is out of a module which displays additional informations for selected points in 3D. #include // Inventor classes are known 1 #include 2 #include 3 #include 4 #include 5 ... 6 eventCB = new SoEventCallback; // add in the constructor 7 eventCB->addEventCallback(SoMouseButtonEvent::getClassTypeId(), 8 mouseClickCB, this); 9 SoSeparator *root = (SoSeparator *)theController->getSceneGraph(0); 10 root->insertChild(eventCB,0); 11 ... 12 SoSeparator *root = (SoSeparator *)theController->getSceneGraph(0); // add in destructor 13 root->removeChild(eventCB); 14 ... 15 void mouseClickCB(void *p, SoEventCallback *eventCallback){ // not in a class 16 SoEventCallback *eventCB = eventCallback; 17 const SoMouseButtonEvent *event = (SoMouseButtonEvent *) 18 eventCallback->getEvent(); 19 if (SO_MOUSE_PRESS_EVENT(event, BUTTON1)) { 20 const SoPickedPoint * pp = eventCallback->getPickedPoint(); 21 SbVec3f sp = pp->getObjectPoint(); 22 if(event->wasShiftDown()){ 23 ((askForValue*)p)->reportValue(sp[0], sp[1], sp[2]); 24 } else { 25 ((askForValue*)p)->reportValue(sp[0], sp[1], sp[2]); 26 } 27 } 28 eventCB = 0; 29 } 30

19.6. Specify the type of connection of a module to some data A module can refuse to be connected to some data. The autoConnect function has to return a non-zero value in this case. Here an example of a module that likes only to be connected to a HxSplineProbe: int someModule::autoConnect(HxObject* primary){ if(primary->isOfType(HxSplineProbe::getClassTypeId())){ portModule.connect(primary); return 1; }

1 2 3 4 5

185

19. Graphical interfaces return 0; }

6 7

19.7. Writing Interfaces The interface of Avizo is written in Qt. You can use Qt to generate interfaces used by your modules. The simplest case is if your reader or writer class needs some input options to be set by the user. Here an example: #include #include #include #include #include #include

"myReaderQtPanel.h"

1 2 3 4 5 6 7

myReaderQtPanel::myReaderQtPanel() : QDialog(NULL,"myReaderQtPanel",true){ flag = 0;

8 9 10 11

setCaption("MyReader Dialog"); resize(200,100);

12 13 14

QVBoxLayout *layout = new QVBoxLayout(this,10,10);

15 16

QCheckBox *checkBox = new QCheckBox("check this out!",this); layout->addWidget(checkBox);

17 18 19

QPushButton *buttonLoad = new QPushButton("Load", this); layout->addWidget(buttonLoad);

20 21 22

connect(buttonLoad, SIGNAL( clicked() ), SLOT( accept())); connect(checkBox, SIGNAL( clicked() ), SLOT( toggleFlag())); } void myReaderQtPanel::toggleFlag() {flag = 1 - flag;} int myReaderQtPanel::getFlag() {return flag;}

23 24 25 26 27

Here is the corresponding .h file: #ifndef TECPLOTREADERQTPANEL_H #define TECPLOTREADERQTPANEL_H

1 2 3

#include

4 5

class myReaderQtPanel : public QDialog { Q_OBJECT

6 7 8

186

19.7. Writing Interfaces public:

9

myReaderQtPanel(); int getFlag();

10 11 12

protected slots: void toggleFlag();

13 14 15

private: int flag; }; #endif

16 17 18 19

This Qt class implements a simple panel to setup a variable (flag). In an Avizo module you just instantiate it and then call its exec() method. This will stop for a modal dialog until you validate or close it. myReaderQtPanel panel; panel->exec(); if(panel->getFlat()) { ...

It is much easier to generate user interfaces with the QtDesigner application. It will generate a XML description of the layout (.ui) which can be used to generate the corresponding C code: ${QTDIR}\bin\uic.exe myForm.ui -o myForm.h ${QTDIR}\bin\uic.exe myForm.ui -impl myForm.h -o myForm.cpp ${QTDIR}\bin\moc.exe myForm.h -o moc_myForm.cpp

187

19. Graphical interfaces

188

20. Use of Avizo build-in libraries 20.1. mclib 20.1.1. Dynamic Arrays An implementation for dynamic arrays is McDArray. You can define the array and change its size: McDArray someData(1); theMsg->printf("size is: %d", someData.size()); someData.resizeKeep(100);

and McDMatrix. Using these structures Avizo will take care of removing the memory after the module allocating the memory is removed from the Avizo workspace.

20.1.2. The flood-fill algorithm #include #include #include

We need to do the flood fill dependent on the input data type. Therefore we will first ask our input (field) about its type. Here is the example for fields that are of type unsigned char. if(field->primType() == McPrimType::mc_uint8){ 1 McData3D* data = new McData3D(dims[0],dims[1],dims[2]); 2 data->clear(); 3 memcpy(data->dataPtr(), field->lattice.dataPtr(), 4 dims[0]*dims[1]*dims[2]*sizeof(unsigned char)); 5 6

McRangeChecker checker; checker.init(0,127,0); McBitfield out; out.resize(dims[0]*dims[1]*dims[2]); out.clearAll();

7 8 9 10 11 12

clock_t t1 = clock(); // lets also count the time we need (include also) 13 mcFloodFill3D26(*data, checker, portPosition.getValue(0), 14 portPosition.getValue(1), portPosition.getValue(2), out, 0); 15 clock_t t2 = clock(); 16

189

20. Use of Avizo build-in libraries float time = (float)(t2-t1)/CLOCKS_PER_SEC; theMsg->printf("Total time: %6.2f sec", time);

17 18 19

for(int i=0;ilattice.dataPtr())[i] = 1; else ((unsigned char *)output->lattice.dataPtr())[i] = 0; }

20 21 22 23 24 25

20.1.3. Line integration in vector fields In order to demonstrate a line integration in vector fields we will use here a module to distribute seed points in a vector field similar to what the HxDisplayISL module is using. The line integration itself will be performed with a third order Runge-Kutte method. #include #include #include ... fieldLineSet = new FieldLineSet(); particles = new ParticleSet();

1 2 3 4 5 6 7

fieldLineSet->defaultLengthForward = 30; fieldLineSet->defaultLengthBackward = 30;

8 9 10

// distribute the particles particles->init(vectorField,0,0,30); particles->setBoundingBox(bb); particles->scalarField = (HxScalarField3*)portDistField.source(); particles->dims.setValue(portResolution.getValue(0), portResolution.getValue(1), portResolution.getValue(2)); particles->resize(portOptions.getValue(0)); particles->setDistMode(portDistribute.getOptValue(), 1);

11 12 13 14 15 16 17 18 19 20

// now copy the particles to the fieldLineSet fieldLineSet->seedPoints.resize(particles->size()); for(int j=0;jsize();j++){ fieldLineSet->seedPoints[j] = (*particles)[j]->seedPoint; } // perform the integration (using ODE3) fieldLineSet->computeFieldLines(vectorField, 1E-9);

21 22 23 24 25 26 27 28

// we got back this many traced points long numPoints = fieldLineSet->points.size();

29 30 31

// here a simple loop which is looking for each time step, // for each line set, for the position and tangent information

190

32 33

20.1. mclib theWorkArea->startWorking("compute..."); for(int t=0;tsetProgressValue(t/(1.0f*(tmax-tmin))); for(int i=0;itraceStart.size();i++){ // for each line if(t > fieldLineSet->traceLength[i]-1) continue; // finish this trace

34 35 36 37 38 39 40

// now the current point is SbVec3f point = fieldLineSet->points[fieldLineSet->traceStart[i]+t]; // and its tangent is SbVec3f tangent = fieldLineSet->tangents[fieldLineSet->traceStart[i]+t]; // now do what you like with this information ...

41 42 43 44 45 46

}

47

} theWorkArea->stopWorking();

48 49

Instead of using the FieldLineSet code one can also use the McODE classes directly which allows a better control of the interpolation component of the algorithm. The McODE3 class can be initialized with the following code McODE3 ode; ode.setHSampling(); ode.setHMinMax(, ); ode.setTolerance(); ode.setRightHandSide(, this); ode.setOrientation(); ode.setNormalization(); ode.setMethod(McODE::RK34); ode.init(Point);

1 2 3 4 5 6 7 8 9

This sets up the integration using the Runge-Kutta method. The vector field would enter this code through the function argument of setRightHandSide. Here is an example: int function(const McVec3f& r, McVec3f& f, void* userData) { // the calling functions clas type is assumed to be HxMe HxMe* me = (HxMe*) userData; HxVectorField3* field = me->field; HxLocation3* loc = me->loc;

1 2 3 4 5 6 7

if (!loc->move(r.x, r.y, r.z)) { return 1; } // add code here to check for NaN or to map the vectors to a sub-space.

8 9 10 11 12 13

field->eval(loc, &f.x);

14

191

20. Use of Avizo build-in libraries return 0; }

16

The vector field is evaluated at the location r and the result is stored in f. In order to run this code one has to call Ode.next(r);

which will return the next streamline value in r.

192

15

21. Use of external libraries 21.1. Working with Numerical Recipes Here an example how to create a module which uses the Numerical Recipies functions f3tensor, matrix, rlft3, free f3tensor, free matrix, to produce two output images (imaginary and real part of the discrete fourier decomposition). In turn it can also reconstruct based on the two output images its input (backward pass). This example also generates a SpreadSheet object and fills it with the power spectrum of the data. #include #include #include #include #include



1 2 3 4 5 6

HX_INIT_CLASS(fft,HxCompModule)

7 8

fft::fft() : HxCompModule(HxUniformScalarField3::getClassTypeId()), portForwardBackward(this,"Direction",2), portOutput(this,"Output",3), portInfo(this,"Info"), portAction(this,"action") { portInfo.setValue("Please use only powers of 2"); portForwardBackward.setLabel(0,"forward"); portForwardBackward.setLabel(1,"backward"); portOutput.setLabel(0,"combined"); portOutput.setLabel(1,"phase+amplitude"); portOutput.setLabel(2,"1D power spectrum"); portAction.setLabel(0,"DoIt"); new HxConnection(this,"phase", HxUniformScalarField3::getClassTypeId()); new HxConnection(this,"ampli", HxUniformScalarField3::getClassTypeId()); }

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

Here is the compute module which will test its input/output connections and call the Numerical Recipes functions copying the result back into the Avizo workspace. #define SQR(a) ((a)*(a)) void fft::compute(){

1 2

193

21. Use of external libraries int k; HxUniformScalarField3 *output; HxUniformScalarField3 *output1; HxUniformScalarField3 *output2; int outdims[3]; HxUniformScalarField3 *data; double *pow; int *nmode; int dims[3]; HxSpreadSheet *ss; int npow = 50; // resolution of the spreadsheet

3 4 5 6 7 8 9 10 11 12 13

if (!portAction.wasHit() || connectionPorts.size() == 0) { return; } // process the inputs (it may be either combined or seperately // phase and amplitude if(connectionPorts[0]->source() != NULL){ data = (HxUniformScalarField3 *)connectionPorts[0]->source(); data->lattice.getSize(dims[0],dims[1],dims[2]); } else if(connectionPorts[1]->source() != NULL && connectionPorts[2]->source() != NULL){ HxUniformScalarField3 *phase = (HxUniformScalarField3 *) connectionPorts[1]->source(); HxUniformScalarField3 *amplitude = (HxUniformScalarField3 *) connectionPorts[2]->source(); const int* pdims = phase->lattice.dims(); const int* adims = amplitude->lattice.dims(); if(pdims[0]!=adims[0] || pdims[1]!=adims[1] || pdims[2]!=adims[2]){ theMsg->printf("ERROR: phase and ampli have different dim."); return; } dims[0] = adims[0]; dims[1] = adims[1]; dims[2] = adims[2]; data = new HxUniformScalarField3(dims,MC_DOUBLE);

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

for(int k=1;kevalReg(i-1,j-1,k-1))); data->set(i-1,j-1,2*k-1, amplitude->evalReg(i-1,j-1,k-1)*sin(phase->evalReg(i-1,j-1,k-1))); } else if(kset(i-1,j-1,2*k-1-1, amplitude->evalReg(i-1,j-1,k-1)*cos(phase->evalReg(i-1,j-1,k-1))); data->set(i-1,j-1,2*k-1, amplitude->evalReg(i-1,j-1,k-1)*sin(phase->evalReg(i-1,j-1,k-1))); data->set(i-1,j-1,dims[2]+1-k-1, amplitude->evalReg(i-1,j-1,k-1)*cos(phase->evalReg(i-1,j-1,k-1))); data->set(i-1,j-1,dims[2]+1-k-1,

194

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

21.1. Working with Numerical Recipes amplitude->evalReg(i-1,j-1,k-1)*sin(phase->evalReg(i-1,j-1,k-1))); } else{ theMsg->printf("should never happen! (fft)"); } } } } //bb {0,dims[0],0,dims[1],0,dims[2]}; float bb[6]; phase->getBoundingBox(bb); data->coords()->setBoundingBox(bb); // field->bbox()); data->lattice.getSize(dims[0],dims[1],dims[2]); McString info = phase->getLabel(); data->setLabel(info); } else // no correct input connected return;

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67

if(!checkpower2(dims[0],dims[1],dims[2])){ theMsg->printf("Error: Dim. is not power2 (use resample)."); return; } // generate the output objects if(portOutput.getValue() == 0){ // save both values in one field // (sufficient for inverse fourier) output = dynamic_cast(getResult()); if(output && !output->isOfType(HxUniformScalarField3::getClassTypeId())){ output = 0; }

68 69 70 71 72 73 74 75 76 77 78 79 80

if(output){ const int* outdims = output->lattice.dims(); if(dims[0] != outdims[0] || dims[1] != outdims[1] || dims[2] != outdims[2]){ output = 0; } }

81 82 83 84 85 86 87 88

if(!output) output = new HxUniformScalarField3(dims,MC_DOUBLE);

89 90 91

float bb[6]; data->getBoundingBox(bb); output->coords()->setBoundingBox(bb); // field->bbox());

92 93 94

} else if(portOutput.getValue() == 1){ // save the phase and amplitude in separate output objects output1 = dynamic_cast(getResult(0)); output2 = dynamic_cast(getResult(1));

95 96 97 98 99

if(output1 && !output1->isOfType(HxUniformScalarField3::getClassTypeId())){ output1 = 0; }

195

100 101 102

21. Use of external libraries if(output2 && !output2->isOfType(HxUniformScalarField3::getClassTypeId())){ output2 = 0; } if(output1){ const int* out1dims = output1->lattice.dims(); if(dims[0] != out1dims[0] || dims[1] != out1dims[1] || dims[2] != out1dims[2]){ output1 = 0; } } if(output2){ const int* out2dims = output2->lattice.dims(); if(dims[0] != out2dims[0] || dims[1] != out2dims[1] || dims[2] != out2dims[2]){ output2 = 0; } } outdims[0] = dims[0]; outdims[1] = dims[1]; outdims[2] = dims[2];

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121

if(!output1){ output1 = new HxUniformScalarField3(outdims,MC_DOUBLE); } if(!output2){ output2 = new HxUniformScalarField3(outdims,MC_DOUBLE); }

122 123 124 125 126 127 128

float bb[6]; data->getBoundingBox(bb); // = {0,dims[0],0,dims[1],0,dims[2]}; output1->coords()->setBoundingBox(bb); // field->bbox()); output2->coords()->setBoundingBox(bb); // field->bbox()); } else { // portOutput.getValue() == 2, spreadsheet with power spectrum ss = new HxSpreadSheet(); ss->composeLabel(data->getLabel().getString(),"PowerStatistics"); ss->addColumn("nmode",HxSpreadSheet::Column::FLOAT); ss->addColumn("power",HxSpreadSheet::Column::FLOAT); } float ***data1 = f3tensor(1,dims[0], 1,dims[1], 1,dims[2]); float **speq1 = matrix (1,dims[0], 1,dims[1]printf("THERE IS SOMETHING MORE????"); } } } } ss->setNumRows(npow); for(int i=0;icolumns[0].setValue(i,(double)pow[i]/nmode[i]); ss->columns[1].setValue(i,(double)nmode[i]); } free(nmode); free(pow); }

234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249

free_matrix(speq1,1,dims[0],1,dims[1]composeLabel(info,"phase"); setResult(1,output2);output2->composeLabel(info,"amplitude"); } else { // construced the spreadsheet so lets save that setResult(0,ss); }

253 254 255 256 257 258 259 260

}

261 262

bool fft::checkpower2(int x, int y, int z){ bool ret = true;

263 264 265

if( floor(log((double)x)/log((double)2)) != log((double)x)/log((double)2)) ret = false; if( floor(log((double)y)/log((double)2)) != log((double)y)/log((double)2)) ret = false; if( floor(log((double)z)/log((double)2)) != log((double)z)/log((double)2)) ret = false; return ret;

266 267 268 269 270 271 272

}

273

21.2. Matlab stuff Here an example Matlab file that will write out a surface object as AmiraMesh. Also it will write a second field that contains data per nodes. This could be either scalar data or vector data. function saveAsAvizoSurface(filename,vertices,triangles,nodeData) % saveAsAvizoSurfaces will save a surface with attached data as % an AmiraMesh data object. % Here an example: % numPoints = 8; % vertices = randn(3,numPoints); % triangles = [randperm(numPoints); randperm(numPoints); randperm(numPoints)]; % nodeData = randn(3,8); % saveAsAvizoSurface(’c:/bla’,vertices,triangles(:,1:7),nodeData);

1 2 3 4 5 6 7 8 9 10

if size(vertices,1) ~= 3, echo ’Error: vertices should have size 3xN’ return end; if size(triangles,1) ~= 3, echo ’Error: triangles should have size 3xN’ return end; if size(vertices,2) ~= size(nodeData,2), echo ’Error: number of vertices and nodes has to be the same’

11 12 13 14 15 16 17 18 19 20

199

21. Use of external libraries return end

21 22 23

% now save the surface as an AmiraMesh object fp1 = fopen(strcat(filename,’_Surface.am’),’w’); fprintf(fp1,’# HyperSurface 0.1 ASCII\n\n’); fprintf(fp1,’Parameters {\nMaterials {\nExterior { Id 1 }\n’); fprintf(fp1,’Yellow { Color 0.9 0.9 0 }\nGreen { Color 0.21 0.8 0.16 }\n}’); fprintf(fp1,’\nBoundaryIds { name \"BoundaryConditions\" }\nFilename’); fprintf(fp1,’ \"%s\"\n}\n’,strcat(filename,’_Surface.am’)); fprintf(fp1,’Vertices %d\n’, size(vertices,2)); for i = 1:size(vertices,2), fprintf(fp1, ’ %f %f %f\n’, vertices(1,i), vertices(2,i), vertices(3,i)); end fprintf(fp1,’\nPatches 1\n{\nInnerRegion Green\nOuterRegion Yello\n’); fprintf(fp1,’Triangles %d\n’, size(triangles,2)); for i = 1:size(triangles,2), fprintf(fp1, ’ %d %d %d\n’, triangles(1,i), triangles(2,i), triangles(3,i)); end fprintf(fp1, ’}\n’); fclose(fp1);

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

% now save the nodes as an AmiraMesh object fp2 = fopen(strcat(filename,’_SurfaceNodes.am’),’w’); fprintf(fp2,’# AmiraMesh 3D ASCII 2.0\n\n’); fprintf(fp2,’nNodes %d\n\nParameters {\n ContentType \"SurfaceField\",’); fprintf(fp2,’\n Encoding \"OnNodes\"\n}\n’,size(nodeData,2)); fprintf(fp2,’NodeData { float[%d] values } @1\n\n’, size(nodeData,1)); fprintf(fp2,’# Data section follows\n@1\n’); for i = 1:size(nodeData,2), for j = 1:size(nodeData,1), fprintf(fp2, ’ %f’, nodeData(j,i)); end fprintf(fp2,’\n’); end fclose(fp2);

43 44 45 46 47 48 49 50 51 52 53 54 55 56

21.3. Working with TinyXML TinyXML consists of a couple of source files that are easy to add to an existing project. Please note that Avizo contains already a mcxerces library which is supposed to be more powerful. The exmple assumes that we have an XML file that contains color information for a segmented data set. The two functions defined below can be called after the file reader and writer. They create or parse the XML structures and read or add them to the data object (usually a label field). #include

200

1

21.3. Working with TinyXML ... 2 // this will read in the following xml file structure 3 /* 4 5 6 7 8 bla.off 9 10 11 12 bla2.off 13 14 ... 15 16 */ 17 void loadXMLColorInformation(char xmlFileName[1024], HxField3 *lf) { 18 Vsg::TiXmlDocument xmlDoc(xmlFileName); 19 bool loadOK = xmlDoc.LoadFile(); 20 if(!loadOK) 21 return; 22 HxParamBundle *materials = lf->parameters.materials(1); // create it if it does 23 not exist materials->bundle("Exterior", 1); // create this first! 24 25

int mat = 0; Vsg::TiXmlElement *handle = xmlDoc.FirstChildElement("Atlas"); if(handle){ Vsg::TiXmlNode *area = handle->FirstChild("Area"); while(area != 0) { theMsg->printf("found an area node in the xml file"); McString name(""); McString id(""); int value = -1; McString abbrev(""); McString r(""); McString g(""); McString b(""); McString surfaceName("");

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

Vsg::TiXmlElement *aElem = area->ToElement(); Vsg::TiXmlAttribute *att = aElem->FirstAttribute(); while(att) { theMsg->printf("attrib: %s = %s", att->Name(), att->Value()); if(strcmp(att->Name(),"name")== 0){ name = att->Value(); name.replaceSubStr(" ", "_"); } if(strcmp(att->Name(),"id")==0) id = att->Value(); if(strcmp(att->Name(),"abbrev")==0)

41 42 43 44 45 46 47 48 49 50 51

201

21. Use of external libraries

abbrev = att->Value(); 52 if(strcmp(att->Name(),"value")==0) 53 if (att->QueryIntValue(&value)!=Vsg::TIXML_SUCCESS) 54 theMsg->printf("Error: value of material \"%s\" is not an 55 integer", name. att = att->Next(); 56

} 57 // now we need the color and surface name 58 Vsg::TiXmlNode *color = area->FirstChild("color"); 59 if(color){ 60 aElem = color->ToElement(); 61 att = aElem->FirstAttribute(); 62 while(att){ 63 if(strcmp(att->Name(), "r")==0) 64 r = att->Value(); 65 if(strcmp(att->Name(), "g")==0) 66 g = att->Value(); 67 if(strcmp(att->Name(), "b")==0) 68 b = att->Value(); 69 att = att->Next(); 70 } 71 bool ok; theMsg->printf("found color with %d %d %d", r.toInt(ok), 72 g.toInt(ok), b. } 73 Vsg::TiXmlNode *surface = area->FirstChild("Surface"); 74 if(surface){ 75 aElem = surface->ToElement(); 76 surfaceName = aElem->GetText(); 77 // theMsg->printf("found a surface with name: %s", text); 78 // surfaceName = aElem->ToText(); 79 } 80 // now we have all the information together to add the material to the81parameter sect HxParamBundle *bundle = materials->bundle(mat+1); 82 if(!bundle) 83 bundle = materials->bundle(name.getString(), 1); 84 float tc[3]; bool ok; 85 tc[0] = r.toInt(ok)/255.0; 86 if(!ok) 87 tc[0] = 0; 88 tc[1] = g.toInt(ok)/255.0; 89 if(!ok) 90 tc[1] = 0; 91 tc[2] = b.toInt(ok)/255.0; 92 if(!ok) 93 tc[2] = 0; 94 bundle->insert(new HxParameter("Color", 3, tc)); 95 bundle->insert(new HxParameter("Id", value)); // trust that this is working 96 bundle->insert(new HxParameter("abbrev", abbrev.getString())); 97 bundle->insert(new HxParameter("Surface", surfaceName.getString())); 98 99

area = area->NextSiblingElement("Area"); mat++; }

202

100 101

21.3. Working with TinyXML } 102 } 103 void saveXMLColorInformation(HxField3 *data, char xmlFileName[1024], HxUniformLabelField3 104 *lf) { if(lf){ 105 // if we have a label field we can save the color information for this field 106 as well strcat(xmlFileName, ".atlas.xml"); 107 Vsg::TiXmlDocument xmlDoc; 108 Vsg::TiXmlDeclaration *decl = new Vsg::TiXmlDeclaration( "1.0", "", "" ); 109 xmlDoc.LinkEndChild( decl ); 110 111

Vsg::TiXmlElement *hl = new Vsg::TiXmlElement( "Atlas" ); xmlDoc.LinkEndChild( hl );

112 113 114

Vsg::TiXmlComment *comment = new Vsg::TiXmlComment(); comment->SetValue(" Created with Avizo www.vsg3d.com[] "); hl->LinkEndChild( comment );

115 116 117 118

HxParamBundle *par = data->parameters.materials(); int numMaterials = par->nBundles(); for(int i = 0; i < numMaterials; i++) { // for each material define an entry in the export document HxParamBundle *currentMat = par->bundle(i); if(!currentMat) continue; const char *strName = currentMat->name(); int id; if(!currentMat->findNum("Id", id)) id = i; float col[4]; if(!currentMat->findColor(col)) col[0] = col[1] = col[2] = col[3] = 0; else { // save the colors from 0..255 col[0] *= 255; col[1] *= 255; col[2] *= 255; } Vsg::TiXmlElement *element = new Vsg::TiXmlElement( "Area" ); element->SetAttribute("abbrev", strName); element->SetAttribute("id", id); element->SetAttribute("name", strName); element->SetAttribute("value", id); hl->LinkEndChild( element );

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142

Vsg::TiXmlElement *element2 = new Vsg::TiXmlElement( "color" ); element2->SetDoubleAttribute("r", col[0]); element2->SetDoubleAttribute("g", col[1]); element2->SetDoubleAttribute("b", col[2]); element->LinkEndChild( element2 );

143 144 145 146 147 148

Vsg::TiXmlElement *msgs = new Vsg::TiXmlElement( "Surface" ); msgs->LinkEndChild( new Vsg::TiXmlText(strName) ); element->LinkEndChild( msgs );

149 150 151

203

21. Use of external libraries }

152 153

xmlDoc.SaveFile(xmlFileName); } }

204

154 155 156

Index AvizoMesh, 69, 114 2D plot, 80 64bit, 95 aminfo, 129 convert binary to ascii, 130 Arithmetic, 115 auto spin animation, 11 big endian, 55 BoundingBox, 104 Cluster create, 58 command line -debug, 96 -no gui, 23 console command all, 30 Create Scalarfield by tcl, 61 Create Menu, 83 createDocFile, 116 crop, 85 cursor color, 11 debugging, 95 Development Wizard, 95 Dimension, 104 DynamicTimeSeriesCtrl, 136 Editor crop, 85 segmentation, 85 Surface simplification, 33 transform, 87 endian, 135 Environment variables C++, 96

predefined, 11 scriping, 72 tcl, 96 field curvilinear, 157 file open save/load dialog, 97 file format PSI, 33, 58, 161 file type AvizoMesh, 45 flip, 85 heightfield, 166 http-server, 116 HxAnnaScalarField3, 61 HxCompModule, 118 HxField3, 120 HxLocation3, 120 HxParameters, 151 HxResource Avizo executable, 161 create modules, 163 data directory, 161 load data, 111 Image filters tcl, 35 install extensions, 9 on unix, 9 on windows, 9 Inventor, 24 iv file, 30 Keys Shift,Ctrl,Alt, 80

205

Index label field C++, 151 LabelField, 115 create, 59 Labelfield create by tcl, 59 LabelSet, 152 LargeDiskData, 111 license file, 10 lightning two-sided, 148 little endian, 55 logo, 24 lua, 115 magic cookies, 21 Matlab Surface write, 145 McFilename, 97 McFilenameCollector reading files, 98 networks, 124 neuron tracing, 86 opengl, 181 OrthoSlice contrast and brightness, 78 png image files, 116 Port HxPortColormap, 100 HxPortFilename, 100 HxPortFloatSlider, 100 HxPortFloatTextN, 100 HxPortIntSlider, 100 HxPortIntTextN, 100 HxPortMultiMenu, 100 HxPortRadioBox, 100 HxPortText, 100 HxPortToggleList, 100 progress bar, 72 registry store and read, 97 remote files, 55

206

resource file .rc, 69, 81, 83, 120 savePorts, 121, 124 SbColor, 152 ScalarField create, 61 screenshot, 22 shaders, 170 SpreadSheet, 195 startup file, 21 stereo, 21, 80 String tokenizer, 151 surface simplification, 33 swap, 85 text font, 182 threads, 106 update viewer, 105 workspace, 98

View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF