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}