Add outlier rejection (#432)

Uses standard deviations from mean x/y location to reject outliers
This commit is contained in:
Matt
2022-02-28 00:44:22 -05:00
committed by GitHub
parent 3120a6439b
commit 50fdfd8bce
6 changed files with 167 additions and 7 deletions

View File

@@ -69,6 +69,8 @@ export default new Vuex.Store({
contourRatio: [0, 12],
contourFullness: [0, 12],
contourSpecklePercentage: 5,
contourFilterRangeX: 5,
contourFilterRangeY: 5,
contourGroupingMode: 0,
contourIntersection: 0,
contourSortMode: 0,

View File

@@ -5,7 +5,7 @@
name="Area"
min="0"
max="100"
step="0.1"
step="0.01"
@input="handlePipelineData('contourArea')"
/>
<CVrangeSlider
@@ -46,6 +46,26 @@
@input="handlePipelineData('contourSpecklePercentage')"
/>
<template v-if="currentPipelineType() !== 3">
<CVslider
v-model="contourFilterRangeX"
name="X filter tightness"
tooltip="Rejects contours whose center X is further than X standard deviations above/below the mean X location"
min="0.1"
max="4"
step="0.1"
:slider-cols="largeBox"
@input="handlePipelineData('contourFilterRangeX')"
/>
<CVslider
v-model="contourFilterRangeY"
name="Y filter tightness"
tooltip="Rejects contours whose center Y is further than X standard deviations above/below the mean Y location"
min="0.1"
max="4"
step="0.1"
:slider-cols="largeBox"
@input="handlePipelineData('contourFilterRangeY')"
/>
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
@@ -207,6 +227,25 @@ export default {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourFilterRangeX: {
get() {
console.log(this.$store.getters.currentPipelineSettings.contourFilterRangeX)
return this.$store.getters.currentPipelineSettings.contourFilterRangeX
},
set(val) {
console.log("set")
console.log(val)
this.$store.commit("mutatePipeline", {"contourFilterRangeX": val});
}
},
contourFilterRangeY: {
get() {
return this.$store.getters.currentPipelineSettings.contourFilterRangeY
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFilterRangeY": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode

View File

@@ -18,6 +18,8 @@
package org.photonvision.common.util.math;
import edu.wpi.first.util.WPIUtilJNI;
import java.util.Arrays;
import java.util.List;
public class MathUtils {
MathUtils() {}
@@ -64,6 +66,58 @@ public class MathUtils {
return (int) Math.floor(map((double) value, inMin, inMax, outMin, outMax) + 0.5);
}
public static long wpiNanoTime() {
return microsToNanos(WPIUtilJNI.now());
}
/**
* Get the value of the nTh percentile of a list
*
* @param list The list to evaluate
* @param p The percentile, in [0,100]
* @return
*/
public static double getPercentile(List<Double> list, double p) {
if ((p > 100) || (p <= 0)) {
throw new IllegalArgumentException("invalid quantile value: " + p);
}
if (list.size() == 0) {
return Double.NaN;
}
if (list.size() == 1) {
return list.get(0); // always return single value for n = 1
}
// Sort array. We avoid a third copy here by just creating the
// list directly.
double[] sorted = new double[list.size()];
for (int i = 0; i < list.size(); i++) {
sorted[i] = list.get(i);
}
Arrays.sort(sorted);
return evaluateSorted(sorted, p);
}
private static double evaluateSorted(final double[] sorted, final double p) {
double n = sorted.length;
double pos = p * (n + 1) / 100;
double fpos = Math.floor(pos);
int intPos = (int) fpos;
double dif = pos - fpos;
if (pos < 1) {
return sorted[0];
}
if (pos >= n) {
return sorted[sorted.length - 1];
}
double lower = sorted[intPos - 1];
double upper = sorted[intPos];
return lower + dif * (upper - lower);
}
/**
* Linearly interpolates between two values.
*
@@ -76,8 +130,4 @@ public class MathUtils {
public static double lerp(double startValue, double endValue, double t) {
return startValue + (endValue - startValue) * t;
}
public static long wpiNanoTime() {
return microsToNanos(WPIUtilJNI.now());
}
}

View File

@@ -18,9 +18,12 @@
package org.photonvision.vision.pipe.impl;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.opencv.core.Rect;
import org.opencv.core.RotatedRect;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.opencv.Contour;
@@ -36,9 +39,64 @@ public class FilterContoursPipe
for (Contour contour : in) {
filterContour(contour);
}
// we need the whole list for outlier rejection
rejectOutliers(m_filteredContours, params.xTol, params.yTol);
return m_filteredContours;
}
private void rejectOutliers(List<Contour> list, double xTol, double yTol) {
if (list.size() < 2) return; // Must have at least 2 points to reject outliers
/*
// Sort by X and find median
list.sort(Comparator.comparingDouble(c -> c.getCenterPoint().x));
double medianX = list.get(list.size() / 2).getCenterPoint().x;
if (list.size() % 2 == 0)
medianX = (medianX + list.get(list.size() / 2 - 1).getCenterPoint().x) / 2;
*/
double meanX = list.stream().mapToDouble(it -> it.getCenterPoint().x).sum() / list.size();
double stdDevX =
list.stream().mapToDouble(it -> Math.pow(it.getCenterPoint().x - meanX, 2.0)).sum();
stdDevX /= (list.size() - 1);
stdDevX = Math.sqrt(stdDevX);
/*
// Sort by Y and find median
list.sort(Comparator.comparingDouble(c -> c.getCenterPoint().y));
double medianY = list.get(list.size() / 2).getCenterPoint().y;
if (list.size() % 2 == 0)
medianY = (medianY + list.get(list.size() / 2 - 1).getCenterPoint().y) / 2;
*/
double meanY = list.stream().mapToDouble(it -> it.getCenterPoint().y).sum() / list.size();
double stdDevY =
list.stream().mapToDouble(it -> Math.pow(it.getCenterPoint().y - meanY, 2.0)).sum();
stdDevY /= (list.size() - 1);
stdDevY = Math.sqrt(stdDevY);
for (var it = list.iterator(); it.hasNext(); ) {
// Reject points more than N standard devs above/below median
// That is, |point - median| > std dev * tol
Contour c = it.next();
double x = c.getCenterPoint().x;
double y = c.getCenterPoint().y;
if (Math.abs(x - meanX) > stdDevX * xTol) {
it.remove();
} else if (Math.abs(y - meanY) > stdDevY * yTol) {
it.remove();
}
// Otherwise we're good! Keep it in
}
}
private void filterContour(Contour contour) {
RotatedRect minAreaRect = contour.getMinAreaRect();
@@ -69,16 +127,22 @@ public class FilterContoursPipe
private final DoubleCouple m_ratio;
private final DoubleCouple m_fullness;
private final FrameStaticProperties m_frameStaticProperties;
private final double xTol; // IQR tolerance for x
private final double yTol; // IQR tolerance for x
public FilterContoursParams(
DoubleCouple area,
DoubleCouple ratio,
DoubleCouple extent,
FrameStaticProperties camProperties) {
FrameStaticProperties camProperties,
double xTol,
double yTol) {
this.m_area = area;
this.m_ratio = ratio;
this.m_fullness = extent;
this.m_frameStaticProperties = camProperties;
this.xTol = xTol;
this.yTol = yTol;
}
public DoubleCouple getArea() {

View File

@@ -100,7 +100,9 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
settings.contourArea,
settings.contourRatio,
settings.contourFullness,
frameStaticProperties);
frameStaticProperties,
settings.contourFilterRangeX,
settings.contourFilterRangeY);
filterContoursPipe.setParams(filterContoursParams);
var groupContoursParams =

View File

@@ -21,6 +21,9 @@ import com.fasterxml.jackson.annotation.JsonTypeName;
@JsonTypeName("ReflectivePipelineSettings")
public class ReflectivePipelineSettings extends AdvancedPipelineSettings {
public double contourFilterRangeX = 2;
public double contourFilterRangeY = 2;
public ReflectivePipelineSettings() {
super();
pipelineType = PipelineType.Reflective;