<commit_before>/*
 * Copyright (C) 2021 tobid.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package net.sf.jaer.eventprocessing.filter;

import ch.unizh.ini.jaer.projects.humanpose.AbstractDavisCNNProcessor;
import ch.unizh.ini.jaer.projects.humanpose.DavisClassifierCNNProcessor;
import com.jogamp.opengl.GL2;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.util.awt.TextRenderer;
import java.io.File;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
import net.sf.jaer.Description;
import net.sf.jaer.DevelopmentStatus;
import net.sf.jaer.chip.AEChip;
import net.sf.jaer.event.BasicEvent;
import net.sf.jaer.event.EventPacket;
import net.sf.jaer.eventprocessing.TimeLimiter;
import net.sf.jaer.graphics.ImageDisplay;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.tensorflow.Graph;
import org.tensorflow.Operation;
import org.tensorflow.SavedModelBundle;
import org.tensorflow.Session;
import org.tensorflow.Tensor;
import com.github.sh0nk.matplotlib4j.Plot;
import com.google.common.primitives.Doubles;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.awt.GLCanvas;
import com.jogamp.opengl.glu.GLU;
import com.jogamp.opengl.glu.GLUquadric;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import net.sf.jaer.event.PolarityEvent;
import static net.sf.jaer.eventprocessing.EventFilter.log;
import net.sf.jaer.graphics.ChipCanvas;
import net.sf.jaer.util.EngineeringFormat;
import static net.sf.jaer.util.avioutput.AVIOutputStream.VideoFormat.PNG;
import org.apache.commons.io.FilenameUtils;
import org.tensorflow.Output;
import org.tensorflow.Shape;
import net.sf.jaer.util.RemoteControlCommand;

/**
 * Noise filter that runs a DNN to denoise events
 *
 * @author <NAME> and <NAME>, 2021
 */
@Description("Denoising noise filter that uses a DNN deep neural network to classify events as signal or noise events")
@DevelopmentStatus(DevelopmentStatus.Status.Experimental)
public class MLPNoiseFilter extends AbstractNoiseFilter implements MouseListener, MouseMotionListener, MouseWheelListener {

    private final String KEY_NETWORK_FILENAME = "lastNetworkFilename";
    private String lastManuallyLoadedNetwork = getString("lastManuallyLoadedNetwork", ""); // stores filename and path to last successfully loaded network that user loaded via doLoadNetwork
    private TextRenderer textRenderer = null;
//    private boolean measurePerformance = getBoolean("measurePerformance", true);

    private String performanceString = null; // holds string representation of processing time
    private TimeLimiter timeLimiter = new TimeLimiter(); // private instance used to accumulate events to slices even if packet has timed out
    private int processingTimeLimitMs = getInt("processingTimeLimitMs", 100); // time limit for processing packet in ms to process OF events (events still accumulate). Overrides the system EventPacket timelimiter, which cannot be used here because we still need to accumulate and render the events.
    private String lastPerformanceString = null;

    private int sxm1; // size of chip minus 1
    private int sym1;
    private int ssx; // size of subsampled timestamp map
    private int ssy;

    private SavedModelBundle tfSavedModelBundle = null;
    private Graph tfExecutionGraph = null;
    private Session tfSession = null;
    private int tfBatchSizeEvents = getInt("tfBatchSizeEvents", 1024);
    private int tfNumInBatchSoFar = 0;
    private FloatBuffer tfInputFloatBuffer = null;
    private ArrayList<BasicEvent> eventList = new ArrayList(tfBatchSizeEvents);
    protected float signalClassifierThreshold = getFloat("signalClassifierThreshold", 0.5f);

    // plotting TI patches and stats
    private DescriptiveStatistics stats = null;
    private volatile boolean showThisTiPatch = false;
    // TI patch display
    private ImageDisplay tiPatchDisplay = null;
    private JFrame tiFrame = null;
    private BasicEvent eventToDisplayTIPatchFor = null;
    private EngineeringFormat eng = new EngineeringFormat();

    public enum TIPatchMethod {
	ExponentialDecay, LinearDecay // default is LinearDecay since it works better (and is faster and cheaper)
    };
    private TIPatchMethod tiPatchMethod = TIPatchMethod.valueOf(getString("tiPatchMethod", TIPatchMethod.LinearDecay.toString()));

    private int patchWidthAndHeightPixels = getInt("patchWidthAndHeightPixels", 7);
    private int[][] timestampImage; // timestamp image
    private int[][] lastPolMap; // timestamp image
    private boolean useTI = getBoolean("useTI", true);
    private boolean usePolarity = getBoolean("usePolarity", false);
    private boolean useTIandPol = getBoolean("useTIandPol", false);

    private boolean showOnlySignalTimeimages = getBoolean("showOnlySignalTimeimages", false);
    private boolean showOnlyNoiseTimeimages = getBoolean("showOnlyNoiseTimeimages", false);
    protected boolean saveTiPatchImages = false;
    private int tiFileCounter = 0;
    private File saveDir = null;
    private int inputSF = 1;

    private final int NONONOTONIC_TIMESTAMP_WARNING_INTERVAL = 100000;
    private int nonmonotonicWarningCount = 0;

    /**
     * Cursor size for drawn mouse cursor when filter is selected.
     */
    protected final float CURSOR_SIZE_CHIP_PIXELS = 7;
    protected GLU glu = new GLU();
    protected GLUquadric quad = null;
    private boolean hasBlendChecked = false, hasBlend = false;
    protected boolean showCrossHairCursor = true;
    protected GLCanvas glCanvas;
    protected ChipCanvas chipCanvas;
    float[] cursorColor = null;

    /**
     * Flag that freezes ROI selection
     */
    protected boolean freezeRoi = getBoolean("freezeRoi", false);

    // roiRect stuff
    /**
     * ROI start/end corner index
     */
    protected int roiStartx, roiStarty, roiEndx, roiEndy;
    /**
     * ROI start/end corners and last clicked mouse point
     */
    protected Point roiStartPoint = null, roiEndPoint = null, clickedPoint = null;
    /**
     * ROI rectangle
     */
    protected Rectangle roiRect = (Rectangle) getObject("roiRect", null);

    /**
     * Boolean that indicates ROI is being selected currently
     */
    protected volatile boolean roiSelecting = false;
    final private static float[] SELECT_COLOR = {.8f, 0, 0, .5f};

    /**
     * The current mouse point in chip pixels, updated by mouseMoved
     */
    protected Point currentMousePoint = null;

    public MLPNoiseFilter(AEChip chip) {
	super(chip);
	String deb = "5. Debug", disp = "2. Display", anal = "4. Analysis", tf = "0. Multilayer Perceptron", input = "1. Input";
	setPropertyTooltip(tf, "loadNetwork", "Load a protobuf .pb file containing the network or select a folder holding SavedModelBundle");
	setPropertyTooltip(tf, "lastManuallyLoadedNetwork", "Last network we manually loaded");
	setPropertyTooltip(disp, "measurePerformance", "Measures and logs time in ms to process each frame along with estimated operations count (MAC=2OPS)");
	setPropertyTooltip(tf, "inputLayerName", "(TensorFlow only) Input layer; parse it from loading the network and examining console output for layers for lines starting with ****");
	setPropertyTooltip(tf, "outputLayerName", "(TensorFlow only) Output layer; parse it from loading the network and examining console output for layers for lines starting with ****");
	setPropertyTooltip(tf, "tfBatchSizeEvents", "Number of events to process in parallel for inference");
	setPropertyTooltip(tf, "patchWidthAndHeightPixels", "Dimension (width and height in pixels) of the timestamp image input to DNN around each event (default 11)"); // TODO fix default to match training
	setPropertyTooltip(tf, "signalClassifierThreshold", "Threshold for clasifying event as signal"); // TODO fix default to match training
	setPropertyTooltip(tf, "useTI", "use TI only as input of MLP"); // TODO fix default to match training
	setPropertyTooltip(tf, "usePolarity", "use Polarity only as input of MLP"); // TODO fix default to match training
	setPropertyTooltip(tf, "useTIandPol", "use both TI and Polarity as input of MLP"); // TODO fix default to match training

	setPropertyTooltip(tf, "tiPatchMethod", "Method used to compute the value of the timestamp image patch values");
	setPropertyTooltip(disp, "showClassificationHistogram", "Shows a histogram of classification results");
	setPropertyTooltip(disp, "showTimeimagePatch", "Shows a window with timestamp image input to MLP");
	setPropertyTooltip(disp, "showOnlyNoiseTimeimages", "Shows timestamp image input to MLP only for noise classifications");
	setPropertyTooltip(disp, "showOnlySignalTimeimages", "Shows timestamp image input to MLP only for signal classifications");
	setPropertyTooltip(disp, "saveTiPatchImages", "Saves the TI patch images to a folder named MLPNoiseFilter-signal or MLPNoiseFilter-noise");
	setPropertyTooltip(tf, "timeWindowS", "Window of time in seconds that the timestamp image counts past events; pixels with older events are set to zero");
	String roi = "Region of interest";
	setPropertyTooltip(roi, "freezeRoi", "Freezes ROI (region of interest) selection");
	setPropertyTooltip(roi, "clearROI", "Clears ROI (region of interest)");
	hideProperty("correlationTimeS");
	hideProperty("antiCasualEnabled");
	hideProperty("sigmaDistPixels");
	hideProperty("adaptiveFilteringEnabled");
	removeNoiseFilterControl();
    }

    @Override
    synchronized public EventPacket<? extends BasicEvent> filterPacket(EventPacket<? extends BasicEvent> in) {
	super.filterPacket(in);
	if (timestampImage == null) {
	    allocateMaps(chip);
	}
	int tauUs = (int) Math.round(getCorrelationTimeS() * 1e6f);
	ssx = sxm1 >> subsampleBy;
	ssy = sym1 >> subsampleBy;

	eventToDisplayTIPatchFor = null;
	if (tiPatchDisplay != null) {
	    tiPatchDisplay.clearImage();
	}
	showThisTiPatch = false;
	ArrayList<PolarityEvent> inList;
	try {
	    inList = createEventList((EventPacket<PolarityEvent>) in);
	} catch (Exception ex) {
	    log.warning(String.format("%s: skipping nonmonotonic packet [%s]", ex, in));
	    return in;
	}

	for (PolarityEvent e : inList) {

	    if (e == null) {
		continue;
	    }
	    if (e.isSpecial()) {
		continue;
	    }
	    totalEventCount++;
	    final int ts = e.timestamp;
	    final int x = (e.x >> subsampleBy), y = (e.y >> subsampleBy); // subsampling address
	    if ((x < 0) || (x > ssx) || (y < 0) || (y > ssy)) { // out of bounds, discard (maybe bad USB or something)
		filterOut(e);
		continue;
	    }
	    if (timestampImage[x][y] == DEFAULT_TIMESTAMP) { // if it is the first event at this pixel
		timestampImage[x][y] = ts; // save timestamp
		if (letFirstEventThrough) {
		    filterIn(e); // if we let through every first event at each pixel, filter it in
		    continue;
		} else {
		    filterOut(e); // otherwise filter out
		    continue;
		}
	    }
//            timestampImage[x][y] = ts;
	    // make timestamp image patch to classify
	    int radius = (patchWidthAndHeightPixels - 1) / 2;
	    tfNumInBatchSoFar++;
	    eventList.add(e); // add the event object to list so we can later filter it in or not, in the original order
	    // see if we display this particular TI patch, only 1 per packet
	    if (eventToDisplayTIPatchFor == null && insideRoi(e)) {
		eventToDisplayTIPatchFor = e;
	    }
	    if (useTI || useTIandPol) {
		for (int indx = x - radius; indx <= x + radius; indx++) {
		    // iterate over NNb, computing the TI patch value
		    for (int indy = y - radius; indy <= y + radius; indy++) {
			if (indx < 0 || indx >= ssx || indy < 0 || indy > ssy) {
			    tfInputFloatBuffer.put(0); // For NNbs that are outside chip address space, set the TI patch input to zero
			    continue;
			}
			int nnbTs = timestampImage[indx][indy]; // NNb timestamp 
			if (nnbTs == DEFAULT_TIMESTAMP) {
			    tfInputFloatBuffer.put(0); // if the NNb pixel had no event, then just write 0 to TI patch
			} else {
			    int dt = nnbTs - ts; // dt is negative delta time, i.e. the time in us of NNb event relative to us.  When NNb ts is older, dt is more negative
			    float v = 0; // value put into TI patch
			    if (dt <= 0) { // ok, NNb pixel timestamp is older than us
				switch (tiPatchMethod) {
				    case ExponentialDecay:
//                                float expDt = (float) Math.exp(dt / tauUs);  // Compute exp(-dt/tau) that decays to zero for very old events in NNb
					v = fastexp((float) dt / tauUs);  // Compute exp(-dt/tau) that decays to zero for very old events in NNb
					break;
				    case LinearDecay:

					if (-dt < tauUs) {
					    v = 1 - ((float) (-dt)) / tauUs;  // if dt is 0, then linearDt is 1, if dt=-tauUs, then linearDt=0
					}
				}
//                            if (indx-x == 0 && indy-y == 0) {
//                                log.info(String.format("dt=%d v=%.2f", dt, v));
//                            }
			    } else {  // if dt>0 then time was nonmonotonic, ignore this pixel
				if (nonmonotonicWarningCount++ % NONONOTONIC_TIMESTAMP_WARNING_INTERVAL == 0) {
				    log.warning(String.format("timestamp in patch in future by %ss", eng.format(1e-6f * dt)));
				}
			    }
			    tfInputFloatBuffer.put(v);
			    if (tiPatchDisplay != null && eventToDisplayTIPatchFor != null && e == eventToDisplayTIPatchFor) {
				tiPatchDisplay.setPixmapGray(indx + radius - x, indy + radius - y, v); // shift back to 0,0 coordinate at LL
			    }
			}
		    }
		}
	    }
	    if (usePolarity || useTIandPol) {
		int pol = e.getPolarity() == PolarityEvent.Polarity.Off ? -1 : 1;
		lastPolMap[x][y] = pol;

		for (int indx = x - radius; indx <= x + radius; indx++) {
		    // iterate over NNb, computing the TI patch value
		    for (int indy = y - radius; indy <= y + radius; indy++) {
			if (indx < 0 || indx >= ssx || indy < 0 || indy > ssy) {
			    tfInputFloatBuffer.put(0); // For NNbs that are outside chip address space, set the TI patch input to zero
			    continue;
			}
			int p = lastPolMap[indx][indy];

			tfInputFloatBuffer.put(p); // if the NNb pixel had no event, then just write 0 to TI patch

		    }
		}
	    }

	    if (tfNumInBatchSoFar >= tfBatchSizeEvents) { // if we have a full batch, classify the events in it
		classifyEvents();<commit_msg>set old event's polarity to be 0<commit_after>/*
 * Copyright (C) 2021 tobid.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package net.sf.jaer.eventprocessing.filter;

import ch.unizh.ini.jaer.projects.humanpose.AbstractDavisCNNProcessor;
import ch.unizh.ini.jaer.projects.humanpose.DavisClassifierCNNProcessor;
import com.jogamp.opengl.GL2;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.util.awt.TextRenderer;
import java.io.File;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
import net.sf.jaer.Description;
import net.sf.jaer.DevelopmentStatus;
import net.sf.jaer.chip.AEChip;
import net.sf.jaer.event.BasicEvent;
import net.sf.jaer.event.EventPacket;
import net.sf.jaer.eventprocessing.TimeLimiter;
import net.sf.jaer.graphics.ImageDisplay;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.tensorflow.Graph;
import org.tensorflow.Operation;
import org.tensorflow.SavedModelBundle;
import org.tensorflow.Session;
import org.tensorflow.Tensor;
import com.github.sh0nk.matplotlib4j.Plot;
import com.google.common.primitives.Doubles;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.awt.GLCanvas;
import com.jogamp.opengl.glu.GLU;
import com.jogamp.opengl.glu.GLUquadric;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import net.sf.jaer.event.PolarityEvent;
import static net.sf.jaer.eventprocessing.EventFilter.log;
import net.sf.jaer.graphics.ChipCanvas;
import net.sf.jaer.util.EngineeringFormat;
import static net.sf.jaer.util.avioutput.AVIOutputStream.VideoFormat.PNG;
import org.apache.commons.io.FilenameUtils;
import org.tensorflow.Output;
import org.tensorflow.Shape;
import net.sf.jaer.util.RemoteControlCommand;

/**
 * Noise filter that runs a DNN to denoise events
 *
 * @author <NAME> and <NAME>, 2021
 */
@Description("Denoising noise filter that uses a DNN deep neural network to classify events as signal or noise events")
@DevelopmentStatus(DevelopmentStatus.Status.Experimental)
public class MLPNoiseFilter extends AbstractNoiseFilter implements MouseListener, MouseMotionListener, MouseWheelListener {

    private final String KEY_NETWORK_FILENAME = "lastNetworkFilename";
    private String lastManuallyLoadedNetwork = getString("lastManuallyLoadedNetwork", ""); // stores filename and path to last successfully loaded network that user loaded via doLoadNetwork
    private TextRenderer textRenderer = null;
//    private boolean measurePerformance = getBoolean("measurePerformance", true);

    private String performanceString = null; // holds string representation of processing time
    private TimeLimiter timeLimiter = new TimeLimiter(); // private instance used to accumulate events to slices even if packet has timed out
    private int processingTimeLimitMs = getInt("processingTimeLimitMs", 100); // time limit for processing packet in ms to process OF events (events still accumulate). Overrides the system EventPacket timelimiter, which cannot be used here because we still need to accumulate and render the events.
    private String lastPerformanceString = null;

    private int sxm1; // size of chip minus 1
    private int sym1;
    private int ssx; // size of subsampled timestamp map
    private int ssy;

    private SavedModelBundle tfSavedModelBundle = null;
    private Graph tfExecutionGraph = null;
    private Session tfSession = null;
    private int tfBatchSizeEvents = getInt("tfBatchSizeEvents", 1024);
    private int tfNumInBatchSoFar = 0;
    private FloatBuffer tfInputFloatBuffer = null;
    private ArrayList<BasicEvent> eventList = new ArrayList(tfBatchSizeEvents);
    protected float signalClassifierThreshold = getFloat("signalClassifierThreshold", 0.5f);

    // plotting TI patches and stats
    private DescriptiveStatistics stats = null;
    private volatile boolean showThisTiPatch = false;
    // TI patch display
    private ImageDisplay tiPatchDisplay = null;
    private JFrame tiFrame = null;
    private BasicEvent eventToDisplayTIPatchFor = null;
    private EngineeringFormat eng = new EngineeringFormat();

    public enum TIPatchMethod {
	ExponentialDecay, LinearDecay // default is LinearDecay since it works better (and is faster and cheaper)
    };
    private TIPatchMethod tiPatchMethod = TIPatchMethod.valueOf(getString("tiPatchMethod", TIPatchMethod.LinearDecay.toString()));

    private int patchWidthAndHeightPixels = getInt("patchWidthAndHeightPixels", 7);
    private int[][] timestampImage; // timestamp image
    private int[][] lastPolMap; // timestamp image
    private boolean useTI = getBoolean("useTI", true);
    private boolean usePolarity = getBoolean("usePolarity", false);
    private boolean useTIandPol = getBoolean("useTIandPol", false);

    private boolean showOnlySignalTimeimages = getBoolean("showOnlySignalTimeimages", false);
    private boolean showOnlyNoiseTimeimages = getBoolean("showOnlyNoiseTimeimages", false);
    protected boolean saveTiPatchImages = false;
    private int tiFileCounter = 0;
    private File saveDir = null;
    private int inputSF = 1;

    private final int NONONOTONIC_TIMESTAMP_WARNING_INTERVAL = 100000;
    private int nonmonotonicWarningCount = 0;

    /**
     * Cursor size for drawn mouse cursor when filter is selected.
     */
    protected final float CURSOR_SIZE_CHIP_PIXELS = 7;
    protected GLU glu = new GLU();
    protected GLUquadric quad = null;
    private boolean hasBlendChecked = false, hasBlend = false;
    protected boolean showCrossHairCursor = true;
    protected GLCanvas glCanvas;
    protected ChipCanvas chipCanvas;
    float[] cursorColor = null;

    /**
     * Flag that freezes ROI selection
     */
    protected boolean freezeRoi = getBoolean("freezeRoi", false);

    // roiRect stuff
    /**
     * ROI start/end corner index
     */
    protected int roiStartx, roiStarty, roiEndx, roiEndy;
    /**
     * ROI start/end corners and last clicked mouse point
     */
    protected Point roiStartPoint = null, roiEndPoint = null, clickedPoint = null;
    /**
     * ROI rectangle
     */
    protected Rectangle roiRect = (Rectangle) getObject("roiRect", null);

    /**
     * Boolean that indicates ROI is being selected currently
     */
    protected volatile boolean roiSelecting = false;
    final private static float[] SELECT_COLOR = {.8f, 0, 0, .5f};

    /**
     * The current mouse point in chip pixels, updated by mouseMoved
     */
    protected Point currentMousePoint = null;

    public MLPNoiseFilter(AEChip chip) {
	super(chip);
	String deb = "5. Debug", disp = "2. Display", anal = "4. Analysis", tf = "0. Multilayer Perceptron", input = "1. Input";
	setPropertyTooltip(tf, "loadNetwork", "Load a protobuf .pb file containing the network or select a folder holding SavedModelBundle");
	setPropertyTooltip(tf, "lastManuallyLoadedNetwork", "Last network we manually loaded");
	setPropertyTooltip(disp, "measurePerformance", "Measures and logs time in ms to process each frame along with estimated operations count (MAC=2OPS)");
	setPropertyTooltip(tf, "inputLayerName", "(TensorFlow only) Input layer; parse it from loading the network and examining console output for layers for lines starting with ****");
	setPropertyTooltip(tf, "outputLayerName", "(TensorFlow only) Output layer; parse it from loading the network and examining console output for layers for lines starting with ****");
	setPropertyTooltip(tf, "tfBatchSizeEvents", "Number of events to process in parallel for inference");
	setPropertyTooltip(tf, "patchWidthAndHeightPixels", "Dimension (width and height in pixels) of the timestamp image input to DNN around each event (default 11)"); // TODO fix default to match training
	setPropertyTooltip(tf, "signalClassifierThreshold", "Threshold for clasifying event as signal"); // TODO fix default to match training
	setPropertyTooltip(tf, "useTI", "use TI only as input of MLP"); // TODO fix default to match training
	setPropertyTooltip(tf, "usePolarity", "use Polarity only as input of MLP"); // TODO fix default to match training
	setPropertyTooltip(tf, "useTIandPol", "use both TI and Polarity as input of MLP"); // TODO fix default to match training

	setPropertyTooltip(tf, "tiPatchMethod", "Method used to compute the value of the timestamp image patch values");
	setPropertyTooltip(disp, "showClassificationHistogram", "Shows a histogram of classification results");
	setPropertyTooltip(disp, "showTimeimagePatch", "Shows a window with timestamp image input to MLP");
	setPropertyTooltip(disp, "showOnlyNoiseTimeimages", "Shows timestamp image input to MLP only for noise classifications");
	setPropertyTooltip(disp, "showOnlySignalTimeimages", "Shows timestamp image input to MLP only for signal classifications");
	setPropertyTooltip(disp, "saveTiPatchImages", "Saves the TI patch images to a folder named MLPNoiseFilter-signal or MLPNoiseFilter-noise");
	setPropertyTooltip(tf, "timeWindowS", "Window of time in seconds that the timestamp image counts past events; pixels with older events are set to zero");
	String roi = "Region of interest";
	setPropertyTooltip(roi, "freezeRoi", "Freezes ROI (region of interest) selection");
	setPropertyTooltip(roi, "clearROI", "Clears ROI (region of interest)");
	hideProperty("correlationTimeS");
	hideProperty("antiCasualEnabled");
	hideProperty("sigmaDistPixels");
	hideProperty("adaptiveFilteringEnabled");
	removeNoiseFilterControl();
    }

    @Override
    synchronized public EventPacket<? extends BasicEvent> filterPacket(EventPacket<? extends BasicEvent> in) {
	super.filterPacket(in);
	if (timestampImage == null) {
	    allocateMaps(chip);
	}
	int tauUs = (int) Math.round(getCorrelationTimeS() * 1e6f);
	ssx = sxm1 >> subsampleBy;
	ssy = sym1 >> subsampleBy;

	eventToDisplayTIPatchFor = null;
	if (tiPatchDisplay != null) {
	    tiPatchDisplay.clearImage();
	}
	showThisTiPatch = false;
	ArrayList<PolarityEvent> inList;
	try {
	    inList = createEventList((EventPacket<PolarityEvent>) in);
	} catch (Exception ex) {
	    log.warning(String.format("%s: skipping nonmonotonic packet [%s]", ex, in));
	    return in;
	}

	for (PolarityEvent e : inList) {

	    if (e == null) {
		continue;
	    }
	    if (e.isSpecial()) {
		continue;
	    }
	    totalEventCount++;
	    final int ts = e.timestamp;
	    final int x = (e.x >> subsampleBy), y = (e.y >> subsampleBy); // subsampling address
	    if ((x < 0) || (x > ssx) || (y < 0) || (y > ssy)) { // out of bounds, discard (maybe bad USB or something)
		filterOut(e);
		continue;
	    }
	    if (timestampImage[x][y] == DEFAULT_TIMESTAMP) { // if it is the first event at this pixel
		timestampImage[x][y] = ts; // save timestamp
		if (letFirstEventThrough) {
		    filterIn(e); // if we let through every first event at each pixel, filter it in
		    continue;
		} else {
		    filterOut(e); // otherwise filter out
		    continue;
		}
	    }
//            timestampImage[x][y] = ts;
	    // make timestamp image patch to classify
	    int radius = (patchWidthAndHeightPixels - 1) / 2;
	    tfNumInBatchSoFar++;
	    eventList.add(e); // add the event object to list so we can later filter it in or not, in the original order
	    // see if we display this particular TI patch, only 1 per packet
	    if (eventToDisplayTIPatchFor == null && insideRoi(e)) {
		eventToDisplayTIPatchFor = e;
	    }
	    if (useTI || useTIandPol) {
		for (int indx = x - radius; indx <= x + radius; indx++) {
		    // iterate over NNb, computing the TI patch value
		    for (int indy = y - radius; indy <= y + radius; indy++) {
			if (indx < 0 || indx >= ssx || indy < 0 || indy > ssy) {
			    tfInputFloatBuffer.put(0); // For NNbs that are outside chip address space, set the TI patch input to zero
			    continue;
			}
			int nnbTs = timestampImage[indx][indy]; // NNb timestamp 
			if (nnbTs == DEFAULT_TIMESTAMP) {
			    tfInputFloatBuffer.put(0); // if the NNb pixel had no event, then just write 0 to TI patch
			} else {
			    int dt = nnbTs - ts; // dt is negative delta time, i.e. the time in us of NNb event relative to us.  When NNb ts is older, dt is more negative
			    float v = 0; // value put into TI patch
			    if (dt <= 0) { // ok, NNb pixel timestamp is older than us
				switch (tiPatchMethod) {
				    case ExponentialDecay:
//                                float expDt = (float) Math.exp(dt / tauUs);  // Compute exp(-dt/tau) that decays to zero for very old events in NNb
					v = fastexp((float) dt / tauUs);  // Compute exp(-dt/tau) that decays to zero for very old events in NNb
					break;
				    case LinearDecay:

					if (-dt < tauUs) {
					    v = 1 - ((float) (-dt)) / tauUs;  // if dt is 0, then linearDt is 1, if dt=-tauUs, then linearDt=0
					}
				}
//                            if (indx-x == 0 && indy-y == 0) {
//                                log.info(String.format("dt=%d v=%.2f", dt, v));
//                            }
			    } else {  // if dt>0 then time was nonmonotonic, ignore this pixel
				if (nonmonotonicWarningCount++ % NONONOTONIC_TIMESTAMP_WARNING_INTERVAL == 0) {
				    log.warning(String.format("timestamp in patch in future by %ss", eng.format(1e-6f * dt)));
				}
			    }
			    tfInputFloatBuffer.put(v);
			    if (tiPatchDisplay != null && eventToDisplayTIPatchFor != null && e == eventToDisplayTIPatchFor) {
				tiPatchDisplay.setPixmapGray(indx + radius - x, indy + radius - y, v); // shift back to 0,0 coordinate at LL
			    }
			}
		    }
		}
	    }
            if (usePolarity || useTIandPol) {
		int pol = e.getPolarity() == PolarityEvent.Polarity.Off ? -1 : 1;
		lastPolMap[x][y] = pol;

		for (int indx = x - radius; indx <= x + radius; indx++) {
		    // iterate over NNb, computing the TI patch value
		    for (int indy = y - radius; indy <= y + radius; indy++) {
			if (indx < 0 || indx >= ssx || indy < 0 || indy > ssy) {
			    tfInputFloatBuffer.put(0); // For NNbs that are outside chip address space, set the TI patch input to zero
			    continue;
			}

			int p = lastPolMap[indx][indy];
			int nnbTs = timestampImage[indx][indy];
			if (nnbTs == DEFAULT_TIMESTAMP) {
			    p = 0; // if the NNb pixel had no event, then just write 0 to TI patch
			} else {
			    int dt = nnbTs - ts; // dt is negative delta time, i.e. the time in us of NNb event relative to us.  When NNb ts is older, dt is more negative

			    if (-dt > tauUs) {
				p = 0;
			    }
			}
			tfInputFloatBuffer.put(p); // if the NNb pixel had no event, then just write 0 to TI patch
		    }
		}

	    }

	    if (tfNumInBatchSoFar >= tfBatchSizeEvents) { // if we have a full batch, classify the events in it
		classifyEvents();
{'fraction_non_alphanumeric': 0.09725984251968504, 'fraction_numerical': 0.006236220472440945, 'mean_word_length': 3.756704119850187, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 50, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 2, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3433288', 'n_tokens_mistral': 9910, 'n_tokens_neox': 9205, 'n_words': 3156}