diff --git a/urchin_compare/+Dev/VersionInfo.m b/urchin_compare/+Dev/VersionInfo.m
new file mode 100644
index 0000000000000000000000000000000000000000..2002fa5fbb50dbde8291e29c1913bdaa6285cbb8
--- /dev/null
+++ b/urchin_compare/+Dev/VersionInfo.m
@@ -0,0 +1,10 @@
+% versionInfo = VersionInfo()
+% Return the version info structure
+%
+% Note: This file is autogenerated by build script DO NOT MODIFY!!
+
+function versionInfo = VersionInfo()
+    jsonVer = '{"name":"urchin_compare","majorVersion":2,"minorVersion":0,"branchName":"master","buildNumber":"2020.12.07.07","buildMachine":"bioimage28","commitHash":[" : 15410c6b7251a6890057e04366431f0286b90d70"," : 07f725fe7b1c500a2743e016d119a7b0f059561e"]}';
+    
+    versionInfo = Utils.ParseJSON(jsonVer);
+end
diff --git a/urchin_compare/.gitignore b/urchin_compare/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..4051d2a75c4484099154eca55f55295928e90b7c
--- /dev/null
+++ b/urchin_compare/.gitignore
@@ -0,0 +1,6 @@
+# Ignore matlab data files
+*.mat
+
+# Ignore some image formats
+*.jpg
+*.nd2
\ No newline at end of file
diff --git a/urchin_compare/compile_urchin.m b/urchin_compare/compile_urchin.m
new file mode 100644
index 0000000000000000000000000000000000000000..b1e40b68891aa442a13bc2b0bac0d7ff35cdb062
--- /dev/null
+++ b/urchin_compare/compile_urchin.m
@@ -0,0 +1,46 @@
+function compile_urchin()
+    %% General compiler setup: Deals with version updates and pulls external dependencies
+    initStruct = Dev.InitCompiler('urchin_compare','2.0');
+    if ( isempty(initStruct) )
+        % User exited the build due to error or uncommited dependencies.
+        return;
+    end
+    
+    compileMATLAB('urchin_compare','urchin_compare','', initStruct.javaList, initStruct.toolboxList);
+end
+
+function outputFile = compileMATLAB(projectName, mainscript, outputdir, extrasList, toolboxList)
+    compileTime = tic();
+    
+    outputFile = [projectName '.exe'];
+    
+    if ( ~exist('extrasList','var') )
+        extrasList = {};
+    end
+    
+	extraCommand = '';
+    if ( ~isempty(extrasList) )
+        extraElems = cellfun(@(x)([' -a ' x]),extrasList, 'UniformOutput',0);
+        extraCommand = [extraElems{:}];
+    end
+    
+    if ( ~exist('toolboxList','var') )
+        toolboxList = {};
+    end
+    
+    toolboxAddCommand = '';
+    if ( ~isempty(toolboxList) )
+        toolboxElems = cellfun(@(x)([' -p "' x '"']), toolboxList, 'UniformOutput',0);
+        toolboxAddCommand = ['-N' toolboxElems{:}];
+    end
+    
+    fprintf('\nMATLAB Compiling: %s...\n', outputFile);
+    result = system(['mcc -v -R -startmsg -m ' mainscript '.m -o ' projectName ' ' toolboxAddCommand extraCommand]);
+    if ( result ~= 0 )
+        error([projectName ': MATLAB compile failed.']);
+    end
+    
+    system(['copy ' projectName '.exe ' fullfile(outputdir,'.')]);
+    
+    fprintf('Done (%f sec)\n', toc(compileTime));
+end
diff --git a/urchin_compare/helpdlg.fig b/urchin_compare/helpdlg.fig
new file mode 100644
index 0000000000000000000000000000000000000000..e48d4f55d39dda373ec42b2bf8b1e20511c2e014
Binary files /dev/null and b/urchin_compare/helpdlg.fig differ
diff --git a/urchin_compare/helpdlg.m b/urchin_compare/helpdlg.m
new file mode 100644
index 0000000000000000000000000000000000000000..c58ee283ab96fea102b203b2f4ab03ec27159adc
--- /dev/null
+++ b/urchin_compare/helpdlg.m
@@ -0,0 +1,86 @@
+function varargout = helpdlg(varargin)
+% HELPDLG MATLAB code for helpdlg.fig
+%      HELPDLG, by itself, creates a new HELPDLG or raises the existing
+%      singleton*.
+%
+%      H = HELPDLG returns the handle to a new HELPDLG or the handle to
+%      the existing singleton*.
+%
+%      HELPDLG('CALLBACK',hObject,eventData,handles,...) calls the local
+%      function named CALLBACK in HELPDLG.M with the given input arguments.
+%
+%      HELPDLG('Property','Value',...) creates a new HELPDLG or raises the
+%      existing singleton*.  Starting from the left, property value pairs are
+%      applied to the GUI before helpdlg_OpeningFcn gets called.  An
+%      unrecognized property name or invalid value makes property application
+%      stop.  All inputs are passed to helpdlg_OpeningFcn via varargin.
+%
+%      *See GUI Options on GUIDE's Tools menu.  Choose "GUI allows only one
+%      instance to run (singleton)".
+%
+% See also: GUIDE, GUIDATA, GUIHANDLES
+
+% Edit the above text to modify the response to help helpdlg
+
+% Last Modified by GUIDE v2.5 18-Jun-2019 03:23:18
+
+% Begin initialization code - DO NOT EDIT
+gui_Singleton = 1;
+gui_State = struct('gui_Name',       mfilename, ...
+                   'gui_Singleton',  gui_Singleton, ...
+                   'gui_OpeningFcn', @helpdlg_OpeningFcn, ...
+                   'gui_OutputFcn',  @helpdlg_OutputFcn, ...
+                   'gui_LayoutFcn',  [] , ...
+                   'gui_Callback',   []);
+if nargin && ischar(varargin{1})
+    gui_State.gui_Callback = str2func(varargin{1});
+end
+
+if nargout
+    [varargout{1:nargout}] = gui_mainfcn(gui_State, varargin{:});
+else
+    gui_mainfcn(gui_State, varargin{:});
+end
+% End initialization code - DO NOT EDIT
+
+
+% --- Executes just before helpdlg is made visible.
+function helpdlg_OpeningFcn(hObject, eventdata, handles, varargin)
+% This function has no output args, see OutputFcn.
+% hObject    handle to figure
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+% varargin   command line arguments to helpdlg (see VARARGIN)
+
+% Choose default command line output for helpdlg
+handles.output = hObject;
+
+% Update handles structure
+guidata(hObject, handles);
+
+% UIWAIT makes helpdlg wait for user response (see UIRESUME)
+% uiwait(handles.help_fig);
+
+
+% --- Outputs from this function are returned to the command line.
+function varargout = helpdlg_OutputFcn(hObject, eventdata, handles) 
+% varargout  cell array for returning output args (see VARARGOUT);
+% hObject    handle to figure
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Get default command line output from handles structure
+varargout{1} = handles.output;
+
+
+% --- Executes on key press with focus on help_fig and none of its controls.
+function help_fig_KeyPressFcn(hObject, eventdata, handles)
+% hObject    handle to help_fig (see GCBO)
+% eventdata  structure with the following fields (see MATLAB.UI.FIGURE)
+%	Key: name of the key that was pressed, in lower case
+%	Character: character interpretation of the key(s) that was pressed
+%	Modifier: name(s) of the modifier key(s) (i.e., control, shift) pressed
+% handles    structure with handles and user data (see GUIDATA)
+if ( strcmpi(eventdata.Key,'escape') )
+    close(hObject);
+end
diff --git a/urchin_compare/settings.json b/urchin_compare/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..0a04ea2ea1046977c8663c03147abeecae8dbb84
--- /dev/null
+++ b/urchin_compare/settings.json
@@ -0,0 +1,8 @@
+{
+  "imagePath" : "H:\\Smadar\\2020-10-Phallodin_Myo",
+  "dataPath" : "H:\\Smadar\\2020-10-Phallodin_Myo",
+  "channelColors" : [
+                      [0, 1, 0],
+                      [1, 0, 0]
+                    ]
+}
diff --git a/urchin_compare/toolsdlg.fig b/urchin_compare/toolsdlg.fig
new file mode 100644
index 0000000000000000000000000000000000000000..be3cc86a14f3b050d531c93ae21c3d150b0c3cd6
Binary files /dev/null and b/urchin_compare/toolsdlg.fig differ
diff --git a/urchin_compare/toolsdlg.m b/urchin_compare/toolsdlg.m
new file mode 100644
index 0000000000000000000000000000000000000000..4c2151738a95f23da57048d96dd62efdccade42c
--- /dev/null
+++ b/urchin_compare/toolsdlg.m
@@ -0,0 +1,537 @@
+function varargout = toolsdlg(varargin)
+% TOOLSDLG MATLAB code for tools.fig
+%      TOOLSDLG, by itself, creates a new TOOLSDLG or raises the existing
+%      singleton*.
+%
+%      H = TOOLSDLG returns the handle to a new TOOLSDLG or the handle to
+%      the existing singleton*.
+%
+%      TOOLSDLG('CALLBACK',hObject,eventData,handles,...) calls the local
+%      function named CALLBACK in TOOLSDLG.M with the given input arguments.
+%
+%      TOOLSDLG('Property','Value',...) creates a new TOOLSDLG or raises the
+%      existing singleton*.  Starting from the left, property value pairs are
+%      applied to the GUI before toolsdlg_OpeningFcn gets called.  An
+%      unrecognized property name or invalid value makes property application
+%      stop.  All inputs are passed to toolsdlg_OpeningFcn via varargin.
+%
+%      *See GUI Options on GUIDE's Tools menu.  Choose "GUI allows only one
+%      instance to run (singleton)".
+%
+% See also: GUIDE, GUIDATA, GUIHANDLES
+
+% Edit the above text to modify the response to help toolsdlg
+
+% Last Modified by GUIDE v2.5 01-Dec-2020 18:35:44
+
+% Begin initialization code - DO NOT EDIT
+gui_Singleton = 1;
+gui_State = struct('gui_Name',       mfilename, ...
+                   'gui_Singleton',  gui_Singleton, ...
+                   'gui_OpeningFcn', @toolsdlg_OpeningFcn, ...
+                   'gui_OutputFcn',  @toolsdlg_OutputFcn, ...
+                   'gui_LayoutFcn',  [] , ...
+                   'gui_Callback',   []);
+if nargin && ischar(varargin{1})
+    gui_State.gui_Callback = str2func(varargin{1});
+end
+
+if nargout
+    [varargout{1:nargout}] = gui_mainfcn(gui_State, varargin{:});
+else
+    gui_mainfcn(gui_State, varargin{:});
+end
+% End initialization code - DO NOT EDIT
+
+
+% --- Executes just before toolsdlg is made visible.
+function toolsdlg_OpeningFcn(hObject, eventdata, handles, varargin)
+% This function has no output args, see OutputFcn.
+% hObject    handle to figure
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+% varargin   command line arguments to toolsdlg (see VARARGIN)
+
+% Choose default command line output for toolsdlg
+handles.output = hObject;
+
+% Update handles structure
+guidata(hObject, handles);
+
+% UIWAIT makes toolsdlg wait for user response (see UIRESUME)
+% uiwait(handles.tools_fig);
+
+
+% --- Outputs from this function are returned to the command line.
+function varargout = toolsdlg_OutputFcn(hObject, eventdata, handles) 
+% varargout  cell array for returning output args (see VARARGOUT);
+% hObject    handle to figure
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Get default command line output from handles structure
+varargout{1} = handles.output;
+
+% Helpers for updating editbox/sliders
+function v = clamp(v, minv,maxv)
+    v = max(minv, min(v, maxv));
+
+% --- Executes on slider movement.
+function update_editbox(hTxt,hSlider)
+    val = get(hSlider, 'Value');
+    if ( ~isempty(hTxt) )
+        set(hTxt, 'String',num2str(val));
+    end
+
+% --- Executes on slider movement.
+function update_slider(hTxt,hSlider)
+    strval = get(hTxt, 'String');
+    val = str2double(strval);
+    if ( ~isempty(hSlider) )
+        val = clamp(val, get(hSlider,'Min'),get(hSlider,'Max'));
+        set(hSlider, 'Value',val);
+    end
+
+% --- Executes on slider movement.
+function contrast_ch2_Callback(hObject, eventdata, handles)
+% hObject    handle to contrast_ch2 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'Value') returns position of slider
+%        get(hObject,'Min') and get(hObject,'Max') to determine range of slider
+global UIInfo
+hTxt = findobj('Tag','contrast_ch2_edit');
+update_editbox(hTxt,hObject);
+UIInfo.updateFcn();
+
+
+% --- Executes during object creation, after setting all properties.
+function contrast_ch2_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to contrast_ch2 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: slider controls usually have a light gray background.
+if isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor',[.9 .9 .9]);
+end
+
+
+% --- Executes on slider movement.
+function bright_ch2_Callback(hObject, eventdata, handles)
+% hObject    handle to bright_ch2 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'Value') returns position of slider
+%        get(hObject,'Min') and get(hObject,'Max') to determine range of slider
+global UIInfo
+hTxt = findobj('Tag','bright_ch2_edit');
+update_editbox(hTxt,hObject);
+UIInfo.updateFcn();
+
+
+% --- Executes during object creation, after setting all properties.
+function bright_ch2_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to bright_ch2 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: slider controls usually have a light gray background.
+if isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor',[.9 .9 .9]);
+end
+
+
+% --- Executes on button press in color_ch2.
+function color_ch2_Callback(hObject, eventdata, handles)
+% hObject    handle to color_ch2 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+global UIInfo
+newC = uisetcolor(get(hObject, 'BackgroundColor'));
+set(hObject, 'BackgroundColor', newC);
+UIInfo.updateFcn();
+
+
+% --- Executes on button press in color_ch1.
+function color_ch1_Callback(hObject, eventdata, handles)
+% hObject    handle to color_ch1 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+global UIInfo
+newC = uisetcolor(get(hObject, 'BackgroundColor'));
+set(hObject, 'BackgroundColor', newC);
+UIInfo.updateFcn();
+
+
+% --- Executes on slider movement.
+function bright_ch1_Callback(hObject, eventdata, handles)
+% hObject    handle to bright_ch1 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'Value') returns position of slider
+%        get(hObject,'Min') and get(hObject,'Max') to determine range of slider
+global UIInfo
+hTxt = findobj('Tag','bright_ch1_edit');
+update_editbox(hTxt,hObject);
+UIInfo.updateFcn();
+
+
+% --- Executes during object creation, after setting all properties.
+function bright_ch1_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to bright_ch1 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: slider controls usually have a light gray background.
+if isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor',[.9 .9 .9]);
+end
+
+
+% --- Executes on slider movement.
+function contrast_ch1_Callback(hObject, eventdata, handles)
+% hObject    handle to contrast_ch1 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'Value') returns position of slider
+%        get(hObject,'Min') and get(hObject,'Max') to determine range of slider
+global UIInfo
+hTxt = findobj('Tag','contrast_ch1_edit');
+update_editbox(hTxt,hObject);
+UIInfo.updateFcn();
+
+
+% --- Executes during object creation, after setting all properties.
+function contrast_ch1_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to contrast_ch1 (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: slider controls usually have a light gray background.
+if isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor',[.9 .9 .9]);
+end
+
+
+% --- Executes on button press in show_regions.
+function show_regions_Callback(hObject, eventdata, handles)
+% hObject    handle to show_regions (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hint: get(hObject,'Value') returns toggle state of show_regions
+global UIInfo
+UIInfo.updateFcn();
+
+
+% --- Executes on button press in show_ch1px.
+function show_ch1px_Callback(hObject, eventdata, handles)
+% hObject    handle to show_ch1px (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hint: get(hObject,'Value') returns toggle state of show_ch1px
+global UIInfo
+UIInfo.updateFcn();
+
+
+% --- Executes on button press in show_ch2px.
+function show_ch2px_Callback(hObject, eventdata, handles)
+% hObject    handle to show_ch2px (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hint: get(hObject,'Value') returns toggle state of show_ch2px
+global UIInfo
+UIInfo.updateFcn();
+
+
+% --- Executes on slider movement.
+function ch1_thresh_Callback(hObject, eventdata, handles)
+% hObject    handle to ch1_thresh (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'Value') returns position of slider
+%        get(hObject,'Min') and get(hObject,'Max') to determine range of slider
+global UIInfo
+hTxt = findobj('Tag','ch1_thresh_edit');
+update_editbox(hTxt,hObject);
+UIInfo.updateThresh();
+
+
+% --- Executes during object creation, after setting all properties.
+function ch1_thresh_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to ch1_thresh (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: slider controls usually have a light gray background.
+if isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor',[.9 .9 .9]);
+end
+
+
+% --- Executes on slider movement.
+function ch2_thresh_Callback(hObject, eventdata, handles)
+% hObject    handle to ch2_thresh (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'Value') returns position of slider
+%        get(hObject,'Min') and get(hObject,'Max') to determine range of slider
+global UIInfo
+hTxt = findobj('Tag','ch2_thresh_edit');
+update_editbox(hTxt,hObject);
+UIInfo.updateThresh();
+
+
+% --- Executes during object creation, after setting all properties.
+function ch2_thresh_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to ch2_thresh (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: slider controls usually have a light gray background.
+if isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor',[.9 .9 .9]);
+end
+
+
+% --- Executes when selected cell(s) is changed in cells_table.
+function cells_table_CellSelectionCallback(hObject, eventdata, handles)
+% hObject    handle to cells_table (see GCBO)
+% eventdata  structure with the following fields (see MATLAB.UI.CONTROL.TABLE)
+%	Indices: row and column indices of the cell(s) currently selecteds
+% handles    structure with handles and user data (see GUIDATA)
+global UIInfo
+
+regionID = -1;
+if ( ~isempty(eventdata.Indices) )
+    Data = get(hObject, 'Data');
+    regionID = Data{eventdata.Indices(1),1};
+end
+
+UIInfo.updateRegionSelect(regionID);
+
+
+% --- Executes when entered data in editable cell(s) in cells_table.
+function cells_table_CellEditCallback(hObject, eventdata, handles)
+% hObject    handle to cells_table (see GCBO)
+% eventdata  structure with the following fields (see MATLAB.UI.CONTROL.TABLE)
+%	Indices: row and column indices of the cell(s) edited
+%	PreviousData: previous data for the cell(s) edited
+%	EditData: string(s) entered by the user
+%	NewData: EditData or its converted form set on the Data property. Empty if Data was not changed
+%	Error: error string when failed to convert EditData to appropriate value for Data
+% handles    structure with handles and user data (see GUIDATA)
+global UIInfo
+
+Data = get(hObject, 'Data');
+
+editData = eventdata.EditData;
+if ( ischar(editData) )
+    %% Support empty if space-bar is used
+    editData = strtrim(editData);
+end
+
+if ( isempty(editData) )
+    %% Allow user to set empty data
+    Data{eventdata.Indices(1),eventdata.Indices(2)} = [];
+    set(hObject, 'Data',Data);
+end
+% % User modifiable count variable
+% cellID = Data{eventdata.Indices(1),1};
+% newCount = Data{eventdata.Indices(1),eventdata.Indices(2)};
+
+% UIInfo.updateCellCount(cellID,newCount);
+
+
+% --- Executes when user attempts to close tools_fig.
+function tools_fig_CloseRequestFcn(hObject, eventdata, handles)
+% hObject    handle to tools_fig (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hint: delete(hObject) closes the figure
+global UIInfo
+set(hObject, 'Visible','off');
+UIInfo.updateMenu();
+
+
+
+function ch1_thresh_edit_Callback(hObject, eventdata, handles)
+% hObject    handle to ch1_thresh_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'String') returns contents of ch1_thresh_edit as text
+%        str2double(get(hObject,'String')) returns contents of ch1_thresh_edit as a double
+global UIInfo
+hSlider = findobj('Tag','ch1_thresh');
+update_slider(hObject,hSlider);
+UIInfo.updateThresh();
+
+
+% --- Executes during object creation, after setting all properties.
+function ch1_thresh_edit_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to ch1_thresh_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: edit controls usually have a white background on Windows.
+%       See ISPC and COMPUTER.
+if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor','white');
+end
+hSlider = findobj('Tag','ch1_thresh');
+update_editbox(hObject,hSlider);
+
+
+
+function ch2_thresh_edit_Callback(hObject, eventdata, handles)
+% hObject    handle to ch2_thresh_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'String') returns contents of ch2_thresh_edit as text
+%        str2double(get(hObject,'String')) returns contents of ch2_thresh_edit as a double
+global UIInfo
+hSlider = findobj('Tag','ch2_thresh');
+update_slider(hObject,hSlider);
+UIInfo.updateThresh();
+
+
+% --- Executes during object creation, after setting all properties.
+function ch2_thresh_edit_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to ch2_thresh_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: edit controls usually have a white background on Windows.
+%       See ISPC and COMPUTER.
+if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor','white');
+end
+hSlider = findobj('Tag','ch2_thresh');
+update_editbox(hObject,hSlider);
+
+
+
+function bright_ch2_edit_Callback(hObject, eventdata, handles)
+% hObject    handle to bright_ch2_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'String') returns contents of bright_ch2_edit as text
+%        str2double(get(hObject,'String')) returns contents of bright_ch2_edit as a double
+global UIInfo
+hSlider = findobj('Tag','bright_ch2');
+update_slider(hObject,hSlider);
+UIInfo.updateFcn();
+
+
+% --- Executes during object creation, after setting all properties.
+function bright_ch2_edit_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to bright_ch2_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: edit controls usually have a white background on Windows.
+%       See ISPC and COMPUTER.
+if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor','white');
+end
+hSlider = findobj('Tag','bright_ch2');
+update_editbox(hObject,hSlider);
+
+
+
+function contrast_ch2_edit_Callback(hObject, eventdata, handles)
+% hObject    handle to contrast_ch2_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'String') returns contents of contrast_ch2_edit as text
+%        str2double(get(hObject,'String')) returns contents of contrast_ch2_edit as a double
+global UIInfo
+hSlider = findobj('Tag','contrast_ch2');
+update_slider(hObject,hSlider);
+UIInfo.updateFcn();
+
+
+% --- Executes during object creation, after setting all properties.
+function contrast_ch2_edit_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to contrast_ch2_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: edit controls usually have a white background on Windows.
+%       See ISPC and COMPUTER.
+if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor','white');
+end
+hSlider = findobj('Tag','contrast_ch2');
+update_editbox(hObject,hSlider);
+
+
+function bright_ch1_edit_Callback(hObject, eventdata, handles)
+% hObject    handle to bright_ch1_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'String') returns contents of bright_ch1_edit as text
+%        str2double(get(hObject,'String')) returns contents of bright_ch1_edit as a double
+global UIInfo
+hSlider = findobj('Tag','bright_ch1');
+update_slider(hObject,hSlider);
+UIInfo.updateFcn();
+
+
+% --- Executes during object creation, after setting all properties.
+function bright_ch1_edit_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to bright_ch1_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: edit controls usually have a white background on Windows.
+%       See ISPC and COMPUTER.
+if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor','white');
+end
+hSlider = findobj('Tag','bright_ch1');
+update_editbox(hObject,hSlider);
+
+
+
+function contrast_ch1_edit_Callback(hObject, eventdata, handles)
+% hObject    handle to contrast_ch1_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    structure with handles and user data (see GUIDATA)
+
+% Hints: get(hObject,'String') returns contents of contrast_ch1_edit as text
+%        str2double(get(hObject,'String')) returns contents of contrast_ch1_edit as a double
+global UIInfo
+hSlider = findobj('Tag','contrast_ch1');
+update_slider(hObject,hSlider);
+UIInfo.updateFcn();
+
+
+% --- Executes during object creation, after setting all properties.
+function contrast_ch1_edit_CreateFcn(hObject, eventdata, handles)
+% hObject    handle to contrast_ch1_edit (see GCBO)
+% eventdata  reserved - to be defined in a future version of MATLAB
+% handles    empty - handles not created until after all CreateFcns called
+
+% Hint: edit controls usually have a white background on Windows.
+%       See ISPC and COMPUTER.
+if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
+    set(hObject,'BackgroundColor','white');
+end
+hSlider = findobj('Tag','contrast_ch1');
+update_editbox(hObject,hSlider);
diff --git a/urchin_compare/urchin_compare.m b/urchin_compare/urchin_compare.m
new file mode 100644
index 0000000000000000000000000000000000000000..d38f4ab2c931b789f9c9feb6f29194fe9949141f
--- /dev/null
+++ b/urchin_compare/urchin_compare.m
@@ -0,0 +1,1232 @@
+function urchin_compare()
+    init_globals();
+    load_config();
+    
+    ui_initialize_main();
+end
+
+function init_globals()
+    global CONSTANTS UIInfo RegionInfo imC bwCh
+    
+    imC = [];
+    bwCh = [];
+    
+    RegionInfo = [];
+    CONSTANTS = [];
+    
+    UIInfo.mainFig = figure();
+    UIInfo.toolFig = [];
+    
+    reset_ui_fields();
+end
+
+function reset_ui_fields()
+    global UIInfo
+    
+    UIInfo.pad = zeros(1,2);
+    UIInfo.padDelta = zeros(1,2);
+    
+    UIInfo.mousePt = zeros(1,2);
+    UIInfo.zoom = 0;
+    UIInfo.selectedRegion = -1;
+    
+    UIInfo.edited = false;
+    UIInfo.panning = false;
+    
+    clf(UIInfo.mainFig);
+end
+
+function imPath = get_image_path(rootDir)
+    imPath = '';
+
+    [filename,pathname] = uigetfile(fullfile(rootDir, '*.*'), 'Open image file');
+    if ( filename == 0 )
+        return;
+    end
+    
+    imPath = fullfile(pathname,filename);
+end
+
+function [im,imD] = fixup_im_channels(im,imD)
+    chNames = {};
+    if ( isfield(imD,'ChannelNames') )
+        chNames = imD.ChannelNames;
+    end
+    
+    if ( ~isempty(chNames) )
+        keepNames = {'Alexa 488'; 'Cy3.5'};
+        sidx = zeros(length(chNames),1);
+        for i=1:length(keepNames)
+            bkn = startsWith(chNames, keepNames{i});
+            sidx(bkn) = i;
+        end
+
+        srtCh = zeros(length(keepNames),1);
+        for i=1:length(keepNames)
+            srtCh(i) = find(sidx==i,1,'first');
+        end
+        
+        imD.ChannelNames = chNames(srtCh);
+    else
+        srtCh = [1;2];
+    end
+    
+    imD.NumberOfChannels = 2;
+    im = im(:,:,:,srtCh,:);
+end
+
+function [im,imD,imPath] = load_image(imPath)
+    im = [];
+    
+    [pathname,filename,fext] = fileparts(imPath);
+    imD = MicroscopeData.Original.ReadMetadata(pathname,[filename fext]);
+    if ( isempty(imD) )
+        return;
+    end
+    
+    im = MicroscopeData.Original.ReadImages(pathname,[filename fext], [], 'zList',[1]);
+    if ( isempty(im) )
+        return;
+    end
+    
+    [im,imD] = fixup_im_channels(im,imD);
+end
+
+%% ---- Size compat-helper
+function sz_dims = size_dims(A, dims)
+    sz_dims = arrayfun(@(x)(size(A,x)), dims);
+end
+
+%% ---- Config handling ----
+function load_config()
+    global Config
+    
+    Config = struct('imagePath',{''},...
+                    'dataPath',{''},...
+                    'channelColors',{[0,1,0; 1,0,0]});
+    
+    % Check for a valid settings file
+    if ( ~exist('settings.json','file') )
+        return;
+    end
+    
+    jsonData = fileread('settings.json');
+    settings = jsondecode(jsonData);
+    
+    if ( isempty(settings) )
+        return;
+    end
+    
+    fields = fieldnames(settings);
+    for i=1:length(fields)
+        Config.(fields{i}) = settings.(fields{i});
+    end
+end
+
+function save_config()
+    global Config
+    
+    jsonData = Utils.CreateJSON(Config);
+    fid = fopen('settings.json','wt');
+    if ( fid >= 0 )
+        fprintf(fid, '%s\n', jsonData);
+        fclose(fid);
+    end
+end
+
+function set_datapath(datapath)
+    global Config
+    
+    pathname = fileparts(datapath);
+    Config.dataPath = pathname;
+end
+
+function set_imagepath(imagepath)
+    global Config
+    
+    pathname = fileparts(imagepath);
+    Config.imagePath = pathname;
+end
+
+
+%% ---- Draw Functions ----
+function draw_image()
+    global imC UIInfo
+    
+    if ( isempty(imC) )
+        return;
+    end
+    
+    imR = im_composite_channels(imC, UIInfo.Cmap,UIInfo.Tfn);
+    
+    render_image(UIInfo.mainFig, imR);
+    draw_regions();
+end
+
+function xor_cmap = color_xor(cmap)
+    xor_cmap = sum(cmap,1);
+    while ( any(xor_cmap > 1.0) )
+        bgo = (xor_cmap > 1.0);
+        xor_cmap(bgo) = xor_cmap(bgo) - 1.0;
+    end
+end
+
+function draw_regions()
+    global UIInfo RegionInfo
+    
+    curAx = get(UIInfo.mainFig, 'CurrentAxes');
+    
+    if ( UIInfo.bShowRegions )
+        for i=1:length(RegionInfo)
+            if ( isempty(RegionInfo(i).id) )
+                continue;
+            end
+            
+            [~,uridx] = sortrows(RegionInfo(i).pts);
+            
+            drawStyle = '-';
+            if ( UIInfo.selectedRegion == i )
+                drawStyle = ':';
+            end
+            plot(RegionInfo(i).pts(:,1),RegionInfo(i).pts(:,2), 'w',...
+                'LineWidth',2,...
+                'LineStyle',drawStyle,...
+                'Parent',curAx,...
+                'ButtonDownFcn',@(src,evt)(set_region_select(i)));
+            
+            text(curAx,...
+                RegionInfo(i).pts(uridx(end),1)+1,...
+                RegionInfo(i).pts(uridx(end),2)+1,...
+                num2str(RegionInfo(i).id),...
+                'Color','w',...
+                'VerticalAlignment','top');
+        end
+    end
+    
+    % Figure out a distinctive overlap color (if all channels on)
+    bShowCh = [UIInfo.bShowChPx, any(UIInfo.bShowChPx)];
+    % NOTE/HACK: color_xor runs across all channels passed in so it's important to start with [0,0,0] for overlap
+    pxCmap = [UIInfo.Cmap; 0 0 0];
+    pxCmap(3,:) = color_xor(pxCmap(bShowCh,:));
+    for c=1:3
+        if ( ~bShowCh(c) )
+            continue;
+        end
+
+        for i=1:length(RegionInfo)
+            if ( isempty(RegionInfo(i).id) )
+                continue;
+            end
+            
+            if ( isempty(RegionInfo(i).chPts{c}) )
+                continue;
+            end
+            
+            plot(RegionInfo(i).chPts{c}(:,1),RegionInfo(i).chPts{c}(:,2), '.',...
+                'Color',pxCmap(c,:),...
+                'Parent',curAx,...
+                'ButtonDownFcn',@(src,evt)(set_region_select(i)));
+        end
+    end
+    
+    drawnow();
+end
+
+function render_image(hFig, imRender)
+    global UIInfo
+    
+    %% Compute padding info
+    imSize = size(imRender);
+    imPad = pad_image(imRender, UIInfo.pad);
+    
+    imLims = [1-UIInfo.pad(2) imSize(2)+UIInfo.pad(2);
+              1-UIInfo.pad(1) imSize(1)+UIInfo.pad(1)];
+
+    %% Setup current image axis and get limits
+    xl = [];
+    yl = [];
+    
+    curAx = get(hFig, 'CurrentAxes');
+    if ( isempty(curAx) )
+        curAx = axes('Parent', hFig);
+        set(hFig, 'CurrentAxes',curAx);
+    else
+        %% Get current axis limit sets
+        xl = xlim(curAx);
+        yl = ylim(curAx);
+        
+        im_xy = (imLims(:,2)-imLims(:,1));
+        prevIm_xy = im_xy - 2*[UIInfo.padDelta(2);UIInfo.padDelta(1)];
+        
+        view_xy = [xl(2)-xl(1); yl(2)-yl(1)];
+        
+        ssrd_xy = 0.5 * view_xy .* (im_xy - prevIm_xy) ./ prevIm_xy;
+        
+        xl = xl + [-ssrd_xy(1), ssrd_xy(1)];
+        yl = yl + [-ssrd_xy(2), ssrd_xy(2)];
+    end
+    
+    % Reset pad delta
+    UIInfo.padDelta = zeros(1,2);
+    
+    %% render image
+    hold(curAx, 'off');
+    hIm = imagesc(imLims(1,:),imLims(2,:),imPad, 'Parent',curAx, [0 1]);
+    set(hIm, 'ButtonDownFcn', @click_down_event);
+    
+    %% Axis positions
+    set(curAx, 'Position',[0 0 1 1]);
+    
+    %% Setup axis defaults
+    axis(curAx, 'off');
+    axis(curAx, 'fill');
+    axis(curAx, 'ij');
+    zoom(hFig, 'reset');
+    
+    %% Reset limits
+    if ( ~isempty(xl) )
+        xlim(curAx, xl);
+        ylim(curAx, yl);
+    end
+    
+    %% Hold for further drawing
+    hold(curAx, 'all');
+end
+
+
+%% ---- Handle aspect-ratio padding
+function imPad = pad_image(im, pad)
+    imSize = size(im);
+    imPad = zeros(imSize + 2*[pad, 0]);
+    
+    imX = pad(2) + (1:imSize(2));
+    imY = pad(1) + (1:imSize(1));
+    
+    imPad(imY,imX,:) = im;
+end
+
+function [pad,finalAR] = compute_pad(hFig, im)
+    imSize = size(im);
+    pos = get(hFig, 'Position');
+    
+    fAR = pos(3) / pos(4);
+    imAR = imSize(2) / imSize(1);
+    
+    pad = zeros(1,2);
+    if ( imAR <= fAR )
+        pad(2) = ceil((fAR * imSize(1) - imSize(2)) / 2);
+    else
+        pad(1) = ceil((imSize(2) / fAR  - imSize(1)) / 2);
+    end
+    
+    finalAR = (imSize(2) + 2*pad(2)) / (imSize(1) + 2*pad(1));
+end
+
+%% ---- Image rendering functions
+function imRender = im_composite_channels(imC, cmap,tfn)
+    imSize = size_dims(imC, 1:4);
+    
+    %% Create normalized images
+    imNrm = zeros([imSize(1:2), imSize(4)]);
+    for c=1:imSize(4)
+        imNrm(:,:,c) = mat2gray(imC(:,:,1,c));
+    end
+    
+    %% Transform channels
+    imT = im_apply_tfn(imNrm, tfn);
+    
+    % Use a default alpha and normalize at the end
+    % NOTE: Alpha = 0.1 makes all channels pretty transparent
+    alpha = 0.1;
+    %% Composite colors
+    imRender = zeros([imSize(1:2), 3]);
+    imTrn = ones([imSize(1:2), 1]);
+    for c=imSize(4):-1:1
+        oppacity = alpha * imT(:,:,c);
+        
+        imPremul = oppacity .* repmat(reshape(cmap(c,:),[1,1,3]), [imSize(1:2), 1]);
+        imRender = (1-oppacity).*imRender + imPremul;
+        imTrn = imTrn .* (1-oppacity);
+    end
+    
+    imRender = clamp(imRender ./ alpha, 0.0,1.0);
+end
+
+function imT = im_apply_tfn(im,tfn)
+    alpha = tfn(:,1).';
+    beta = tfn(:,2).';
+    % gamma = tfn(:,3).';
+    
+    imR = reshape(im, [size(im,1)*size(im,2), size(im,3)]);
+    imTl = clamp(alpha .* (clamp(imR + beta,0.0,1.0) - 0.5) + 0.5, 0.0, 1.0);
+%     imTl = clamp(clamp(alpha .* ((imR.^gamma) - 0.5),-0.5,0.5) + 0.5 + beta, 0.0, 1.0);
+    
+    imT = reshape(imTl, size(im));
+end
+
+function outV = clamp(inV, minV, maxV)
+    outV = min(max(inV, minV), maxV);
+end
+
+
+%% ---- UI Init Functions ----
+function ui_initialize_main()
+    global UIInfo
+    
+    set(UIInfo.mainFig,...
+        'SizeChangedFcn',       @resize_event,...
+        'WindowScrollWheelFcn', @scroll_event,...
+        'WindowButtonMotionFcn',@move_event,...
+        'KeyPressFcn',          @key_event,...
+        'CloseRequestFcn',      @close_event,...
+        'Menu',                 'none',...
+        'ToolBar',              'none',...
+        'BusyAction',           'cancel',...
+        'Interruptible',        'off',...
+        'NumberTitle',          'off',...
+        'Name',                 'Sea Urchin Calcein Counter');
+    
+    ui_create_menubar(UIInfo.mainFig);
+end
+
+function ui_create_menubar(hFig)
+    global UIInfo
+    
+    fileMenu = uimenu(...
+        'Parent',           hFig,...
+        'Label',            'File',...
+        'HandleVisibility', 'callback');
+    
+    toolsMenu = uimenu(...
+        'Parent',           hFig,...
+        'Label',            'Tools',...
+        'HandleVisibility', 'callback');
+    
+    helpMenu = uimenu(...
+        'Parent',           hFig,...
+        'Label',            'Help',...
+        'HandleVisibility', 'callback',...
+        'Callback',         @(src,evt)(helpdlg()));
+    
+    uimenu(...
+        'Parent',           fileMenu,...
+        'Label',            'Open Image',...
+        'HandleVisibility', 'callback', ...
+        'Callback',         @open_image_event,...
+        'Accelerator',      'o');
+    
+    uimenu(...
+        'Parent',           fileMenu,...
+        'Label',            'Save Data',...
+        'HandleVisibility', 'callback', ...
+        'Callback',         @save_data_event,...
+        'Accelerator',      's',...
+        'Separator',        'on');
+    
+    uimenu(...
+        'Parent',           fileMenu,...
+        'Label',            'Load Data',...
+        'HandleVisibility', 'callback',...
+        'Callback',         @open_data_event,...
+        'Accelerator',      'l');
+    
+    showTools = uimenu(...
+        'Parent',           toolsMenu,...
+        'Label',            'Show Tools',...
+        'HandleVisibility', 'callback',...
+        'Callback',         @(src,evt)(ui_toggle_tools()),...
+        'Accelerator',      't',...
+        'Enable',          'off',...
+        'Checked',         'off');
+    
+    UIInfo.handles.showTools = showTools;
+end
+
+function ui_update_title()
+    global CONSTANTS UIInfo
+    
+    datasetName = CONSTANTS.imageData.DatasetName;
+    
+    title = datasetName;
+    if ( is_edited() )
+        title = [title ' *'];
+    end
+    
+    set(UIInfo.mainFig, 'NumberTitle','off');
+    set(UIInfo.mainFig, 'Name',title);
+end
+
+function ui_create_tooldlg()
+    global UIInfo
+    
+    if ( ~check_valid_handle(UIInfo.toolFig) )
+        UIInfo.toolFig = toolsdlg();
+        UIInfo.updateMenu = @ui_update_toolsmenu;
+        UIInfo.updateFcn = @ui_update_fcn;
+        UIInfo.updateThresh = @ui_update_thresh_fcn;
+        UIInfo.updateRegionSelect = @set_region_select;
+        % UIInfo.updateCellCount = @ui_update_cellcount;
+    end
+    
+    ui_setup_toolhandles();
+    ui_set_channel_names();
+    
+    ui_update_region_table();
+    ui_init_chthresh();
+    
+    ui_update_from_info();
+    ui_update_toolsmenu();
+end
+
+function ui_toggle_tools()
+    global UIInfo
+    
+    if ( check_valid_handle(UIInfo.toolFig) )
+        visible = get(UIInfo.toolFig, 'Visible');
+        if ( strcmpi(visible,'on') )
+            set(UIInfo.toolFig, 'Visible','off');
+        else
+            set(UIInfo.toolFig, 'Visible','on');
+        end
+    else
+        ui_create_tooldlg();
+    end
+    
+    ui_update_toolsmenu();
+end
+
+function ui_update_toolsmenu()
+    global UIInfo CONSTANTS
+    
+    strEn = 'off';
+    if ( ~isempty(CONSTANTS) )
+        strEn = 'on';
+    end
+    
+    if ( check_valid_handle(UIInfo.toolFig) )
+        strCh = get(UIInfo.toolFig, 'Visible');
+    end
+
+    set(UIInfo.handles.showTools, 'Checked',strCh);
+    set(UIInfo.handles.showTools, 'Enable',strEn);
+end
+
+function bValid = check_valid_handle(chkHandle)
+    bValid = false;
+    if ( isempty(chkHandle) )
+        return;
+    end
+    
+    if ( ~isvalid(chkHandle) )
+        return;
+    end
+    
+    bValid = true;
+end
+
+
+%% ---- Tools UI Helpers ----
+function ui_update_fcn()
+    ui_update_tool_values();
+    draw_image();
+end
+
+function ui_update_tool_values()
+    ui_load_colors();
+    ui_load_display_settings();
+    ui_compute_transferfcn();
+end
+
+function ui_update_thresh_fcn()
+    ui_update_chthresh();
+    
+    edit_op(@update_region_chbw);
+    ui_update_region_table();
+    draw_image();
+end
+
+function ui_setup_toolhandles()
+    global CONSTANTS UIInfo
+    
+    UIInfo.tools = [];
+    UIInfo.tools.hShowCh = findobj(UIInfo.toolFig, 'Tag','show_regions');
+    
+    minChan = min(2,CONSTANTS.imageData.NumberOfChannels);
+    
+    UIInfo.tools.hChColor = zeros(1,minChan);
+    UIInfo.tools.hChBright = zeros(1,minChan);
+    UIInfo.tools.hChContrast = zeros(1,minChan);
+    
+    for c=1:minChan
+        UIInfo.tools.hChColor(c) = findobj(UIInfo.toolFig, 'Tag',['color_ch' num2str(c)]);
+        UIInfo.tools.hChBright(c) = findobj(UIInfo.toolFig, 'Tag',['bright_ch' num2str(c)]);
+        UIInfo.tools.hChContrast(c) = findobj(UIInfo.toolFig, 'Tag',['contrast_ch' num2str(c)]);
+
+        UIInfo.tools.hShowChPx(c) = findobj(UIInfo.toolFig, 'Tag',['show_ch' num2str(c) 'px']);
+        UIInfo.tools.hChThresh(c) = findobj(UIInfo.toolFig, 'Tag', ['ch' num2str(c) '_thresh']);
+    end
+    
+    UIInfo.tools.hRegionTable = findobj(UIInfo.toolFig, 'Tag','cells_table');
+end
+
+function ui_set_channel_names()
+    global CONSTANTS UIInfo
+    
+    for c=1:2
+        hChPanel = findobj(UIInfo.toolFig, 'Tag',['uipanel_ch' num2str(c)]);
+        if ( ~isempty(hChPanel) && ~isempty(CONSTANTS.imageData.ChannelNames{c}) )
+            set(hChPanel, 'Title',CONSTANTS.imageData.ChannelNames{c});
+        end
+    end
+end
+
+function ui_init_chthresh()
+    ui_update_chthresh();
+    update_bw_ch();
+end
+
+function ui_set_channel_colors()
+    global UIInfo Config CONSTANTS
+    
+    if ( isfield(CONSTANTS.imageData,'ChannelColors') && ~isempty(CONSTANTS.imageData.ChannelColors) )
+        UIInfo.Cmap = CONSTANTS.imageData.ChannelColors;
+    else
+        UIInfo.Cmap = Config.channelColors;
+    end
+end
+
+function ui_reset_display_settings()
+    global UIInfo
+    
+    UIInfo.bShowRegions = true;
+    UIInfo.bShowChPx = [true, true];
+    
+    UIInfo.ThrScale = [1.0, 1.0];
+    
+    UIInfo.Tfn(1,:) = [1,0,1];
+    UIInfo.Tfn(2,:) = [1,0,1];
+    
+    ui_set_channel_colors();
+end
+
+function ui_update_from_info()
+    global UIInfo
+    
+    %% Setup display settings
+    set(UIInfo.tools.hShowCh, 'Value',UIInfo.bShowRegions);
+    for c=1:length(UIInfo.tools.hShowChPx)
+        set(UIInfo.tools.hShowChPx(c), 'Value',UIInfo.bShowChPx(c));
+    end
+    
+    %% Setup colors
+    for c=1:size(UIInfo.Cmap,1)
+        set(UIInfo.tools.hChColor(c), 'BackgroundColor',UIInfo.Cmap(c,:));
+    end
+    
+    %% Setup transfer functions
+    for c=1:size(UIInfo.Tfn,1)
+        alpha = UIInfo.Tfn(c,1);
+        beta = UIInfo.Tfn(c,2);
+        
+        vBr = beta;
+        vCon = 1-2.^(-alpha);
+        
+        set(UIInfo.tools.hChBright(c), 'Value',vBr);
+        set(UIInfo.tools.hChContrast(c), 'Value',vCon);
+    end
+    
+    %% Setup threshold
+    for c=1:length(UIInfo.tools.hChThresh)
+        tSc = UIInfo.ThrScale(c);
+        vCt = 1-2.^(-tSc);
+        set(UIInfo.tools.hChThresh(c), 'Value',vCt);
+    end
+end
+
+function ui_load_display_settings()
+    global UIInfo
+    
+    UIInfo.bShowRegions = (get(UIInfo.tools.hShowCh, 'Value') > 0.0);
+    for c=1:length(UIInfo.tools.hShowChPx)
+        UIInfo.bShowChPx(c) = (get(UIInfo.tools.hShowChPx(c), 'Value') > 0.0);
+    end
+end
+
+function ui_load_colors()
+    global UIInfo Config
+    cmap = [1,0,0; 0,1,0];
+    
+    for c=1:length(UIInfo.tools.hChColor)
+        cmap(c,:) = get(UIInfo.tools.hChColor(c), 'BackgroundColor');
+    end
+    
+    UIInfo.Cmap = cmap;
+    Config.channelColors = cmap;
+end
+
+function ui_compute_transferfcn()
+    global UIInfo
+    gamma = 1.2;
+    tfn = [1,0,gamma;1,0,gamma];
+    
+    for c=1:length(UIInfo.tools.hChBright)
+        vBr = get(UIInfo.tools.hChBright(c), 'Value');
+        vCon = get(UIInfo.tools.hChContrast(c), 'Value');
+        
+        alpha = -log2(1-vCon);
+        beta = vBr;
+        
+        tfn(c,:) = [alpha,beta,1];
+    end
+    
+    UIInfo.Tfn = tfn;
+end
+
+function ui_update_chthresh()
+    global UIInfo
+    
+    thrScale = zeros(1,2);
+    for c=1:length(UIInfo.tools.hChThresh)
+        tVal = get(UIInfo.tools.hChThresh(c), 'Value');
+        thrScale(c) = -log2(1-tVal);
+    end
+
+    UIInfo.ThrScale = thrScale;
+end
+
+
+%% ---- Partial region (drawing) handling ----
+function update_cell_edge(point_xy)
+    global PartialCell
+    
+    updateLine = PartialCell.lines(end);
+    set(updateLine, 'XData', [PartialCell.pts(end,1) point_xy(1)]);
+    set(updateLine, 'YData', [PartialCell.pts(end,2) point_xy(2)]);
+end
+
+function start_cell(curAx, point_xy)
+    global PartialCell
+    
+    PartialCell = [];
+    PartialCell.pts = point_xy;
+    
+    newLine = line([PartialCell.pts(end,1) point_xy(1)], [PartialCell.pts(end,2) point_xy(2)], 'Parent',curAx, 'color','w');
+    PartialCell.lines = newLine;
+end
+
+function chk_next_edge(curAx, point_xy)
+    global PartialCell
+    
+    if ( isempty(PartialCell) )
+        return;
+    end
+    
+    if ( sum((PartialCell.pts(end,:) - point_xy).^2,1) < 0.6 )
+        return;
+    end
+    
+    next_cell_edge(curAx, point_xy);
+end
+
+function next_cell_edge(curAx, point_xy)
+    global PartialCell
+    
+    if ( isempty(PartialCell) )
+        return;
+    end
+    
+    PartialCell.pts = [PartialCell.pts; point_xy];
+    
+    newLine = line([PartialCell.pts(end,1) point_xy(1)], [PartialCell.pts(end,2) point_xy(2)], 'Parent',curAx, 'color','w');
+    PartialCell.lines = [PartialCell.lines; newLine];
+end
+
+function delete_partial_cell()
+    global PartialCell
+    
+    if ( ~isempty(PartialCell) )
+        for i=1:length(PartialCell.lines)
+            delete(PartialCell.lines(i));
+        end
+    end
+    
+    PartialCell = [];
+end
+
+
+%% ---- Region handling ----
+function ui_update_region_table()
+    global RegionInfo UIInfo
+    
+    data = {};
+    if ( isempty(RegionInfo) )
+        set(UIInfo.tools.hRegionTable, 'Data',data);
+    else
+        bValidRegions = arrayfun(@(x)(~isempty(x.id)), RegionInfo);
+        validRegions = RegionInfo(bValidRegions);
+
+        % Channel counts 1x3 array: [ch1Count,ch2Count,overlapCount]
+        % NOTE: The channel 1/2 counts are only the disjoint non-overlapping pixels so we add 3 to both
+        chCounts = arrayfun(@(x)([x.chCount(1) x.chCount(2) 0] + x.chCount(3)), validRegions, 'UniformOutput',false);
+        chCounts = vertcat(chCounts{:});
+
+        data = cell(length(validRegions),5);
+        [data{:,1}] = deal(validRegions.id);
+        [data{:,2}] = deal(validRegions.area);
+        data(:,3) = num2cell(chCounts(:,1));
+        data(:,4) = num2cell(chCounts(:,2));
+        data(:,5) = num2cell(chCounts(:,3));
+
+        set(UIInfo.tools.hRegionTable, 'Data',data);
+    end
+end
+
+function update_region_chpoints(regionIDs)
+    global RegionInfo bwCh
+
+    %% Get above-threshold points for each region (handle overlap as well)
+    pts = [];
+    chansz = zeros(1,3);
+    bOverlap = all(bwCh,3);
+    % These are disjoint (non-overlapping points)
+    for ch=1:size(bwCh,3)
+        [r,c] = find(bwCh(:,:,ch) & ~bOverlap);
+
+        pts = [pts; c,r];
+        chansz(ch) = size(r,1);
+    end
+    % Handle overlap points
+    [ro,co] = find(bOverlap);
+    pts = [pts; co,ro];
+    chansz(3) = size(ro,1);
+    % Starts for channel/overlap points
+    startidx = [0,cumsum(chansz)];
+    % Make a bool map for point sets
+    bChPx = false(sum(chansz),3);
+    for i=1:3
+        bChPx(startidx(i)+(1:chansz(i)),i) = true;
+    end
+
+    %% Compute counts and channel points for all valid regions in region list
+    for i=1:length(regionIDs)
+        idx = regionIDs(i);
+        if ( isempty(RegionInfo(idx).id) )
+            continue;
+        end
+
+        bInR = inpolygon(pts(:,1),pts(:,2), RegionInfo(idx).pts(:,1),RegionInfo(idx).pts(:,2));
+        RegionInfo(idx).chCount = zeros(1,3);
+        for ch=1:3
+            bch = bInR & bChPx(:,ch);
+            chPts = pts(bch,:);
+
+            RegionInfo(idx).chPts{ch} = chPts;
+            RegionInfo(idx).chCount(ch) = size(chPts,1);
+        end
+    end
+end
+
+function update_region_chbw()
+    global RegionInfo
+    
+    update_bw_ch();
+    update_region_chpoints(1:length(RegionInfo));
+end
+
+function update_bw_ch()
+    global UIInfo imC bwCh
+    
+    bwCh = false(size_dims(imC,[1,2,4]));
+    for c=1:size(imC,4)
+        imN = mat2gray(imC(:,:,1,c));
+        mt = multithresh(imN(:),2);
+        thresh = UIInfo.ThrScale(c) * mt(2);
+        
+        bwCh(:,:,c) = (imN >= thresh);
+    end
+end
+
+function create_region(partialRegion)
+    global RegionInfo
+    
+    if ( isempty(partialRegion) )
+        return;
+    end
+    
+    newID = 1;
+    if ( ~isempty(RegionInfo) )
+        newID = size(RegionInfo,1) + 1;
+    end
+    
+    newRegion = make_empty_cell();
+    newRegion.id = newID;
+    newRegion.pts = [partialRegion.pts; partialRegion.pts(1,:)];
+    newRegion.area = polyarea(newRegion.pts(:,1),newRegion.pts(:,2));
+    
+    RegionInfo = [RegionInfo; newRegion];
+    update_region_chpoints(newID);
+    
+    ui_update_region_table();
+end
+
+function delete_region(cellID)
+    global RegionInfo
+    
+    RegionInfo(cellID) = make_empty_cell();
+end
+
+function cellStruct = make_empty_cell()
+    cellStruct = struct('id',{[]}, 'pts',{[]}, 'area',{[]}, 'chPts',{[]},'chCount',{[]}, 'count',{[]});
+end
+
+function regionID = check_region_select(point_xy)
+    global RegionInfo
+    
+    bValidRegions = arrayfun(@(x)(~isempty(x.id)), RegionInfo);
+    validRegions = RegionInfo(bValidRegions);
+    
+    regionID = -1;
+    for i=1:length(validRegions)
+        [bIn,bOn] = inpolygon(point_xy(1),point_xy(2), validRegions(i).pts(:,1),validRegions(i).pts(:,2));
+        if ( any(bIn | bOn) )
+            regionID = validRegions(i).id;
+            return
+        end
+    end
+end
+
+%% ---- Editor support ----
+function edit_op(editfcn, varargin)
+    editfcn(varargin{:});
+    set_edited(true);
+end
+
+function bEdited = is_edited()
+    global UIInfo
+    bEdited = UIInfo.edited;
+end
+
+function set_edited(bEdited)
+    global UIInfo
+
+    UIInfo.edited = bEdited;
+    ui_update_title();
+end
+
+
+%% ---- Menu Events ----
+function open_image_event(hSrc,evt)
+    global CONSTANTS Config imC
+    
+    chk_const = [];
+    if ( ~check_continue_unsaved() )
+        return;
+    end
+    
+    imPath = get_image_path(Config.imagePath);
+    if ( isempty(imPath) )
+        return;
+    end
+    
+    [imC,chk_const.imageData] = load_image(imPath);
+    if ( isempty(imC) )
+        msgbox('Unable to read image data from file', 'Error reading image', 'warn');
+        return;
+    end
+
+    chk_const.imagePath = imPath;
+    chk_const.dataPath = '';
+
+    CONSTANTS = chk_const;
+    
+    set_imagepath(imPath);
+    set_datapath(imPath);
+    
+    clear_data();
+    reset_ui_fields();
+    
+    ui_update_title();
+    ui_reset_display_settings();
+    ui_create_tooldlg();
+    
+    resize_padding();
+    draw_image();
+end
+
+function open_data_event(hSrc,evt)
+    global CONSTANTS Config
+    
+    if ( ~check_continue_unsaved() )
+        return;
+    end
+    
+    [filename,pathname] = uigetfile(fullfile(Config.dataPath,'*.mat'));
+    if ( filename == 0 )
+        return;
+    end
+    
+    datapath = fullfile(pathname,filename);
+    
+    open_data(datapath)
+    CONSTANTS.dataPath = datapath;
+    
+    ui_create_tooldlg();
+    draw_image();
+end
+
+function save_data_event(hSrc,evt)
+    global CONSTANTS Config
+    
+    if ( isempty(CONSTANTS) )
+        return;
+    end
+    
+    if ( isempty(CONSTANTS.dataPath) )
+        [filename,pathname] = uiputfile(fullfile(Config.dataPath,'*.mat'));
+        if ( filename == 0 )
+            return;
+        end
+        
+        CONSTANTS.dataPath = fullfile(pathname,filename);
+    end
+    
+    save_data(CONSTANTS.dataPath);
+end
+
+
+function bContinue = check_continue_unsaved()
+    bContinue = true;
+    if ( is_edited() )
+        bContinue = unsaved_verify();
+    end
+end
+
+function bContinue = unsaved_verify()
+    userSel = questdlg('You have unsaved data, are you sure you wish to continue without saving?','Unsaved Data','Continue','Cancel','Cancel');
+    bContinue = ~strcmpi(userSel,'Cancel');
+end
+
+%% ---- Data handling ----
+function clear_data()
+    global RegionInfo
+    
+    RegionInfo = [];
+end
+
+function open_data(datapath)
+    global CONSTANTS RegionInfo UIInfo imC
+    
+    s = load(datapath);
+    CONSTANTS = s.CONSTANTS;
+    RegionInfo = s.RegionInfo;
+    
+    imC = s.CONSTANTS.im;
+    
+    %% Update from ui after load
+    fields = fieldnames(s.displayInfo);
+    for i=1:length(fields)
+        UIInfo.(fields{i}) = s.displayInfo.(fields{i});
+    end
+    
+    set_edited(false);
+    set_datapath(datapath);
+end
+
+function save_data(datapath)
+    global CONSTANTS RegionInfo UIInfo imC
+    
+    CONSTANTS.im = imC;
+    displayInfo = struct('Tfn',{UIInfo.Tfn}, 'Cmap',{UIInfo.Cmap},...
+                         'ThrScale',{UIInfo.ThrScale},...
+                         'bShowRegions',{UIInfo.bShowRegions},...
+                         'bShowChPx',{UIInfo.bShowChPx});
+    
+    save(datapath, 'CONSTANTS','RegionInfo','displayInfo');
+    
+    set_edited(false);
+    set_datapath(datapath);
+end
+
+
+%% ---- UI Events ----
+function click_down_event(hSrc,evt)
+    global UIInfo PartialCell
+    curAx = get(UIInfo.mainFig, 'CurrentAxes');
+    currentPoint = get(curAx, 'CurrentPoint');
+    point_xy = currentPoint(1,1:2);
+    
+    selectType = get(UIInfo.mainFig, 'SelectionType');
+    cellID = check_region_select(point_xy);
+    
+    set_region_select(cellID);
+    
+    if ( isempty(PartialCell) && strcmpi(selectType,'normal') && (cellID < 0) )
+        start_cell(curAx, point_xy);
+        set(UIInfo.mainFig, 'WindowButtonUpFcn',@click_up_event);
+    elseif ( strcmpi(selectType,'alt') )
+        UIInfo.panning = true;
+        set(UIInfo.mainFig, 'WindowButtonUpFcn',@click_up_event);
+    end
+end
+
+function click_up_event(hSrc,evt)
+    global UIInfo PartialCell
+    
+    UIInfo.panning = false;
+    set(UIInfo.mainFig, 'WindowButtonUpFcn','');
+    
+    if ( ~isempty(PartialCell) )
+        %% Don't create a cell if it's only a local click
+        if ( size(PartialCell.pts,1) > 2 )
+            edit_op(@create_region, PartialCell);
+        end
+        
+        delete_partial_cell();
+        draw_image();
+    end
+end
+
+function set_region_select(regionID)
+    global UIInfo
+    
+    if ( UIInfo.selectedRegion ~= regionID )
+        UIInfo.selectedRegion = regionID;
+        draw_image();
+    end
+end
+
+function move_event(hSrc,evt)
+    global UIInfo PartialCell
+    
+    curAx = get(hSrc, 'CurrentAxes');
+    if ( UIInfo.panning )
+        stable_mouse_xlat(curAx);
+    end
+    
+    if ( ~isempty(curAx) )
+        curPt = get(curAx, 'CurrentPoint');
+        UIInfo.mousePt = curPt(1,1:2);
+    end
+    
+    if ( ~isempty(PartialCell) )
+        update_cell_edge(UIInfo.mousePt);
+        chk_next_edge(curAx,UIInfo.mousePt);
+    end
+end
+
+function key_event(hSrc,evt)
+    global UIInfo
+
+    if ( strcmpi(evt.Key,'escape') )
+        delete_partial_cell();
+        set(UIInfo.mainFig, 'WindowButtonUpFcn','');
+    elseif ( strcmpi(evt.Key,'backspace') || strcmpi(evt.Key,'delete') )
+        if ( UIInfo.selectedRegion > 0 )
+            edit_op(@delete_region, UIInfo.selectedRegion)
+            
+            UIInfo.selectedRegion = -1;
+            ui_update_region_table();
+            draw_image();
+        end
+    end
+end
+
+function scroll_event(hSrc,evt)
+    global UIInfo
+    
+    zBase = 1.4;
+    
+    curAx = get(UIInfo.mainFig, 'CurrentAxes');
+    if ( isempty(curAx) )
+        return;
+    end
+    
+    currentPoint = get(curAx, 'CurrentPoint');
+    point_xy = currentPoint(1,1:2);
+    
+    wheelDelta = -evt.VerticalScrollCount;
+    newZoom = clamp(wheelDelta + UIInfo.zoom, 0, 10);
+    zoomDelta = newZoom - UIInfo.zoom;
+    
+    if ( abs(zoomDelta) > 0 )
+        xl = xlim(curAx);
+        yl = ylim(curAx);
+        
+        UIInfo.zoom = UIInfo.zoom + zoomDelta;
+        zoomFactor = zBase ^ zoomDelta;
+        
+        lims_xy = [xl.', yl.'];
+        zoomlims_xy = compute_smooth_zoom(zoomFactor, lims_xy, point_xy);
+        
+        xlim(curAx, zoomlims_xy(:,1));
+        ylim(curAx, zoomlims_xy(:,2));
+    elseif ( wheelDelta < 0 )
+        UIInfo.zoom = 0;
+        zoom(curAx, 'out')
+    end
+end
+
+function zm_lims_xy = compute_smooth_zoom(zoomFactor, lims_xy, point_xy)
+    %% Compute zoomed xy-limits such that point_xy is fixed (doesn't move)
+    % NOTE: lims_xy = [xmin ymin
+    %                  xmax ymax]
+    
+    loc_xy = point_xy - lims_xy(1,:);
+    vsz_xy = lims_xy(2,:) - lims_xy(1,:);
+
+    delta_xy = loc_xy - (loc_xy / zoomFactor);
+    zoomsz_xy = vsz_xy / zoomFactor;
+    
+    zm_lims_xy = [lims_xy(1,:); lims_xy(1,:) + zoomsz_xy] + delta_xy;
+end
+
+function stable_mouse_xlat(curAx)
+    global UIInfo
+    
+    xl = xlim(curAx);
+    yl = ylim(curAx);
+    
+    currentPoint = get(curAx, 'CurrentPoint');
+    point_xy = currentPoint(1,1:2);
+    
+    delta_xy = UIInfo.mousePt - point_xy;
+    xl = xl + delta_xy(1);
+    yl = yl + delta_xy(2);
+    
+    xlim(curAx, xl);
+    ylim(curAx, yl);
+end
+
+function resize_padding()
+    global UIInfo imC
+    
+    if ( isempty(imC) )
+        return;
+    end
+    
+    padding = compute_pad(UIInfo.mainFig, imC);
+    
+    UIInfo.padDelta = padding - UIInfo.pad;
+    UIInfo.pad = padding;
+end
+
+function resize_event(hFig,evt)
+    resize_padding();
+    draw_image();
+end
+
+function close_event(hFig,evt)
+    global UIInfo
+
+    if ( ~check_continue_unsaved() )
+        return;
+    end
+    
+    save_config();
+    
+    if ( check_valid_handle(UIInfo.toolFig) )
+        delete(UIInfo.toolFig);
+    end
+    
+    delete(hFig);
+end
diff --git a/urchin_compare/version.json b/urchin_compare/version.json
new file mode 100644
index 0000000000000000000000000000000000000000..ff8f7bf05c33c2d12446661bd1f05a5f61149bd4
--- /dev/null
+++ b/urchin_compare/version.json
@@ -0,0 +1,12 @@
+{
+  "name" : "urchin_compare",
+  "majorVersion" : 2,
+  "minorVersion" : 0,
+  "branchName" : "master",
+  "buildNumber" : "2020.12.07.07",
+  "buildMachine" : "bioimage28",
+  "commitHash" : [
+                   " : 15410c6b7251a6890057e04366431f0286b90d70",
+                   " : 07f725fe7b1c500a2743e016d119a7b0f059561e"
+                 ]
+}