001/* 002 * Copyright (C) Photon Vision. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program. If not, see <https://www.gnu.org/licenses/>. 016 */ 017 018package org.photonvision.estimation; 019 020import edu.wpi.first.math.geometry.Pose3d; 021import edu.wpi.first.math.geometry.Rotation2d; 022import edu.wpi.first.math.geometry.Rotation3d; 023import edu.wpi.first.math.geometry.Translation3d; 024import edu.wpi.first.math.util.Units; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.stream.Collectors; 028 029/** Describes the 3d model of a target. */ 030public class TargetModel { 031 /** 032 * Translations of this target's vertices relative to its pose. Rectangular and spherical targets 033 * will have four vertices. See their respective constructors for more info. 034 */ 035 public final List<Translation3d> vertices; 036 037 /** True if the target is planar, like an AprilTag. False otherwise. */ 038 public final boolean isPlanar; 039 040 /** True if the target is spherical, like a ball. False otherwise. */ 041 public final boolean isSpherical; 042 043 /** The model of AprilTags in the 16h5 family used by <i>FIRST</i> in FRC 2023. */ 044 public static final TargetModel kAprilTag16h5 = 045 new TargetModel(Units.inchesToMeters(6), Units.inchesToMeters(6)); 046 047 /** 048 * The model of AprilTags in the 36h11 family used by <i>FIRST</i> in FRC 2024 and later years. 049 */ 050 public static final TargetModel kAprilTag36h11 = 051 new TargetModel(Units.inchesToMeters(6.5), Units.inchesToMeters(6.5)); 052 053 /** 054 * Creates a rectangular, planar target model given the width and height. The model has four 055 * vertices: 056 * 057 * <ul> 058 * <li>Point 0: [0, -width/2, -height/2] 059 * <li>Point 1: [0, width/2, -height/2] 060 * <li>Point 2: [0, width/2, height/2] 061 * <li>Point 3: [0, -width/2, height/2] 062 * </ul> 063 */ 064 public TargetModel(double widthMeters, double heightMeters) { 065 this.vertices = 066 List.of( 067 // this order is relevant for AprilTag compatibility 068 new Translation3d(0, -widthMeters / 2.0, -heightMeters / 2.0), 069 new Translation3d(0, widthMeters / 2.0, -heightMeters / 2.0), 070 new Translation3d(0, widthMeters / 2.0, heightMeters / 2.0), 071 new Translation3d(0, -widthMeters / 2.0, heightMeters / 2.0)); 072 this.isPlanar = true; 073 this.isSpherical = false; 074 } 075 076 /** 077 * Creates a cuboid target model given the length, width, height. The model has eight vertices: 078 * 079 * <ul> 080 * <li>Point 0: [length/2, -width/2, -height/2] 081 * <li>Point 1: [length/2, width/2, -height/2] 082 * <li>Point 2: [length/2, width/2, height/2] 083 * <li>Point 3: [length/2, -width/2, height/2] 084 * <li>Point 4: [-length/2, -width/2, height/2] 085 * <li>Point 5: [-length/2, width/2, height/2] 086 * <li>Point 6: [-length/2, width/2, -height/2] 087 * <li>Point 7: [-length/2, -width/2, -height/2] 088 * </ul> 089 */ 090 public TargetModel(double lengthMeters, double widthMeters, double heightMeters) { 091 this( 092 List.of( 093 new Translation3d(lengthMeters / 2.0, -widthMeters / 2.0, -heightMeters / 2.0), 094 new Translation3d(lengthMeters / 2.0, widthMeters / 2.0, -heightMeters / 2.0), 095 new Translation3d(lengthMeters / 2.0, widthMeters / 2.0, heightMeters / 2.0), 096 new Translation3d(lengthMeters / 2.0, -widthMeters / 2.0, heightMeters / 2.0), 097 new Translation3d(-lengthMeters / 2.0, -widthMeters / 2.0, heightMeters / 2.0), 098 new Translation3d(-lengthMeters / 2.0, widthMeters / 2.0, heightMeters / 2.0), 099 new Translation3d(-lengthMeters / 2.0, widthMeters / 2.0, -heightMeters / 2.0), 100 new Translation3d(-lengthMeters / 2.0, -widthMeters / 2.0, -heightMeters / 2.0))); 101 } 102 103 /** 104 * Creates a spherical target model which has similar dimensions regardless of its rotation. This 105 * model has four vertices: 106 * 107 * <ul> 108 * <li>Point 0: [0, -radius, 0] 109 * <li>Point 1: [0, 0, -radius] 110 * <li>Point 2: [0, radius, 0] 111 * <li>Point 3: [0, 0, radius] 112 * </ul> 113 * 114 * <i>Q: Why these vertices?</i> A: This target should be oriented to the camera every frame, much 115 * like a sprite/decal, and these vertices represent the ellipse vertices (maxima). These vertices 116 * are used for drawing the image of this sphere, but do not match the corners that will be 117 * published by photonvision. 118 */ 119 public TargetModel(double diameterMeters) { 120 double radius = diameterMeters / 2.0; 121 this.vertices = 122 List.of( 123 new Translation3d(0, -radius, 0), 124 new Translation3d(0, 0, -radius), 125 new Translation3d(0, radius, 0), 126 new Translation3d(0, 0, radius)); 127 this.isPlanar = false; 128 this.isSpherical = true; 129 } 130 131 /** 132 * Creates a target model from arbitrary 3d vertices. Automatically determines if the given 133 * vertices are planar(x == 0). More than 2 vertices must be given. If this is a planar model, the 134 * vertices should define a non-intersecting contour. 135 * 136 * @param vertices Translations representing the vertices of this target model relative to its 137 * pose. 138 */ 139 public TargetModel(List<Translation3d> vertices) { 140 this.isSpherical = false; 141 if (vertices == null || vertices.size() <= 2) { 142 vertices = new ArrayList<>(); 143 this.isPlanar = false; 144 } else { 145 boolean cornersPlanar = true; 146 for (Translation3d corner : vertices) { 147 if (corner.getX() != 0) cornersPlanar = false; 148 } 149 this.isPlanar = cornersPlanar; 150 } 151 this.vertices = vertices; 152 } 153 154 /** 155 * This target's vertices offset from its field pose. 156 * 157 * <p>Note: If this target is spherical, use {@link #getOrientedPose(Translation3d, 158 * Translation3d)} with this method. 159 * 160 * @param targetPose The field pose to offset the vertices by. 161 * @return The list of vertices offset from the target pose. 162 */ 163 public List<Translation3d> getFieldVertices(Pose3d targetPose) { 164 var basisChange = new RotTrlTransform3d(targetPose.getRotation(), targetPose.getTranslation()); 165 return vertices.stream().map(basisChange::apply).collect(Collectors.toList()); 166 } 167 168 /** 169 * Returns a Pose3d with the given target translation oriented (with its relative x-axis aligned) 170 * to the camera translation. This is used for spherical targets which should not have their 171 * projection change regardless of their own rotation. 172 * 173 * @param tgtTrl This target's translation 174 * @param cameraTrl Camera's translation 175 * @return This target's pose oriented to the camera 176 */ 177 public static Pose3d getOrientedPose(Translation3d tgtTrl, Translation3d cameraTrl) { 178 var relCam = cameraTrl.minus(tgtTrl); 179 var orientToCam = 180 new Rotation3d( 181 0, 182 new Rotation2d(Math.hypot(relCam.getX(), relCam.getY()), -relCam.getZ()).getRadians(), 183 new Rotation2d(relCam.getX(), relCam.getY()).getRadians()); 184 return new Pose3d(tgtTrl, orientToCam); 185 } 186 187 @Override 188 public boolean equals(Object obj) { 189 return this == obj 190 && obj instanceof TargetModel o 191 && vertices.equals(o.vertices) 192 && isPlanar == o.isPlanar 193 && isSpherical == o.isSpherical; 194 } 195}