updates to Rocca (#161)

* allow Rocca to run without classifiers

Fixed bug that threw an error if no classifier files were specified in Rocca Params dialog

* add rocca switch to enable dev mode

currently only shows/hides extra buttons in the Params dialog, but will
extend to more options in the future

* Fix memory issue with RoccaContourDataBlocks not being released for
garbage collection

Set RoccaContourDataBlock objects to null and stop PamObserver Timer to
force release

* Fix problem tracing whistles in Rocca spectrogram pop-up

Whistle and raw data were being cleared before the user had time to trace out the whistle, causing PAMGuard to throw an exception.  Both were already being cleared when the pop-up window is closed, so no need to do it here.

* updated for training/testing classifiers

* added option to apply whistle classification threshold as the difference between the highest and second highest vote, instead of as an absolute value

* update Rocca help files

update help files to include description of new strong whistle options

* fix typo

Fix typo in ONR Temperate Pacific classifier reference
This commit is contained in:
m2oswald 2024-09-19 15:52:14 +01:00 committed by GitHub
parent 2d207a15c3
commit cee21dbc3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 141 additions and 11 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@ -129,7 +129,7 @@ as classification variables.</p>
<br>
<h3><a name="rocca_ParamsClassifier">Contour/Classifier
tab</a></h3>
<img alt="" src="file:///C:/Users/SCANS/workspace/Pamguard_BWbranch/src/help/detectors/roccaHelp/docs/images/rocca_params_contour-classifier5.png" border="0">&nbsp;<br>
<img alt="" src="images/rocca_params_contour-classifier6.png" border="0">&nbsp;<br>
<ol>
<li>
<p><a name="rocca_ClassifierModel">Whistle
@ -180,6 +180,13 @@ species in the random forest falls below this threshold, the encounter
is considered non-classifiable and is labelled as &#8216;ambiguous&#8217;.</p>
</li>
<li>
<p>The whistle/click threshold can be used in one of two ways: as an absolute value, or as the
minimum difference between the highest vote and the second-highest vote. If used as an absolute, whistles will only be classified
if the percent of trees in the random forest voting for the predicted species is higher than the selected threshold.
If used as a difference, whistles will only be classified if the highest percentage of trees in the random forest voting for the
predicted species is greater than the second-highest percentage by the threshold value.</p>
</li>
<li>
<p>Noise Sensitivity: specify the global noise sensitivity
parameter to use when <a href="rocca_ContourExtractionManipulation.html">extracting a
whistle

View File

@ -341,8 +341,8 @@ public class RoccaClassifyThis {
contourStats.put(RoccaContourStats.ParamIndx.INFLMEANDELTA, Double.parseDouble(dataArray[56]));
contourStats.put(RoccaContourStats.ParamIndx.INFLSTDDEVDELTA, Double.parseDouble(dataArray[57]));
contourStats.put(RoccaContourStats.ParamIndx.INFLMEDIANDELTA, Double.parseDouble(dataArray[58]));
//contourStats.put(RoccaContourStats.ParamIndx.INFLDUR, Double.parseDouble(dataArray[59]));
//contourStats.put(RoccaContourStats.ParamIndx.STEPDUR, Double.parseDouble(dataArray[60]));
contourStats.put(RoccaContourStats.ParamIndx.INFLDUR, Double.parseDouble(dataArray[59]));
contourStats.put(RoccaContourStats.ParamIndx.STEPDUR, Double.parseDouble(dataArray[60]));
// Run the classifier
roccaProcess.roccaClassifier.classifyContour2(rcdb);

View File

@ -306,6 +306,15 @@ public class RoccaParameters implements Serializable, Cloneable, ManagedParamete
*/
int sightingThreshold = 40;
/**
* boolean indicating whether the Strong Whistle threshold represents the difference
* between the highest vote and the second highest vote (=true) or whether it
* is used as an absolute value (=false). The original method was to use as an
* absolute value, but new testing shows that using it as a difference seems to
* work better
*/
private boolean strongWhistleDiff = false;
/** the filename template, modelled after the Ishmael template */
String filenameTemplate = "Encounter%X-%f-Channel%t-%Y%M%D_%H%m%s";
@ -422,6 +431,7 @@ public class RoccaParameters implements Serializable, Cloneable, ManagedParamete
private boolean trimWav = false;
public RoccaParameters() {
}
@ -855,6 +865,17 @@ public class RoccaParameters implements Serializable, Cloneable, ManagedParamete
this.trimWav = trimWav;
}
public boolean isStrongWhistleDiff() {
return strongWhistleDiff;
}
public void setStrongWhistleDiff(boolean strongWhistleDiff) {
this.strongWhistleDiff = strongWhistleDiff;
}
@Override
public PamParameterSet getParameterSet() {
PamParameterSet ps = PamParameterSet.autoGenerate(this, ParameterSetType.DETECTOR);
@ -927,4 +948,5 @@ public class RoccaParameters implements Serializable, Cloneable, ManagedParamete
return ps;
}
}

View File

@ -43,6 +43,7 @@ import java.util.Vector;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.GroupLayout;
import javax.swing.JButton;
@ -137,6 +138,9 @@ public class RoccaParametersDialog extends PamDialog implements ActionListener,
JCheckBox ancCalcs4Clicks; // serialVersionUID=22 2015/06/13 added
JCheckBox ancCalcs4Whistles; // serialVersionUID=22 2015/06/13 added
JCheckBox trimWav;
JRadioButton absStrongThreshold;
JRadioButton diffStrongThreshold;
ButtonGroup strongThreshold;
JLabel outputDirLbl;
JTextField outputDirTxt;
JButton outputDirectoryButton;
@ -557,6 +561,11 @@ public class RoccaParametersDialog extends PamDialog implements ActionListener,
sightingThreshold = new JTextField(3);
sightingThreshold.setMaximumSize(new Dimension(40, sightingThreshold.getHeight()));
JLabel schoolUnits = new JLabel("%");
absStrongThreshold = new JRadioButton("Threshold is absolute value",true);
diffStrongThreshold = new JRadioButton("Threshold is difference between highest and 2nd highest votes");
strongThreshold = new ButtonGroup();
strongThreshold.add(absStrongThreshold);
strongThreshold.add(diffStrongThreshold);
GroupLayout thresholdLayout = new GroupLayout(thresholdSubPanel);
thresholdSubPanel.setLayout(thresholdLayout);
thresholdLayout.setAutoCreateGaps(true);
@ -571,6 +580,10 @@ public class RoccaParametersDialog extends PamDialog implements ActionListener,
.addComponent(schoolLbl)
.addComponent(sightingThreshold)
.addComponent(schoolUnits))
.addGroup(thresholdLayout.createSequentialGroup()
.addComponent(absStrongThreshold))
.addGroup(thresholdLayout.createSequentialGroup()
.addComponent(diffStrongThreshold))
);
thresholdLayout.setVerticalGroup(
thresholdLayout.createSequentialGroup()
@ -582,6 +595,10 @@ public class RoccaParametersDialog extends PamDialog implements ActionListener,
.addComponent(schoolLbl)
.addComponent(sightingThreshold)
.addComponent(schoolUnits))
.addGroup(thresholdLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(absStrongThreshold))
.addGroup(thresholdLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(diffStrongThreshold))
);
thresholdLayout.linkSize(SwingConstants.HORIZONTAL, whistleLbl, schoolLbl);
classifierPanel.add(thresholdSubPanel);
@ -1311,6 +1328,13 @@ public class RoccaParametersDialog extends PamDialog implements ActionListener,
ancCalcs4Clicks.setSelected(roccaParameters.runAncCalcs4Clicks); // serialVersionUID=22 2015/06/13 added
ancCalcs4Whistles.setSelected(roccaParameters.runAncCalcs4Whistles); // serialVersionUID=22 2015/06/13 added
trimWav.setSelected(roccaParameters.isTrimWav());
if (roccaParameters.isStrongWhistleDiff()) {
diffStrongThreshold.setSelected(true);
absStrongThreshold.setSelected(false);
} else {
diffStrongThreshold.setSelected(false);
absStrongThreshold.setSelected(true);
}
classificationThreshold.setText(String.format("%d",
roccaParameters.getClassificationThreshold()));
sightingThreshold.setText(String.format("%d",
@ -1637,6 +1661,12 @@ public class RoccaParametersDialog extends PamDialog implements ActionListener,
roccaParameters.setGpsSource(gpsSourcePanel.getSourceName());
}
if(diffStrongThreshold.isSelected()) {
roccaParameters.setStrongWhistleDiff(true);
} else {
roccaParameters.setStrongWhistleDiff(false);
}
// will throw an exception if the number format of any of the parameters is invalid,
// so catch the exception and return false to prevent exit from the dialog.

View File

@ -501,7 +501,7 @@ public class RoccaProcess extends PamProcess {
// classifiers. Check if the loaded classifier model filename matches one of the classifier
// names created for the project. If so, compare the click to the parameters used to prune
// the datasets and exit if the click falls outside of the thresholds
if (roccaControl.roccaParameters.roccaClassifierModelFilename.getName().equals("TemPacClick.model") &&
if (roccaControl.roccaParameters.roccaClickClassifierModelFilename.getName().equals("TempPacClick.model") &&
(rcdb.getContour().get(RoccaContourStats.ParamIndx.SNR) > 35. ||
rcdb.getContour().get(RoccaContourStats.ParamIndx.DURATION) < 0.005 ||
rcdb.getContour().get(RoccaContourStats.ParamIndx.DURATION) > 0.6 )) {
@ -509,7 +509,7 @@ public class RoccaProcess extends PamProcess {
rcdb = null;
return;
}
if (roccaControl.roccaParameters.roccaClassifierModelFilename.getName().equals("HIClick.model") &&
if (roccaControl.roccaParameters.roccaClickClassifierModelFilename.getName().equals("HIClick.model") &&
(rcdb.getContour().get(RoccaContourStats.ParamIndx.SNR) > 40. ||
rcdb.getContour().get(RoccaContourStats.ParamIndx.DURATION) < 0.01 ||
rcdb.getContour().get(RoccaContourStats.ParamIndx.DURATION) > 0.6 )) {
@ -517,7 +517,7 @@ public class RoccaProcess extends PamProcess {
rcdb = null;
return;
}
if (roccaControl.roccaParameters.roccaClassifierModelFilename.getName().equals("NWAtlClick.model") &&
if (roccaControl.roccaParameters.roccaClickClassifierModelFilename.getName().equals("NWAtlClick.model") &&
(rcdb.getContour().get(RoccaContourStats.ParamIndx.SNR) > 35. ||
rcdb.getContour().get(RoccaContourStats.ParamIndx.DURATION) < 0.005 ||
rcdb.getContour().get(RoccaContourStats.ParamIndx.DURATION) > 0.6 )) {

View File

@ -24,6 +24,8 @@
package rocca;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import weka.classifiers.AbstractClassifier;
import weka.core.DenseInstance;
@ -98,6 +100,9 @@ public class RoccaRFModel implements java.io.Serializable {
double[] theseVotes =
roccaClassifierModel.distributionForInstance(rcdbInst);
double treeConfClassified = theseVotes[(int) speciesNum];
double[] dupVotes = theseVotes.clone();
Arrays.sort(dupVotes);
double bigDiff = dupVotes[dupVotes.length-1]-dupVotes[dupVotes.length-2];
// save the tree votes to rcdb. Step through the species list one at a time and
// compare to the species in the current model. When we find a match, save the
@ -124,9 +129,16 @@ public class RoccaRFModel implements java.io.Serializable {
// if the vote is less than the threshold, set the class to AMBIG and exit
if (treeConfClassified <
boolean threshIsDiff = roccaClassifier.roccaControl.roccaParameters.isStrongWhistleDiff();
if (
(!threshIsDiff && treeConfClassified <
((float) roccaClassifier.roccaControl.roccaParameters.getClassificationThreshold())
/100) {
/100)
||
(threshIsDiff && bigDiff <
((float) roccaClassifier.roccaControl.roccaParameters.getClassificationThreshold())
/100)
){
rcdb.setClassifiedAs(classifiedAs);
// otherwise, check if there is a next stage for the classified species

View File

@ -26,6 +26,7 @@ package rocca;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.util.Date;
import javax.swing.JFileChooser;
@ -34,6 +35,7 @@ import javax.swing.filechooser.FileNameExtensionFilter;
import weka.classifiers.trees.RandomForest;
import weka.core.Instances;
import weka.core.SerializationHelper;
import weka.core.TechnicalInformation;
/**
* Single-stage classifier creation. Dataset must be a WEKA-formatted arff file, with the correct
@ -54,10 +56,24 @@ public class RoccaTrainClassifier {
public static void main(String[] args) {
RoccaTrainClassifier rtc = new RoccaTrainClassifier();
// Get a single file
File arffFile = rtc.getArff();
if (arffFile!=null) {
String modelName = rtc.trainClassifier(arffFile);
}
// Get a folder full of files
// File arffFolder = rtc.getAllArff();
// File[] arffFiles = arffFolder.listFiles(new FilenameFilter() {
// public boolean accept(File dirFiles, String filename) {
// return filename.endsWith(".txt");
// }
// });
// for (File aFile : arffFiles) {
// String modelName = rtc.trainClassifier(aFile);
// }
//
}
@ -91,6 +107,29 @@ public class RoccaTrainClassifier {
}
}
public File getAllArff() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setDialogTitle("Select directory containing training data");
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
fileChooser.setFileHidingEnabled(true);
fileChooser.setApproveButtonText("Select");
//FileNameExtensionFilter restrict = new FileNameExtensionFilter("Only .arff files", "arff");
//FileNameExtensionFilter restrict = new FileNameExtensionFilter("Only .txt files", "txt");
//fileChooser.addChoosableFileFilter(restrict);
File arffFolder;
int state = fileChooser.showOpenDialog(null);
if (state == JFileChooser.APPROVE_OPTION) {
// load the folder
arffFolder = fileChooser.getSelectedFile();
return arffFolder;
} else {
return null;
}
}
/**
* Actual code to train the classifier
@ -125,7 +164,7 @@ public class RoccaTrainClassifier {
System.out.println("Setting Options...");
String[] options = new String[6];
options[0] = "-I"; // number of iterations/trees
options[1] = "10000"; // = 750
options[1] = "750"; // = 750
options[2] = "-K"; // number of attributes (aka mtry)
options[3] = "5"; // = 3
options[4] = "-S"; // seed for random number generator
@ -153,13 +192,33 @@ public class RoccaTrainClassifier {
return null;
}
Enumeration<?> e = model.enumerateMeasures();
System.out.print("Enumeration of classifier:");
while (e.hasMoreElements())
System.out.println("\nValue is: " + e.nextElement());
String globalInfo = model.globalInfo();
System.out.print("\n\nGlobal Info of classifier:\n");
System.out.print(globalInfo);
TechnicalInformation techRef = model.getTechnicalInformation();
System.out.print("\n\nGlobal Info of classifier:\n");
System.out.print(techRef);
String[] modeloptions = model.getOptions();
System.out.print("Options");
System.out.print(modeloptions);
// save the classifier
// String[] curOptions = model.getOptions();
// Enumeration test = model.listOptions();
Instances header = new Instances(trainData,0);
int index = arffFile.getAbsolutePath().lastIndexOf(".");
String modelName = arffFile.getAbsolutePath().substring(0,index) + ".model";
System.out.println("Saving Classifier..." + modelName);
System.out.println("\nSaving Classifier..." + modelName);
try {
SerializationHelper.writeAll
// ("C:\\Users\\Mike\\Documents\\Work\\Java\\WEKA\\weka vs R\\RF_8sp_54att_110whistle-subset.model",