Initial commit
This commit is contained in:
commit
fc13916f0b
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,80 @@
|
|||
import java.text.SimpleDateFormat
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'maven-publish'
|
||||
id 'signing'
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
group = 'com.tearabite.ftctearabits'
|
||||
version = '1.0'
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java { srcDirs = ["src/java"] }
|
||||
resources { srcDir "src/resources" }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = 'com.tearabite.ftctearabits'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 24
|
||||
productFlavors {
|
||||
biscuit {
|
||||
aarMetadata {
|
||||
minCompileSdk 24
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testFixtures {
|
||||
enable true
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
groupId = 'com.tearabite.ftctearabits'
|
||||
artifactId = 'ftctearabits'
|
||||
version = '1.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://maven.brott.dev/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.firstinspires.ftc:RobotCore:9.0.1'
|
||||
implementation 'com.acmerobotics.roadrunner:core:1.0.0-beta6'
|
||||
implementation 'com.acmerobotics.roadrunner:actions:1.0.0-beta6'
|
||||
implementation 'org.firstinspires.ftc:Vision:9.0.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Note: the actual manifest file used in your APK merges this file with contributions
|
||||
from the modules on which your app depends (such as FtcRobotController, etc).
|
||||
So it won't ultimately be as empty as it might here appear to be :-) -->
|
||||
|
||||
<!-- The package name here determines the package for your R class and your BuildConfig class -->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application/>
|
||||
</manifest>
|
|
@ -0,0 +1,4 @@
|
|||
package com.tearabite.ftctearabits;
|
||||
|
||||
public enum Alliance { Blue, Red }
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tearabite.ftctearabits.localization;
|
||||
|
||||
import static java.lang.Math.PI;
|
||||
import static java.lang.Math.asin;
|
||||
import static java.lang.Math.atan2;
|
||||
import static java.lang.Math.cos;
|
||||
import static java.lang.Math.pow;
|
||||
import static java.lang.Math.sin;
|
||||
import static java.lang.Math.tan;
|
||||
|
||||
import com.acmerobotics.roadrunner.Pose2d;
|
||||
import com.acmerobotics.roadrunner.Vector2d;
|
||||
|
||||
import org.firstinspires.ftc.robotcore.external.matrices.VectorF;
|
||||
import org.firstinspires.ftc.robotcore.external.navigation.Quaternion;
|
||||
import org.firstinspires.ftc.vision.apriltag.AprilTagDetection;
|
||||
import org.firstinspires.ftc.vision.apriltag.AprilTagPoseFtc;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
|
||||
public class AprilTagPoseEstimator {
|
||||
|
||||
private final Pose2d robotOffset;
|
||||
|
||||
public AprilTagPoseEstimator() {
|
||||
this(new Pose2d(new Vector2d(0, 0), 0));
|
||||
}
|
||||
|
||||
public AprilTagPoseEstimator(Pose2d robotOffset) {
|
||||
this.robotOffset = robotOffset;
|
||||
}
|
||||
|
||||
|
||||
public Pose2d estimatePose(AprilTagDetection detection) {
|
||||
if (detection == null || detection.metadata == null || detection.metadata.fieldPosition == null || detection.ftcPose == null) {
|
||||
throw new InvalidParameterException();
|
||||
}
|
||||
|
||||
AprilTagPoseFtc ftcPose = detection.ftcPose;
|
||||
VectorF fieldPosition = detection.metadata.fieldPosition;
|
||||
Quaternion fieldOrientation = detection.metadata.fieldOrientation;
|
||||
return estimatePose(
|
||||
Math.toRadians(ftcPose.yaw),
|
||||
Math.toRadians(ftcPose.bearing),
|
||||
ftcPose.range,
|
||||
fieldPosition.get(0),
|
||||
fieldPosition.get(1),
|
||||
fieldOrientation.x,
|
||||
fieldOrientation.y,
|
||||
fieldOrientation.z,
|
||||
fieldOrientation.w);
|
||||
}
|
||||
|
||||
private Pose2d estimatePose(double yaw, double bearing, double range, double Tx, double Ty, double Ta, double Tb, double Tc, double Td) {
|
||||
double aprilTagHeading = getTYaw(Ta, Tb, Tc, Td);
|
||||
double cameraRotation = ((PI / 2) - aprilTagHeading + yaw);
|
||||
double cx = Tx + range * cos(bearing) * cos(cameraRotation) + range * sin(bearing) * sin(cameraRotation);
|
||||
double cy = Ty + range * cos(bearing) * sin(cameraRotation) - range * sin(bearing) * cos(cameraRotation);
|
||||
|
||||
double aprilTagX = this.robotOffset.position.x;
|
||||
double aprilTagY = this.robotOffset.position.y;
|
||||
double cameraRotationOnRobot = this.robotOffset.heading.toDouble();
|
||||
double robotRotation = (PI / 2) + aprilTagHeading + yaw - cameraRotationOnRobot;
|
||||
double rx = cx + aprilTagX * cos(robotRotation) - aprilTagY * sin(robotRotation);
|
||||
double ry = cy + aprilTagX * sin(robotRotation) + aprilTagY * cos(robotRotation);
|
||||
double rh = tan(yaw - cameraRotationOnRobot);
|
||||
|
||||
return new Pose2d(rx, ry, rh);
|
||||
}
|
||||
|
||||
private static double getTPitch(double a, double b, double c, double d) {
|
||||
return asin(2 * ((a * d) - (b * c)));
|
||||
}
|
||||
|
||||
private static double getTRoll(double a, double b, double c, double d) {
|
||||
return atan2(2 * ((a * c) + (b * d)), 1 - 2 * (pow(c, 2) + pow(d, 2)));
|
||||
}
|
||||
|
||||
private static double getTYaw(double a, double b, double c, double d) {
|
||||
return atan2(2 * ((a * b) + (c * d)), 1 - 2 * ((pow(b, 2) + pow(c, 2))));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package com.tearabite.ftctearabits.vision;
|
||||
|
||||
import static com.tearabite.ftctearabits.vision.Colors.WHITE;
|
||||
import static com.tearabite.ftctearabits.vision.OpenCVUtil.getLargestContour;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration;
|
||||
import org.firstinspires.ftc.vision.VisionProcessor;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.MatOfPoint;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.core.Size;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class BasicColorDetectionVisionProcessor implements VisionProcessor {
|
||||
public static final Size BLUR_SIZE = new Size(7, 7);
|
||||
public static final int ERODE_DILATE_ITERATIONS = 2;
|
||||
public static final Mat STRUCTURING_ELEMENT = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5, 5));
|
||||
public static final Point ANCHOR = new Point((STRUCTURING_ELEMENT.cols() / 2f), STRUCTURING_ELEMENT.rows() / 2f);
|
||||
|
||||
private final Mat blurred = new Mat();
|
||||
private final ScalarRange[] colorRanges;
|
||||
@Getter private Detection detection;
|
||||
private final Mat hsv = new Mat();
|
||||
private final double ignoreSmallerThan;
|
||||
private final double ignoreLargerThan;
|
||||
private final Mat mask = new Mat();
|
||||
private final Mat tmpMask = new Mat();
|
||||
|
||||
|
||||
public BasicColorDetectionVisionProcessor(double ignoreSmallerThan, double ignoreLargerThan, ScalarRange... colorRanges) {
|
||||
this.ignoreSmallerThan = ignoreSmallerThan;
|
||||
this.ignoreLargerThan = ignoreLargerThan;
|
||||
this.colorRanges = colorRanges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(int width, int height, CameraCalibration calibration) {
|
||||
this.detection = new Detection(new Size(width, height), ignoreSmallerThan, ignoreLargerThan);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object processFrame(Mat input, long captureTimeNanos) {
|
||||
Imgproc.GaussianBlur(input, blurred, BLUR_SIZE, 0);
|
||||
Imgproc.cvtColor(blurred, hsv, Imgproc.COLOR_RGB2HSV);
|
||||
|
||||
mask.release();
|
||||
for (ScalarRange colorRange : this.colorRanges) {
|
||||
Core.inRange(hsv, colorRange.getLower(), colorRange.getUpper(), tmpMask);
|
||||
if (mask.empty() || mask.rows() <= 0) {
|
||||
Core.inRange(hsv, colorRange.getLower(), colorRange.getUpper(), mask);
|
||||
}
|
||||
Core.add(mask, tmpMask, mask);
|
||||
}
|
||||
|
||||
Imgproc.erode(mask, mask, STRUCTURING_ELEMENT, ANCHOR, ERODE_DILATE_ITERATIONS);
|
||||
Imgproc.dilate(mask, mask, STRUCTURING_ELEMENT, ANCHOR, ERODE_DILATE_ITERATIONS);
|
||||
|
||||
ArrayList<MatOfPoint> contours = new ArrayList<>();
|
||||
Imgproc.findContours(mask, contours, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
|
||||
|
||||
detection.setContour(getLargestContour(contours));
|
||||
|
||||
return input;
|
||||
}
|
||||
@Override
|
||||
public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) {
|
||||
if (detection != null && detection.isValid()) {
|
||||
Point center = detection.getCenterPx();
|
||||
canvas.drawCircle((float) center.x, (float) center.y, 10, WHITE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.tearabite.ftctearabits.vision;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import org.opencv.core.Scalar;
|
||||
public class Colors {
|
||||
public static Scalar FTC_RED_LOWER = new Scalar(165, 80, 80);
|
||||
public static Scalar FTC_RED_UPPER = new Scalar(15, 255, 255);
|
||||
public static ScalarRange FTC_RED_RANGE_1 = new ScalarRange(new Scalar(180, FTC_RED_UPPER.val[1], FTC_RED_UPPER.val[2]), FTC_RED_LOWER);
|
||||
public static ScalarRange FTC_RED_RANGE_2 = new ScalarRange(FTC_RED_UPPER, new Scalar(0, FTC_RED_LOWER.val[1], FTC_RED_LOWER.val[2]));
|
||||
public static Scalar FTC_BLUE_LOWER = new Scalar(75, 40, 80);
|
||||
public static Scalar FTC_BLUE_UPPER = new Scalar(120, 255, 255);
|
||||
public static ScalarRange FTC_BLUE_RANGE = new ScalarRange(FTC_BLUE_UPPER, FTC_BLUE_LOWER);
|
||||
public static Scalar FTC_WHITE_LOWER = new Scalar(0, 0, 40);
|
||||
public static Scalar FTC_WHITE_UPPER = new Scalar(180, 30, 255);
|
||||
|
||||
public static OpenCVUtil.LinePaint RED = new OpenCVUtil.LinePaint(Color.RED);
|
||||
public static OpenCVUtil.LinePaint BLUE = new OpenCVUtil.LinePaint(Color.BLUE);
|
||||
public static OpenCVUtil.LinePaint BLACK = new OpenCVUtil.LinePaint(Color.BLACK);
|
||||
public static OpenCVUtil.LinePaint WHITE = new OpenCVUtil.LinePaint(Color.WHITE);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.tearabite.ftctearabits.vision;
|
||||
|
||||
import org.opencv.core.Scalar;
|
||||
|
||||
public class Constants {
|
||||
public static Scalar RED = new Scalar(255, 0, 0);
|
||||
public static Scalar GREEN = new Scalar(0, 255, 0);
|
||||
public static Scalar BLUE = new Scalar(0, 0, 255);
|
||||
public static Scalar WHITE = new Scalar(255, 255, 255);
|
||||
public static Scalar GRAY = new Scalar(80, 80, 80);
|
||||
public static Scalar BLACK = new Scalar(0, 0, 0);
|
||||
public static Scalar ORANGE = new Scalar(255, 165, 0);
|
||||
public static Scalar YELLOW = new Scalar(255, 255, 0);
|
||||
public static Scalar PURPLE = new Scalar(128, 0, 128);
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package com.tearabite.ftctearabits.vision;
|
||||
|
||||
import static com.tearabite.ftctearabits.vision.Constants.GREEN;
|
||||
import static com.tearabite.ftctearabits.vision.OpenCVUtil.drawConvexHull;
|
||||
import static com.tearabite.ftctearabits.vision.OpenCVUtil.drawPoint;
|
||||
import static com.tearabite.ftctearabits.vision.OpenCVUtil.fillConvexHull;
|
||||
import static com.tearabite.ftctearabits.vision.OpenCVUtil.getBottomLeftOfContour;
|
||||
import static com.tearabite.ftctearabits.vision.OpenCVUtil.getBottomRightOfContour;
|
||||
import static com.tearabite.ftctearabits.vision.OpenCVUtil.getCenterOfContour;
|
||||
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.MatOfPoint;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.core.Size;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
// Class for a Detection
|
||||
public class Detection {
|
||||
public static final Point INVALID_POINT = new Point(Double.MIN_VALUE, Double.MIN_VALUE);
|
||||
public static final double INVALID_AREA = -1;
|
||||
public static final Detection INVALID_DETECTION = new Detection(new Size(0, 0), 0);
|
||||
|
||||
private double minAreaPx;
|
||||
private double maxAreaPx;
|
||||
private final Size maxSizePx;
|
||||
private double areaPx = INVALID_AREA;
|
||||
private Point centerPx = INVALID_POINT;
|
||||
private Point bottomLeftPx = INVALID_POINT;
|
||||
private Point bottomRightPx = INVALID_POINT;
|
||||
private MatOfPoint contour;
|
||||
|
||||
// Constructor
|
||||
public Detection(Size frameSize, double minAreaFactor) {
|
||||
this.maxSizePx = frameSize;
|
||||
this.minAreaPx = frameSize.area() * minAreaFactor;
|
||||
this.maxAreaPx = frameSize.area();
|
||||
}
|
||||
|
||||
public Detection(Size frameSize, double minAreaFactor, double maxAreaFactor) {
|
||||
this.maxSizePx = frameSize;
|
||||
this.minAreaPx = frameSize.area() * minAreaFactor;
|
||||
this.maxAreaPx = frameSize.area() * maxAreaFactor;
|
||||
}
|
||||
|
||||
public void setMinArea(double minAreaFactor) {
|
||||
this.minAreaPx = maxSizePx.area() * minAreaFactor;
|
||||
}
|
||||
|
||||
public void setMaxArea(double maxAreaFactor) {
|
||||
this.minAreaPx = maxSizePx.area() * maxAreaFactor;
|
||||
}
|
||||
|
||||
// Draw a convex hull around the current detection on the given image
|
||||
public void draw(Mat img, Scalar color) {
|
||||
if (isValid()) {
|
||||
drawConvexHull(img, contour, color);
|
||||
drawPoint(img, centerPx, GREEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a convex hull around the current detection on the given image
|
||||
public void fill(Mat img, Scalar color) {
|
||||
if (isValid()) {
|
||||
fillConvexHull(img, contour, color);
|
||||
drawPoint(img, centerPx, GREEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the current Detection is valid
|
||||
public boolean isValid() {
|
||||
return (this.contour != null) && (this.areaPx != INVALID_AREA);
|
||||
}
|
||||
|
||||
// Get the current contour
|
||||
public MatOfPoint getContour() {
|
||||
return contour;
|
||||
}
|
||||
|
||||
// Set the values of the current contour
|
||||
public void setContour(MatOfPoint contour) {
|
||||
this.contour = contour;
|
||||
|
||||
double area;
|
||||
if (contour != null && (area = Imgproc.contourArea(contour)) > minAreaPx && area < maxAreaPx) {
|
||||
this.areaPx = area;
|
||||
this.centerPx = getCenterOfContour(contour);
|
||||
this.bottomLeftPx = getBottomLeftOfContour(contour);
|
||||
this.bottomRightPx = getBottomRightOfContour(contour);
|
||||
} else {
|
||||
this.areaPx = INVALID_AREA;
|
||||
this.centerPx = INVALID_POINT;
|
||||
this.bottomLeftPx = INVALID_POINT;
|
||||
this.bottomRightPx = INVALID_POINT;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the center of the Detection, normalized so that the width and height of the frame is from [-50,50]
|
||||
public Point getCenter() {
|
||||
if (!isValid()) {
|
||||
return INVALID_POINT;
|
||||
}
|
||||
|
||||
double normalizedX = ((centerPx.x / maxSizePx.width) * 100) - 50;
|
||||
double normalizedY = ((centerPx.y / maxSizePx.height) * -100) + 50;
|
||||
|
||||
return new Point(normalizedX, normalizedY);
|
||||
}
|
||||
|
||||
// Get the center point in pixels
|
||||
public Point getCenterPx() {
|
||||
return centerPx;
|
||||
}
|
||||
|
||||
// Get the area of the Detection, normalized so that the area of the frame is 100
|
||||
public double getArea() {
|
||||
if (!isValid()) {
|
||||
return INVALID_AREA;
|
||||
}
|
||||
|
||||
return (areaPx / (maxSizePx.width * maxSizePx.height)) * 100;
|
||||
}
|
||||
|
||||
// Get the leftmost bottom corner of the detection
|
||||
public Point getBottomLeftCornerPx() {
|
||||
return bottomLeftPx;
|
||||
}
|
||||
|
||||
// Get the rightmost bottom corner of the detection
|
||||
public Point getBottomRightCornerPx() {
|
||||
return bottomRightPx;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package com.tearabite.ftctearabits.vision;
|
||||
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.MatOfInt;
|
||||
import org.opencv.core.MatOfPoint;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.core.Rect;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
import org.opencv.imgproc.Moments;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
// CV Helper Functions
|
||||
public class OpenCVUtil {
|
||||
|
||||
public static String telem = "nothing";
|
||||
|
||||
// Draw a point
|
||||
public static void drawPoint(Mat img, Point point, Scalar color) {
|
||||
Imgproc.circle(img, point, 3, color, -1);
|
||||
}
|
||||
|
||||
// Get the center of a contour
|
||||
public static Point getCenterOfContour(MatOfPoint contour) {
|
||||
Moments moments = Imgproc.moments(contour);
|
||||
return new Point(moments.m10 / moments.m00, moments.m01/ moments.m00);
|
||||
}
|
||||
|
||||
// Get the bottom left of a contour
|
||||
public static Point getBottomLeftOfContour(MatOfPoint contour) {
|
||||
Rect boundingRect = Imgproc.boundingRect(contour);
|
||||
return new Point(boundingRect.x, boundingRect.y+boundingRect.height);
|
||||
}
|
||||
|
||||
// Get the bottom right of a contour
|
||||
public static Point getBottomRightOfContour(MatOfPoint contour) {
|
||||
Rect boundingRect = Imgproc.boundingRect(contour);
|
||||
return new Point(boundingRect.x+boundingRect.width, boundingRect.y+boundingRect.height);
|
||||
}
|
||||
|
||||
// Draw a contour
|
||||
public static void drawContour(Mat img, MatOfPoint contour, Scalar color) {
|
||||
Imgproc.drawContours(img, Collections.singletonList(contour), 0, color, 2);
|
||||
}
|
||||
|
||||
// Draw a convex hull around a contour
|
||||
public static void drawConvexHull(Mat img, MatOfPoint contour, Scalar color) {
|
||||
MatOfInt hull = new MatOfInt();
|
||||
Imgproc.convexHull(contour, hull);
|
||||
Imgproc.drawContours(img, Collections.singletonList(convertIndexesToPoints(contour, hull)), 0, color, 2);
|
||||
}
|
||||
|
||||
// Draw a filled in convex hull around a contour
|
||||
public static void fillConvexHull(Mat img, MatOfPoint contour, Scalar color) {
|
||||
MatOfInt hull = new MatOfInt();
|
||||
Imgproc.convexHull(contour, hull);
|
||||
Imgproc.drawContours(img, Collections.singletonList(convertIndexesToPoints(contour, hull)), 0, color, -1);
|
||||
}
|
||||
|
||||
// Convert indexes to points that is used in order to draw the contours
|
||||
public static MatOfPoint convertIndexesToPoints(MatOfPoint contour, MatOfInt indexes) {
|
||||
int[] arrIndex = indexes.toArray();
|
||||
Point[] arrContour = contour.toArray();
|
||||
Point[] arrPoints = new Point[arrIndex.length];
|
||||
|
||||
for (int i=0;i<arrIndex.length;i++) {
|
||||
arrPoints[i] = arrContour[arrIndex[i]];
|
||||
}
|
||||
|
||||
MatOfPoint hull = new MatOfPoint();
|
||||
hull.fromArray(arrPoints);
|
||||
return hull;
|
||||
}
|
||||
|
||||
// Get the largest contour out of a list
|
||||
public static MatOfPoint getLargestContour(List<MatOfPoint> contours) {
|
||||
if (contours.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
return getLargestContours(contours, 1).get(0);
|
||||
}
|
||||
|
||||
// Get the top largest contours
|
||||
public static List<MatOfPoint> getLargestContours(List<MatOfPoint> contours, int numContours) {
|
||||
Collections.sort(contours, (a, b) -> (int) Imgproc.contourArea(b) - (int) Imgproc.contourArea(a));
|
||||
return contours.subList(0, Math.min(numContours, contours.size()));
|
||||
}
|
||||
|
||||
public static class LinePaint extends Paint
|
||||
{
|
||||
public LinePaint(int color)
|
||||
{
|
||||
setColor(color);
|
||||
setAntiAlias(true);
|
||||
setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.tearabite.ftctearabits.vision;
|
||||
|
||||
import org.opencv.core.Scalar;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class ScalarRange {
|
||||
private Scalar upper;
|
||||
private Scalar lower;
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package com.tearabite.ftctearabits.localization;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import com.acmerobotics.roadrunner.Pose2d;
|
||||
|
||||
import org.firstinspires.ftc.robotcore.external.matrices.VectorF;
|
||||
import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit;
|
||||
import org.firstinspires.ftc.robotcore.external.navigation.Quaternion;
|
||||
import org.firstinspires.ftc.vision.apriltag.AprilTagDetection;
|
||||
import org.firstinspires.ftc.vision.apriltag.AprilTagMetadata;
|
||||
import org.firstinspires.ftc.vision.apriltag.AprilTagPoseFtc;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
class AprilTagPoseEstimatorTest {
|
||||
|
||||
@Test
|
||||
public void estimatePose_null_throws() {
|
||||
AprilTagPoseEstimator estimator = new AprilTagPoseEstimator();
|
||||
assertThrows(InvalidParameterException.class, () -> estimator.estimatePose(null));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideEstimatePosTestValues")
|
||||
public void estimatePos_notNull_returnsPose(AprilTagMetadata metadata, AprilTagPoseFtc poseFtc, Pose2d expectedPose) {
|
||||
AprilTagPoseEstimator estimator = new AprilTagPoseEstimator(new Pose2d(-7.77, 0.505, 0));
|
||||
AprilTagDetection detection = new AprilTagDetection(1, 0, 0, null, null, metadata, poseFtc, null, 0);
|
||||
|
||||
Pose2d estimatedPose = estimator.estimatePose(detection);
|
||||
|
||||
assertIsClose(estimatedPose, expectedPose);
|
||||
}
|
||||
|
||||
private boolean isClose(double a, double b) {
|
||||
return Math.abs(a - b) < 0.1;
|
||||
}
|
||||
|
||||
private void assertIsClose(Pose2d a, Pose2d b) {
|
||||
boolean isClose = isClose(a.position.x, b.position.x)
|
||||
&& isClose(a.position.y, b.position.y)
|
||||
&& isClose(a.heading.toDouble(), b.heading.toDouble());
|
||||
|
||||
if (!isClose) {
|
||||
fail(String.format("Expected (%.1f, %.1f, %.1f) to be close to (%.1f, %.1f, %.1f)",
|
||||
a.position.x, a.position.y, a.heading.toDouble(),
|
||||
b.position.x, b.position.y, b.heading.toDouble()));
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> provideEstimatePosTestValues() {
|
||||
final AprilTagMetadata metadata = new AprilTagMetadata(
|
||||
2,
|
||||
"testTag",
|
||||
0,
|
||||
new VectorF(60.25f, 35.41f, 4f), DistanceUnit.INCH,
|
||||
new Quaternion(0.3536f, -0.6124f, 0.6124f, -0.3536f, 0));
|
||||
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
metadata,
|
||||
new AprilTagPoseFtc(0, 0, 0, 0, 0, 0, 24, 0, 0),
|
||||
new Pose2d(28.5, 35.9, 0)),
|
||||
Arguments.of(
|
||||
metadata,
|
||||
new AprilTagPoseFtc(0, 0, 0, 0, 0, 0, 24, -45, 0),
|
||||
new Pose2d(35.5, 18.9, 0)),
|
||||
Arguments.of(
|
||||
metadata,
|
||||
new AprilTagPoseFtc(0, 0, 0, -45, 0, 0, 24, -45, 0),
|
||||
new Pose2d(31.1, 41.3, -1))
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue