Adding a parameter type to DIPimage
In this post I want to show how to enable to DIPimage GUI to work with a new data type. In a previous post we developed a new class, dip_snake, for use with the function snakeminimize. The fully developed version of snakeminimize, which you can download here, integrates with the DIPimage GUI and uses DIPimage’s built-in automatic parameter parsing. We actually need to do something to DIPimage to get all that to work, because the dip_snake class is unknown to DIPimage. This post explains how to teach DIPimage about a new parameter type.
Note: This post is meant as a tutorial for people developing DIPimage applications, and contains nothing of interest to anybody else. If you’re not developing for DIPimage, you might want to look here instead.
There are two places where DIPimage needs to know about a parameter type: when generating the GUI interface to a function and when doing the automatic parameter parsing for that function (through getparams). Each function in DIPimage starts by defining a big structure that describes the input and output parameters for that function. For example, gaussf defines a structure specifying 3 input parameters and one output parameter. For each input parameter, it gives the expected data type, some constraints on that parameter, whether it is required or optional, and what the default value should be if the user doesn’t provide that parameter. The output parameter is not described in as much detail. This is what the structure looks like:
gaussf('DIP_GetParamList')
ans =
menu: 'Filters'
display: 'Gaussian filter'
inparams: [1x3 struct]
outparams: [1x1 struct]
inparams(1) =
name: 'image_in'
description: 'Input image'
type: 'image'
dim_check: []
range_check: {'tensor' 'noncomplex'}
required: 1
default: 'a'
inparams(2) =
name: 'sigma'
description: 'Sigma of gaussian'
type: 'array'
dim_check: 1
range_check: 'R+'
required: 0
default: 1
inparams(3) =
name: 'method'
description: 'Method used for computation'
type: 'option'
dim_check: 0
range_check: {'best' 'fir' 'iir' 'ft'}
required: 0
default: 'best'
outparams(1) =
name: 'image_out'
description: 'Output image'
type: 'image'
When the user selects the function “Gaussian filter” from the DIPimage GUI menu, the function gaussf is called with a special input parameter (the 'DIP_GetParamList' above) that causes it to return this data structure. This way, the GUI knows what elements to draw to let the user enter the parameters. But there is only a limited set of data types known to DIPimage. If you write a new function that returns 'snake' for the value of inparams(1).type, DIPimage will generate an error because it doesn’t recognize that parameter type, and doesn’t know what to do with it. To have it recognize it, all that is needed is to create a function called paramtype_snake in the dip/common/dipimage/private/ directory. You might need to do
clean functions
to have the toolbox find your new file. All files starting with paramtype_ in that directory are good examples for you to build your own. Pick the parameter type that’s closest to what you need, and start from there. The string that comes after the underscore in the file name is the name of the parameter type as it will be known to DIPimage.
The function paramtype_snake will be called in multiple manners, the first argument is always a “command” that needs to be executed. We’ll go over these commands one by one.
function varargout = paramtype_snake(command,param,varargin) switch command
The first command is 'control_create'. This is the command used by the DIPimage GUI to create a control for the particular input argument. [Edit: This functionality was slightly simplified in DIPimage 2.2, I've edited this post to reflect these changes.] Thus, when creating the GUI for gaussf, the following three commands are executed:
h1 = paramtype_image('control_create',inparams(1),h); h2 = paramtype_array('control_create',inparams(2),h); h3 = paramtype_option('control_create',inparams(3),h);
The 'control_create' command has 2 parameters: the parameter definition structure and the figure handle in which to create the control (this is always the DIPimage GUI figure). The output is a handle to the control (or an array with two handles as in, for example, the “infile” parameter type). The GUI will take care of placing the control in the right place, all that you need to do is create the control using the uicontrol function. The initial value of the control should, of course, be the default value given in the parameter definition param. The following code does this for our new parameter type:
case 'control_create' % arg 1 = figure handle. fig = varargin{1}; h = uicontrol(fig,... 'Style','edit',... 'String',param.default,... 'Visible','off',... 'HorizontalAlignment','left',... 'BackgroundColor',[1,1,1],... 'ButtonDownFcn','dipimage do_contextmenu dip_snake'); cm = uicontextmenu('Parent',fig,'UserData',h); set(h,'UserData',struct('contextmenu',cm)); varargout{1} = h;
Note that I added a context menu by creating an empty context menu and putting its handle in the 'UserData' element of the control, and setting a 'ButtonDownFcn' callback. This callback will add all objects of type dip_snake defined in the workspace at the time that the user right-clicks the control, and show the context menu. Feel free to add other types of callback to your control!
When the user is done entering the parameter values in the GUI, and presses the “execute” button, the parameter values need to be read out. Because this is dependent on the type of control created, our function must take care of this as well. It will be called with the command 'control_value', and is expected to return the parameter read from the control. This is a good moment to, for example, translate possible wrong inputs into a correct input. Our control was an edit box. We will read the string from the edit box and evaluate it in the base workspace. This allows the user to type in a variable name, but also a more complex expression whose result will be used as the input:
case 'control_value' varargout{2} = get(varargin{1},'String'); if isempty(varargout{2}) varargout{2} = '[]'; end varargout{1} = evalin('base',varargout{2}); if ~isa(varargout{1},'dip_snake') varargout{1} = dip_snake(varargout{1}); end
Note that there are two output arguments: the first one is the parameter value, the second one is the string entered by the user. This string will be printed to the command line as feedback.
This is all that’s needed for the GUI. But there’s a second set of commands needed for getparams, the automatic parameter parsing built into DIPimage. Whenever you call gaussf, either through the GUI or the command line, it needs to validate the input arguments and possibly convert them to the expected data types. It does so by calling getparams, and passing on the structure defining all input parameters as well as its full list of input arguments. The output is those same parameters, tested, corrected and filled out with defaults where needed. This system avoids a lot of common code in all the DIPimage functions, since the input parameters tend to be so similar for many functions. To help getparams parse the new parameter type we need two more commands.
The first one it needs, 'default_value', simply returns the default value for the parameter. Usually this is accomplished by reading the appropriate field in the params structure, but some parameter types do it differently. For example, the “handle” type returns the output of gcf, totally ignoring any defaults defined in the parameter structure. For our “snake” parameter type we do this:
case 'default_value' try varargout{1} = evalin('base',param.default); if ~isa(varargout{1},'dip_snake') varargout{1} = dip_snake(varargout{1}); end catch error('Default dip_snake evaluation failed.') end
Just as in the “image” data type, we assume that the default value is a string that needs to be evaluated to get an actual value. The other command, 'value_test', is the one doing the actual work. For our “snake” parameter type it is quite simple, but I invite you to examine the corresponding code for the “image” or “array” data types (<grin>): a lot more checking is required, since a lot more options are offered. For example, in the “array” data type this command checks the input array for size, limits, and optionally to make sure only integer values are given. Here we will simply test the argument to see if it is of class dip_snake, and if not, try to convert it to that class:
case 'value_test' varargout{1} = ''; value = varargin{1}; if ~isa(value,'dip_snake') & ~isnumeric(value) varargout{1} = 'dip_snake expected'; elseif nargout>1 & ~isa(value,'dip_snake') value = dip_snake(value); end varargout{2} = value;
The fifth and final command that our function must handle is a test for correctness of the parameter definition structure. This one does not test the end user entering proper values, but tests the person writing the function that uses our data type. This code tests the structure so that other parts of the function do not need to do those tests. Again, for the “snake” parameter type this is rather simple, because so far we have only used the param.default value; we don’t need to check anything else. More complex parameter types might have more complex checks.
case 'definition_test' varargout{1} = ''; if ~param.required if ~ischar(param.default) varargout{1} = 'DEFAULT dip_snake must be a string'; end end
We need to end our file by closing the switch statement started at the beginning:
end
That’s all folks! Try out the function snakeminimize to see all of this working. You can get it right here.
RSS: