/* * Neuron_Morpho - Plugin for ImageJ to obtain neuronal morphological data * from stacks of images. * Copyright (C) 2001 Giampaolo D'Alessandro * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * * A copy of the license is available at: * http://www.gnu.org/copyleft/gpl.html * * please send comments, bugs, and feature requests to * G.D'Alessandro@maths.soton.ac.uk * or see http://www.maths.soton.ac.uk/staff/D'Alessandro/morpho/ * * file: Neuron_Morpho.java * Neuron_Morpho: Version 1.1.6 25th May 2007 * Change from 1.1.5 * After a line is deleted the nodes are re-numbered so that they are * always in sequential and continguous order. This is done because * cvapp needs the nodes to be consecutive. * * Author: Giampaolo D'Alessandro (G.D'Alessandro@maths.soton.ac.uk) */ import ij.*; import ij.plugin.*; import ij.process.*; import ij.gui.*; import ij.text.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.lang.Math; import java.net.*; import javax.swing.* ; import javax.swing.event.*; import javax.swing.table.*; import java.text.*; import java.util.*; import javax.swing.table.AbstractTableModel; /** * Class to obtain in a relatively painless manner neuron morphometric * data from a stack of images. This class is supposed to work as a * plugin for the NIH sponsored image analysis program ImageJ. * * This plugin creates a JFrame to view the data. * * @version 1.1.5 12th July 2002 * @author Giampaolo D'Alessandro * */ public class Neuron_Morpho implements ActionListener, MouseListener, PlugIn, TableModelListener { /** * Creates a window to display the measurements that the user can * obtain by appropriately clicking the mouse on the stack of * images. It requires a stack to operate. * */ /* ImageJ related classes. */ ImagePlus img; // The stack of images ImageCanvas canvas; // The canvas on which the images are displayed ImageStack stack; // Stack of images to be analysed /* Frame related variables and objects. */ JFrame frame; // Frame where the data will be shown. static JFrame instance;// Only one copy of the plugin can run at // the same time. This variable is used // to make sure that this is the case. JButton start; // Button to start the data capture. JButton stop; // Button to stop the data capture. /* Table related variables. */ JTable dataTable ; // This is the table where all the data will // be displayed. String[] type = {"undefined", "soma", "axon", "dendrite", "apical-dendrite", "fork", "end point", "custom", ""}; JComboBox forkBox; // Box(list) that contains the fork points // that have occurred in the file. int row; // Row of the table that will contain the data // of the next working point. /* Work variables. */ int Xold; // Coordinates of the point where the mouse has int Yold; // been pressed. boolean doMeasurement; // Flag to say the the data point is valid. File morphoFile; // File that contains the morphology data. java.awt.List header; // Header of the file that contains the data. /* The interface requires that there is a run(String) method. * This creates the window with the table and sets up the plugin. */ public void run(String arg) { // If the plugin is already running, i.e. if instance is set, // then just pop the plugin window to the front. if (instance!=null) { instance.toFront(); return; } /* Initialise a few variables. */ Xold = 0; Yold = 0; doMeasurement = false; /* Frame parameters. */ frame = new JFrame(); instance = frame; frame.setSize(600,240) ; frame.setTitle("Neuron_Morpho plugin"); frame.getContentPane().setLayout(new BorderLayout()); /* Button sections. There are two button areas. The one at * the top deals with the input and output of the data and it * allows the user to load or store data to a file and to exit * the plugin. The bottom areas contains two buttons: one * starts the other stops the data capture. */ JPanel northPanel = new JPanel(); northPanel.setLayout(new GridLayout(1,5)); addButton("Open",northPanel); addButton("Save",northPanel); addButton("Del.Row",northPanel); addButton("Help",northPanel); addButton("Quit",northPanel); frame.getContentPane().add(northPanel, BorderLayout.NORTH); JPanel southPanel = new JPanel(); southPanel.setLayout(new GridLayout(1,2)); start = new JButton("Start"); start.addActionListener(this); southPanel.add(start); stop = new JButton("Stop"); stop.addActionListener(this); southPanel.add(stop); // The stop button should be enables only if the plugin is // running. stop.setEnabled(false); frame.getContentPane().add(southPanel, BorderLayout.SOUTH); /* Data table section. */ // Create a model of the data table. NeuronMorphoTableModel dataModel = new NeuronMorphoTableModel(); dataModel.addTableModelListener(this); dataTable = new JTable(dataModel); dataTable.setFont(new Font("SansSerif", Font.PLAIN, 12)); // Special columns (Type and Previous). // The "Type" column is editable and its possible contents are // listed in a (non-editable) combo box. TableColumn typeColumn = dataTable.getColumn("Type"); // Create a combo box to use for the "Type" column) JComboBox typeBox = new JComboBox(); typeBox.setMaximumRowCount(type.length); for (int i = 0; i < type.length; i++) { typeBox.addItem(type[i]); } // Use the combo box as the editor in the "Type" column. typeColumn.setCellEditor(new DefaultCellEditor(typeBox)); typeColumn.setPreferredWidth(150); // Set a tooltip for the "Type" column renderer. DefaultTableCellRenderer typeColumnRenderer = new DefaultTableCellRenderer(); typeColumnRenderer.setToolTipText ("Click for list"); typeColumn.setCellRenderer(typeColumnRenderer); // The "Previous" column is editable and its possible contents // are listed in an editable combo box. This contains the // list of fork points in the dendritic tree. TableColumn previousColumn = dataTable.getColumn("Previous"); // Create a combo box to use for the "Previous" column). // This box must be known also by the other methods because // its contents are updated as the fork points are added. forkBox = new JComboBox(); forkBox.setEditable(true); // Use the combo box as the editor in the "Type" column. previousColumn.setCellEditor(new DefaultCellEditor(forkBox)); // Set a tooltip for the "Previous" column renderer. DefaultTableCellRenderer previousColumnRenderer = new DefaultTableCellRenderer(); previousColumnRenderer.setToolTipText("Click for list" + " or enter your own choice"); previousColumn.setCellRenderer(previousColumnRenderer); // Finally, add the table to the JFrame and make it visible. frame.getContentPane().add( new JScrollPane(dataTable), BorderLayout.CENTER); frame.pack(); frame.setVisible(true); /* Initialise some of the variables. */ row = 0; // Initialise the table row number. // The morphology file is initialised to be the directory // where the images are stored. However, we must also check // that the ImageJ preference file exists. If it does not // exist then the choice for the morphology file directory is // the directory where ImageJ is stored (assumed to be the // current working directory). String defaultMorphoFile = Prefs.getString(Prefs.DIR_IMAGE); if (defaultMorphoFile == null) { defaultMorphoFile = System.getProperty("user.dir") ; } morphoFile = new File(defaultMorphoFile); // Create an empty file header. header = new java.awt.List(); } /****************************************************************** * Actions performed when the buttons are pressed * ******************************************************************/ public void actionPerformed(ActionEvent e) { String label = e.getActionCommand(); if (label==null) return; /********************************* * North panel - File operations. *********************************/ if (label=="Open") { JFileChooser chooser = new JFileChooser(morphoFile); if ( morphoFile.isFile() ) { chooser.setSelectedFile(morphoFile); } int result = chooser.showOpenDialog(frame); if (result == JFileChooser.CANCEL_OPTION) return; morphoFile = chooser.getSelectedFile(); try { FileReader readIn = new FileReader(morphoFile); BufferedReader in = new BufferedReader(readIn); String inLine; header = new java.awt.List(); row = 0; while ((inLine = in.readLine()) != null) { // Check if the line is part of the header // (i.e. if it starts with a # symbol). In this // case add the line to the header list, otherwise // add the data to the table. if ( inLine.charAt(0) == '#' ) { header.add(inLine); } else { StringTokenizer st = new StringTokenizer(inLine); int col = 0; dataTable.setValueAt(st.nextToken(),row,col); col = 1; dataTable.setValueAt(type[new Integer(st.nextToken()).intValue()],row,col); col = 2; while ( st.hasMoreTokens() ) { dataTable.setValueAt(st.nextToken(),row,col); ++col; } ++row; } } readIn.close(); } catch (IOException ioe) {} return; } if (label=="Save") { JFileChooser chooser = new JFileChooser(morphoFile); if ( morphoFile.isFile() ) { chooser.setSelectedFile(morphoFile); } int result = chooser.showSaveDialog(frame); if (result == JFileChooser.CANCEL_OPTION) return; morphoFile = chooser.getSelectedFile(); try { FileWriter writeOut = new FileWriter(morphoFile); PrintWriter printOut = new PrintWriter(writeOut); // First of all write the header (it may be empty). if ( header.getItemCount() > 0 ) { for (int i = 0; i < header.getItemCount(); i++) { printOut.println(header.getItem(i)); } } // Now write the data. for (int i = 0; i < row; i++) { String outLine = dataTable.getValueAt(i,0) + " " + findType((String)dataTable.getValueAt(i,1)) + " "; for (int col = 2; col < dataTable.getColumnCount(); col++) { outLine = outLine + dataTable.getValueAt(i,col) + " "; } printOut.println(outLine); } writeOut.close(); } catch (IOException ioe) {} return; } if (label=="Del.Row") { // Get the index of the selected row. int selRow = dataTable.getSelectedRow(); // Check if any row has been selected. if ( selRow < 0 ) { IJ.beep(); IJ.showStatus("No row has been selected"); return; } // Check that the selected row has actually something in // it. If not, then return without doing anything. if ( selRow > row ) { return; } // Store the label of the point in the selected row. int labelSelRow = Integer.valueOf ((String) dataTable.getValueAt(selRow,0)).intValue(); // If the eliminated row was a fork point then it must // be removed from the list of possible fork points. if (type[5].equals(dataTable.getValueAt(selRow,1))) { forkBox.removeItem(dataTable.getValueAt(selRow,0)); } // Copy up the rows below the selected row. for (int i = selRow; i < row; i++) { for (int col = 0; col < dataTable.getColumnCount(); col++) { dataTable.setValueAt( dataTable.getValueAt(i+1,col),i,col); } } // Clear the last row for (int col = 0; col < dataTable.getColumnCount(); col++) { dataTable.setValueAt("",row-1,col); } // Now some book keeping. // 1) The total number of rows must be decreased by one. row -= 1; // 2) Scan the table for "previous" labels that are equal // to the index of the selected row. They will be // replaced by the index of the previous row. int prevRowLab = 0 ; if ( selRow > 0 ) { prevRowLab = Integer.valueOf ((String)dataTable.getValueAt(selRow-1,0)).intValue(); } for (int i = 0; i < row; i++) { int prevLab = Integer.valueOf ((String)dataTable.getValueAt(i,6)).intValue(); if ( prevLab >= labelSelRow ) { dataTable.setValueAt(Integer.toString(prevLab-1),i,6); } if ( i >= selRow ) { dataTable.setValueAt(Integer.toString(i+1),i,0); } } return; } if (label=="Help") { // Find the current working directory: it is assumed that // ImageJ is in this directory. String urlString = "file:" + System.getProperty("user.dir") + "/plugins/morpho_doc/index.html"; new NeuronMorphoHelpViewer(urlString); return; }; if (label=="Quit") { instance = null; frame.dispose(); return; } /***************************************** * South panel - Measurements operations. *****************************************/ /* We need an image. */ img = WindowManager.getCurrentImage(); if (img==null) { IJ.beep(); IJ.showStatus("No image"); return; } /* The start operation consists in attaching a mouse Listener * to the image. */ if (label=="Start") { IJ.showStatus("Neuron_Morpho plugin started"); ImageWindow win = img.getWindow(); canvas = win.getCanvas(); canvas.addMouseListener(this); img.setColor(Color.red); // The stop button is enabled and the start button disabled. start.setEnabled(false); stop.setEnabled(true); return; } /* The stop operation consists in detaching the mouse Listener * from the image. */ if (label=="Stop") { IJ.showStatus("Neuron_Morpho plugin stopped"); canvas.removeMouseListener(this); // The stop button is disabled and the start button enabled. start.setEnabled(true); stop.setEnabled(false); return; } } /****************************************************************** * Mouse events routines * They deal with data acquisition: the compute the starting and * ending point of the selection and use them to produce a new * data line for the table. ******************************************************************/ /* * If the mouse is pressed while the Shift key is held down then * we are at the starting point of the selection. */ public void mousePressed(MouseEvent e) { if (e.isShiftDown()) { int x = e.getX(); int y = e.getY(); Xold = canvas.offScreenX(x); Yold = canvas.offScreenY(y); img.getProcessor().moveTo(Xold,Yold); // Set the flag to say that this is a valid measurement. doMeasurement = true; } } /* * If the mouse is released while the Shift key is held down then * we are at the end point of the selection. The length of the * selection and its centre of mass are computed and stored * together with other additional data in the table. */ public void mouseReleased(MouseEvent e) { if (e.isShiftDown() && doMeasurement) { // Reset the measurement flag. doMeasurement = false; // Get the coordinates and compute the parameters. int x = e.getX(); int y = e.getY(); int z = img.getCurrentSlice(); int Xnew = canvas.offScreenX(x); int Ynew = canvas.offScreenY(y); img.getProcessor().lineTo(Xnew,Ynew); img.updateAndRepaintWindow(); double Xcm = (Xold + Xnew)/2.; double Ycm = (Yold + Ynew)/2.; double radius = 0.5*Math.sqrt((double) ((Xold - Xnew)*(Xold - Xnew) + (Yold - Ynew)*(Yold - Ynew))); // Determine the label of the selected point. If this is // the first row then the label is set to 1, otherwise it // is set to one plus the value of the label in the // previous row. int point = 1; if (row > 0) { point = 1+ Integer.valueOf ((String)dataTable.getValueAt(row-1,0)).intValue(); } dataTable.setValueAt(Integer.toString(point), row, 0); // Choose the type of neuron section. If this is the // first row then choose "undefined", otherwise chose the // entry of the previous line. The only exceptions are // if the entries where "fork" or "end point" in which // case the type will default to "dendrite". if ( row == 0 ) { dataTable.setValueAt(type[0], row, 1); } else { if (findType((String) dataTable.getValueAt(row-1,1)) < 5) { dataTable.setValueAt(dataTable.getValueAt(row-1,1),row,1); } else { dataTable.setValueAt(type[3], row, 1); } } dataTable.setValueAt(IJ.d2s(Xcm), row, 2); dataTable.setValueAt(IJ.d2s(Ycm), row, 3); dataTable.setValueAt(IJ.d2s(z), row, 4); dataTable.setValueAt(IJ.d2s(radius), row, 5); // The "Previous" columns requires some guessing. If // this is the first row the entry is by default "-1". // Otherwise the suggested value of the entry depends on // whether the point in the previous row is or is not an // "end point". if ( row == 0 ) { dataTable.setValueAt(Integer.toString(-1), row, 6); } else { if (type[6].equals((String)dataTable.getValueAt(row-1,1))) { // The previous point is an end point. The // program looks at the list of available fork // points and finds the most recently created fork // point that has only one branch assigned. This // will be the suggested value of the previous // point. dataTable.setValueAt(suggestedPrevious(), row, 6); } else { // The previous point is not an end point. // Therefore its label is the suggested entry for // the "Previous" column. dataTable.setValueAt( (String) dataTable.getValueAt(row-1,0), row, 6); } } dataTable.repaint(); row += 1; } } public void mouseExited(MouseEvent e) {} public void mouseClicked(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} /****************************************************************** * Table events routines * They deal with changes in the type of neuronal section. ******************************************************************/ public void tableChanged(TableModelEvent e) { // Check which column has been updated. The only column of // interest is the "Type" column. If a "fork" is created or // deleted we must change the list of fork points accordingly. // This procedure always attempts to remove the item from the // list (the removeItem method does not complain if the item // to be removed is not in the list). If the "Type" of // section is "fork" then the item is added to the list. The // advantage of removing the item is that no duplicate lines // ever appear. if (e.getColumn() == 1) { forkBox.removeItem(dataTable.getValueAt(e.getFirstRow(),0)); if (type[5].equals(dataTable.getValueAt(e.getFirstRow(),1))) { // This is a fork point and should be added to the // lists of fork points. forkBox.addItem(dataTable.getValueAt(e.getFirstRow(),0)); } } } /****************************************************************** * Service routines. * ******************************************************************/ /** Adds a button with label to a given panel. */ void addButton(String label, JPanel panel) { JButton b = new JButton(label); b.addActionListener(this); panel.add(b); } /** Tries to match a given string to one of the elements in the * "Type" string array. If it can find a match it returns the * index of the matching entry, otherwise it returns -1. */ public int findType(String arg) { int j = -1; for (int i = 0; i < type.length; i++) { if (arg.equals(type[i])) { j = i ; } } return j; } /** Returns a string that contains a suggested value for the entry * in the "Previous" column. */ public String suggestedPrevious() { int count; // Service variable. // Default value of the suggestion: . String prev = ""; // First of all rebuild the fork list in increasing row order // by rescanning the table. forkBox.removeAllItems(); for (int i=0; i < row; i++) { if (type[5].equals((String) dataTable.getValueAt(i,1))) { forkBox.addItem(dataTable.getValueAt(i,0)); } } // Now go backwards through the list and find the first fork // point that has not two descendants. This is the suggested // entry. for (int i=forkBox.getItemCount()-1; i>=0; i--) { count = 0; for (int j=0; j