diff --git a/photon-client/src/store/index.js b/photon-client/src/store/index.js
index 714534c57..9a462a262 100644
--- a/photon-client/src/store/index.js
+++ b/photon-client/src/store/index.js
@@ -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,
diff --git a/photon-client/src/views/PipelineViews/ContoursTab.vue b/photon-client/src/views/PipelineViews/ContoursTab.vue
index c239e04ee..364bd6968 100644
--- a/photon-client/src/views/PipelineViews/ContoursTab.vue
+++ b/photon-client/src/views/PipelineViews/ContoursTab.vue
@@ -5,7 +5,7 @@
name="Area"
min="0"
max="100"
- step="0.1"
+ step="0.01"
@input="handlePipelineData('contourArea')"
/>
+
+
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());
- }
}
diff --git a/photon-core/src/main/java/org/photonvision/vision/pipe/impl/FilterContoursPipe.java b/photon-core/src/main/java/org/photonvision/vision/pipe/impl/FilterContoursPipe.java
index 90d0eac6d..19409c9d6 100644
--- a/photon-core/src/main/java/org/photonvision/vision/pipe/impl/FilterContoursPipe.java
+++ b/photon-core/src/main/java/org/photonvision/vision/pipe/impl/FilterContoursPipe.java
@@ -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 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() {
diff --git a/photon-core/src/main/java/org/photonvision/vision/pipeline/ReflectivePipeline.java b/photon-core/src/main/java/org/photonvision/vision/pipeline/ReflectivePipeline.java
index 13c375a7d..ac7476bed 100644
--- a/photon-core/src/main/java/org/photonvision/vision/pipeline/ReflectivePipeline.java
+++ b/photon-core/src/main/java/org/photonvision/vision/pipeline/ReflectivePipeline.java
@@ -100,7 +100,9 @@ public class ReflectivePipeline extends CVPipeline