mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-04 03:11:40 +00:00
Add outlier rejection (#432)
Uses standard deviations from mean x/y location to reject outliers
This commit is contained in:
@@ -69,6 +69,8 @@ export default new Vuex.Store({
|
|||||||
contourRatio: [0, 12],
|
contourRatio: [0, 12],
|
||||||
contourFullness: [0, 12],
|
contourFullness: [0, 12],
|
||||||
contourSpecklePercentage: 5,
|
contourSpecklePercentage: 5,
|
||||||
|
contourFilterRangeX: 5,
|
||||||
|
contourFilterRangeY: 5,
|
||||||
contourGroupingMode: 0,
|
contourGroupingMode: 0,
|
||||||
contourIntersection: 0,
|
contourIntersection: 0,
|
||||||
contourSortMode: 0,
|
contourSortMode: 0,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
name="Area"
|
name="Area"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.1"
|
step="0.01"
|
||||||
@input="handlePipelineData('contourArea')"
|
@input="handlePipelineData('contourArea')"
|
||||||
/>
|
/>
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
@@ -46,6 +46,26 @@
|
|||||||
@input="handlePipelineData('contourSpecklePercentage')"
|
@input="handlePipelineData('contourSpecklePercentage')"
|
||||||
/>
|
/>
|
||||||
<template v-if="currentPipelineType() !== 3">
|
<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
|
<CVselect
|
||||||
v-model="contourGroupingMode"
|
v-model="contourGroupingMode"
|
||||||
name="Target Grouping"
|
name="Target Grouping"
|
||||||
@@ -207,6 +227,25 @@ export default {
|
|||||||
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
|
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: {
|
contourGroupingMode: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentPipelineSettings.contourGroupingMode
|
return this.$store.getters.currentPipelineSettings.contourGroupingMode
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
package org.photonvision.common.util.math;
|
package org.photonvision.common.util.math;
|
||||||
|
|
||||||
import edu.wpi.first.util.WPIUtilJNI;
|
import edu.wpi.first.util.WPIUtilJNI;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class MathUtils {
|
public class MathUtils {
|
||||||
MathUtils() {}
|
MathUtils() {}
|
||||||
@@ -64,6 +66,58 @@ public class MathUtils {
|
|||||||
return (int) Math.floor(map((double) value, inMin, inMax, outMin, outMax) + 0.5);
|
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.
|
* Linearly interpolates between two values.
|
||||||
*
|
*
|
||||||
@@ -76,8 +130,4 @@ public class MathUtils {
|
|||||||
public static double lerp(double startValue, double endValue, double t) {
|
public static double lerp(double startValue, double endValue, double t) {
|
||||||
return startValue + (endValue - startValue) * t;
|
return startValue + (endValue - startValue) * t;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long wpiNanoTime() {
|
|
||||||
return microsToNanos(WPIUtilJNI.now());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,12 @@
|
|||||||
package org.photonvision.vision.pipe.impl;
|
package org.photonvision.vision.pipe.impl;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.opencv.core.Rect;
|
import org.opencv.core.Rect;
|
||||||
import org.opencv.core.RotatedRect;
|
import org.opencv.core.RotatedRect;
|
||||||
|
import org.photonvision.common.util.math.MathUtils;
|
||||||
import org.photonvision.common.util.numbers.DoubleCouple;
|
import org.photonvision.common.util.numbers.DoubleCouple;
|
||||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||||
import org.photonvision.vision.opencv.Contour;
|
import org.photonvision.vision.opencv.Contour;
|
||||||
@@ -36,9 +39,64 @@ public class FilterContoursPipe
|
|||||||
for (Contour contour : in) {
|
for (Contour contour : in) {
|
||||||
filterContour(contour);
|
filterContour(contour);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we need the whole list for outlier rejection
|
||||||
|
rejectOutliers(m_filteredContours, params.xTol, params.yTol);
|
||||||
|
|
||||||
return m_filteredContours;
|
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) {
|
private void filterContour(Contour contour) {
|
||||||
RotatedRect minAreaRect = contour.getMinAreaRect();
|
RotatedRect minAreaRect = contour.getMinAreaRect();
|
||||||
|
|
||||||
@@ -69,16 +127,22 @@ public class FilterContoursPipe
|
|||||||
private final DoubleCouple m_ratio;
|
private final DoubleCouple m_ratio;
|
||||||
private final DoubleCouple m_fullness;
|
private final DoubleCouple m_fullness;
|
||||||
private final FrameStaticProperties m_frameStaticProperties;
|
private final FrameStaticProperties m_frameStaticProperties;
|
||||||
|
private final double xTol; // IQR tolerance for x
|
||||||
|
private final double yTol; // IQR tolerance for x
|
||||||
|
|
||||||
public FilterContoursParams(
|
public FilterContoursParams(
|
||||||
DoubleCouple area,
|
DoubleCouple area,
|
||||||
DoubleCouple ratio,
|
DoubleCouple ratio,
|
||||||
DoubleCouple extent,
|
DoubleCouple extent,
|
||||||
FrameStaticProperties camProperties) {
|
FrameStaticProperties camProperties,
|
||||||
|
double xTol,
|
||||||
|
double yTol) {
|
||||||
this.m_area = area;
|
this.m_area = area;
|
||||||
this.m_ratio = ratio;
|
this.m_ratio = ratio;
|
||||||
this.m_fullness = extent;
|
this.m_fullness = extent;
|
||||||
this.m_frameStaticProperties = camProperties;
|
this.m_frameStaticProperties = camProperties;
|
||||||
|
this.xTol = xTol;
|
||||||
|
this.yTol = yTol;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DoubleCouple getArea() {
|
public DoubleCouple getArea() {
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
|||||||
settings.contourArea,
|
settings.contourArea,
|
||||||
settings.contourRatio,
|
settings.contourRatio,
|
||||||
settings.contourFullness,
|
settings.contourFullness,
|
||||||
frameStaticProperties);
|
frameStaticProperties,
|
||||||
|
settings.contourFilterRangeX,
|
||||||
|
settings.contourFilterRangeY);
|
||||||
filterContoursPipe.setParams(filterContoursParams);
|
filterContoursPipe.setParams(filterContoursParams);
|
||||||
|
|
||||||
var groupContoursParams =
|
var groupContoursParams =
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import com.fasterxml.jackson.annotation.JsonTypeName;
|
|||||||
|
|
||||||
@JsonTypeName("ReflectivePipelineSettings")
|
@JsonTypeName("ReflectivePipelineSettings")
|
||||||
public class ReflectivePipelineSettings extends AdvancedPipelineSettings {
|
public class ReflectivePipelineSettings extends AdvancedPipelineSettings {
|
||||||
|
public double contourFilterRangeX = 2;
|
||||||
|
public double contourFilterRangeY = 2;
|
||||||
|
|
||||||
public ReflectivePipelineSettings() {
|
public ReflectivePipelineSettings() {
|
||||||
super();
|
super();
|
||||||
pipelineType = PipelineType.Reflective;
|
pipelineType = PipelineType.Reflective;
|
||||||
|
|||||||
Reference in New Issue
Block a user