Compare commits

...

7 Commits

Author SHA1 Message Date
Matt
72717cecf0 Disable Roborio finder (#450)
Rio finder has been linked to weird crashes after Autonomous
2022-03-31 22:55:51 -04:00
Matt
971ff3ac40 Calculate aspect ratio using rotated rect (#447) 2022-03-31 22:51:14 -04:00
Banks T
b80e436f02 Force fs sync on all .json writes (#451) 2022-03-31 22:46:12 -04:00
Matt
be1a053cbe Fix PhotoVersion template typo (#446) 2022-03-16 21:39:02 -07:00
Matt
f4555dc545 Fix offset point bug (#445)
Fixes bug where offset point can be wrong
2022-03-16 21:38:47 -07:00
Matt
54fdd1db51 Add test mode from path (#440)
adds --path to --test-mode
2022-03-16 21:33:20 -07:00
Matt
1805785cc6 Rio discovery slowdown (#444)
* Only send rio IPs on settings button click

* Wpiformat
2022-03-14 20:44:14 -07:00
15 changed files with 284 additions and 109 deletions

View File

@@ -19,6 +19,8 @@ export default new Vuex.Store({
connected: false,
address: "",
clients: 0,
},
networkInfo: {
possibleRios: ["Loading..."],
deviceips: ["Loading..."],
},
@@ -155,6 +157,7 @@ export default new Vuex.Store({
calibrationData: set('calibrationData'),
metrics: set('metrics'),
ntConnectionInfo: set('ntConnectionInfo'),
networkInfo: set('networkInfo'),
backendConnected: set('backendConnected'),
logString: (state, newStr) => {
const str = state.logMessages;

View File

@@ -18,6 +18,14 @@
step="0.1"
@input="handlePipelineData('contourRatio')"
/>
<CVselect
v-model="contourTargetOrientation"
name="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:list="['Portrait', 'Landscape']"
@input="handlePipelineData('contourTargetOrientation')"
@rollback="e=> rollback('contourTargetOrientation', e)"
/>
<CVrangeSlider
v-if="currentPipelineType() !== 3"
v-model="contourFullness"
@@ -203,6 +211,14 @@ export default {
this.$store.commit("mutatePipeline", {"contourRatio": val});
}
},
contourTargetOrientation: {
get() {
return this.$store.getters.currentPipelineSettings.contourTargetOrientation
},
set(val) {
this.$store.commit("mutatePipeline", {"contourTargetOrientation": val});
}
},
contourFullness: {
get() {
return this.$store.getters.currentPipelineSettings.contourFullness

View File

@@ -87,7 +87,7 @@
</thead>
<tbody>
<tr
v-for="(value, index) in $store.state.ntConnectionInfo.deviceips"
v-for="(value, index) in $store.state.networkInfo.deviceips"
:key="index"
>
<td>{{ value }}</td>
@@ -115,7 +115,7 @@
</thead>
<tbody>
<tr
v-for="(value, index) in $store.state.ntConnectionInfo.possibleRios"
v-for="(value, index) in $store.state.networkInfo.possibleRios"
:key="index"
>
<td>{{ value }}</td>

View File

@@ -442,4 +442,8 @@ public class ConfigManager {
}
}
}
public void unloadCameraConfigs() {
this.config.getCameraConfigurations().clear();
}
}

View File

@@ -17,20 +17,12 @@
package org.photonvision.common.dataflow.networktables;
import edu.wpi.first.cscore.CameraServerJNI;
import edu.wpi.first.networktables.LogMessage;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableInstance;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
@@ -87,6 +79,7 @@ public class NetworkTablesManager {
private void broadcastConnectedStatusImpl() {
HashMap<String, Object> map = new HashMap<>();
var subMap = new HashMap<String, Object>();
subMap.put("connected", ntInstance.isConnected());
if (ntInstance.isConnected()) {
var connections = getInstance().ntInstance.getConnections();
@@ -99,73 +92,6 @@ public class NetworkTablesManager {
map.put("ntConnectionInfo", subMap);
DataChangeService.getInstance()
.publishEvent(new OutgoingUIEvent<>("networkTablesConnected", map));
// Seperate from the above so we don't hold stuff up
System.setProperty("java.net.preferIPv4Stack", "true");
subMap.put(
"deviceips",
Arrays.stream(CameraServerJNI.getNetworkInterfaces())
.filter(it -> !it.equals("0.0.0.0"))
.toArray());
logger.info("Searching for rios");
List<String> possibleRioList = new ArrayList<>();
for (var ip : CameraServerJNI.getNetworkInterfaces()) {
logger.info("Trying " + ip);
var possibleRioAddr = getPossibleRioAddress(ip);
if (possibleRioAddr != null) {
logger.info("Maybe found " + ip);
searchForHost(possibleRioList, possibleRioAddr);
} else {
logger.info("Didn't match RIO IP");
}
}
String name =
"roboRIO-"
+ ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
+ "-FRC.local";
searchForHost(possibleRioList, name);
name =
"roboRIO-"
+ ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
+ "-FRC.lan";
searchForHost(possibleRioList, name);
name =
"roboRIO-"
+ ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
+ "-FRC.frc-field.local";
searchForHost(possibleRioList, name);
subMap.put("possibleRios", possibleRioList.toArray());
DataChangeService.getInstance()
.publishEvent(new OutgoingUIEvent<>("networkTablesConnected", map));
}
String getPossibleRioAddress(String ip) {
try {
InetAddress addr = InetAddress.getByName(ip);
var address = addr.getAddress();
if (address[0] != (byte) (10 & 0xff)) return null;
address[3] = (byte) (2 & 0xff);
return InetAddress.getByAddress(address).getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return null;
}
void searchForHost(List<String> list, String hostname) {
try {
logger.info("Looking up " + hostname);
InetAddress testAddr = InetAddress.getByName(hostname);
logger.info("Pinging " + hostname);
var canContact = testAddr.isReachable(500);
if (canContact) {
logger.info("Was able to connect to " + hostname);
if (!list.contains(hostname)) list.add(hostname);
} else {
logger.info("Unable to reach " + hostname);
}
} catch (IOException ignored) {
}
}
private void broadcastVersion() {

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.networking;
import edu.wpi.first.cscore.CameraServerJNI;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class RoborioFinder {
private static RoborioFinder instance;
private static final Logger logger = new Logger(RoborioFinder.class, LogGroup.General);
public static RoborioFinder getInstance() {
if (instance == null) instance = new RoborioFinder();
return instance;
}
public void findRios() {
HashMap<String, Object> map = new HashMap<>();
var subMap = new HashMap<String, Object>();
// Seperate from the above so we don't hold stuff up
System.setProperty("java.net.preferIPv4Stack", "true");
subMap.put(
"deviceips",
Arrays.stream(CameraServerJNI.getNetworkInterfaces())
.filter(it -> !it.equals("0.0.0.0"))
.toArray());
logger.info("Searching for rios");
List<String> possibleRioList = new ArrayList<>();
for (var ip : CameraServerJNI.getNetworkInterfaces()) {
logger.info("Trying " + ip);
var possibleRioAddr = getPossibleRioAddress(ip);
if (possibleRioAddr != null) {
logger.info("Maybe found " + ip);
searchForHost(possibleRioList, possibleRioAddr);
} else {
logger.info("Didn't match RIO IP");
}
}
// String name =
// "roboRIO-"
// +
// ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
// + "-FRC.local";
// searchForHost(possibleRioList, name);
// name =
// "roboRIO-"
// +
// ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
// + "-FRC.lan";
// searchForHost(possibleRioList, name);
// name =
// "roboRIO-"
// +
// ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
// + "-FRC.frc-field.local";
// searchForHost(possibleRioList, name);
// subMap.put("possibleRios", possibleRioList.toArray());
subMap.put("possibleRios", possibleRioList.toArray());
map.put("networkInfo", subMap);
DataChangeService.getInstance().publishEvent(new OutgoingUIEvent<>("deviceIpInfo", map));
}
String getPossibleRioAddress(String ip) {
try {
InetAddress addr = InetAddress.getByName(ip);
var address = addr.getAddress();
if (address[0] != (byte) (10 & 0xff)) return null;
address[3] = (byte) (2 & 0xff);
return InetAddress.getByAddress(address).getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return null;
}
void searchForHost(List<String> list, String hostname) {
try {
logger.info("Looking up " + hostname);
InetAddress testAddr = InetAddress.getByName(hostname);
logger.info("Pinging " + hostname);
var canContact = testAddr.isReachable(500);
if (canContact) {
logger.info("Was able to connect to " + hostname);
if (!list.contains(hostname)) list.add(hostname);
} else {
logger.info("Unable to reach " + hostname);
}
} catch (IOException ignored) {
}
}
}

View File

@@ -34,7 +34,7 @@ import java.nio.file.Path;
public class JacksonUtils {
public static <T> void serialize(Path path, T object) throws IOException {
serialize(path, object, false);
serialize(path, object, true);
}
public static <T> void serialize(Path path, T object, boolean forceSync) throws IOException {
@@ -80,7 +80,7 @@ public class JacksonUtils {
public static <T> void serialize(Path path, T object, Class<T> ref, StdSerializer<T> serializer)
throws IOException {
serialize(path, object, ref, serializer, false);
serialize(path, object, ref, serializer, true);
}
public static <T> void serialize(

View File

@@ -18,16 +18,14 @@
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;
import org.photonvision.vision.pipe.CVPipe;
import org.photonvision.vision.target.TargetCalculations;
public class FilterContoursPipe
extends CVPipe<List<Contour>, List<Contour>, FilterContoursPipe.FilterContoursParams> {
@@ -114,8 +112,7 @@ public class FilterContoursPipe
if (contourArea <= minFullness || contourArea >= maxFullness) return;
// Aspect Ratio Filtering.
Rect boundingRect = contour.getBoundingRect();
double aspectRatio = (double) boundingRect.width / boundingRect.height;
double aspectRatio = TargetCalculations.getAspectRatio(contour.getMinAreaRect(), params.isLandscape);
if (aspectRatio < params.getRatio().getFirst() || aspectRatio > params.getRatio().getSecond())
return;
@@ -129,6 +126,7 @@ public class FilterContoursPipe
private final FrameStaticProperties m_frameStaticProperties;
private final double xTol; // IQR tolerance for x
private final double yTol; // IQR tolerance for x
public final boolean isLandscape;
public FilterContoursParams(
DoubleCouple area,
@@ -136,13 +134,14 @@ public class FilterContoursPipe
DoubleCouple extent,
FrameStaticProperties camProperties,
double xTol,
double yTol) {
double yTol, boolean isLandscape) {
this.m_area = area;
this.m_ratio = ratio;
this.m_fullness = extent;
this.m_frameStaticProperties = camProperties;
this.xTol = xTol;
this.yTol = yTol;
this.isLandscape = isLandscape;
}
public DoubleCouple getArea() {

View File

@@ -30,6 +30,7 @@ import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
import org.photonvision.vision.pipe.impl.*;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.PotentialTarget;
import org.photonvision.vision.target.TargetOrientation;
import org.photonvision.vision.target.TrackedTarget;
/** Represents a pipeline for tracking retro-reflective targets. */
@@ -102,7 +103,8 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
settings.contourFullness,
frameStaticProperties,
settings.contourFilterRangeX,
settings.contourFilterRangeY);
settings.contourFilterRangeY,
settings.contourTargetOrientation == TargetOrientation.Landscape);
filterContoursPipe.setParams(filterContoursParams);
var groupContoursParams =

View File

@@ -54,26 +54,27 @@ public class TargetCalculations {
minAreaRect.points(vertices);
Point bl = getMiddle(vertices[0], vertices[1]);
Point tl = getMiddle(vertices[1], vertices[2]);
Point tr = getMiddle(vertices[2], vertices[3]);
Point br = getMiddle(vertices[3], vertices[0]);
boolean orientation;
if (isLandscape) {
orientation = minAreaRect.size.width > minAreaRect.size.height;
} else {
orientation = minAreaRect.size.width < minAreaRect.size.height;
}
Point bottom = getMiddle(vertices[0], vertices[1]);
Point left = getMiddle(vertices[1], vertices[2]);
Point top = getMiddle(vertices[2], vertices[3]);
Point right = getMiddle(vertices[3], vertices[0]);
boolean orientationCorrect = minAreaRect.size.width > minAreaRect.size.height;
if (!isLandscape) orientationCorrect = !orientationCorrect;
switch (offsetRegion) {
case Top:
return orientation ? tl : tr;
if (orientationCorrect) return (left.y < right.y) ? left : right;
else return (top.y < bottom.y) ? top : bottom;
case Bottom:
return orientation ? br : bl;
if (orientationCorrect) return (left.y > right.y) ? left : right;
else return (top.y > bottom.y) ? top : bottom;
case Left:
return orientation ? bl : tl;
if (orientationCorrect) return (top.x < bottom.x) ? top : bottom;
else return (left.x < right.x) ? left : right;
case Right:
return orientation ? tr : br;
if (orientationCorrect) return (top.x > bottom.x) ? top : bottom;
else return (left.x > right.x) ? left : right;
default:
return minAreaRect.center;
}
@@ -110,6 +111,23 @@ public class TargetCalculations {
}
}
public static double getAspectRatio(RotatedRect rect, boolean isLandscape) {
if (rect.size.width == 0 || rect.size.height == 0) return 0;
double ratio = rect.size.width / rect.size.height;
// In landscape, we should be shorter than we are wide (that is, aspect ratio should be >1)
if (isLandscape && ratio < 1) {
ratio = 1.0 / ratio;
}
// If portrait, should always be taller than wide (ratio < 1)
else if (!isLandscape && ratio > 1) {
ratio = 1.0 / ratio;
}
return ratio;
}
public static Point calculateDualOffsetCrosshair(
DualOffsetValues dualOffsetValues, double currentArea) {
boolean firstLarger = dualOffsetValues.firstPointArea >= dualOffsetValues.secondPointArea;

View File

@@ -92,10 +92,21 @@ public class TargetCalculationsTest {
Size rectSize = new Size(10, 5);
double angle = 30;
RotatedRect rect = new RotatedRect(center, rectSize, angle);
Point result =
TargetCalculations.calculateTargetOffsetPoint(false, TargetOffsetPointEdge.Top, rect);
assertEquals(4.3, result.x, 0.33, "Target offset x not as expected");
assertEquals(2.5, result.y, 0.05, "Target offset Y not as expected");
// We pretend like x/y are in pixels, so the "top" is actually the bottom
var result =
TargetCalculations.calculateTargetOffsetPoint(true, TargetOffsetPointEdge.Top, rect);
assertEquals(1.25, result.x, 0.1, "Target offset x not as expected");
assertEquals(-2.17, result.y, 0.1, "Target offset Y not as expected");
result =
TargetCalculations.calculateTargetOffsetPoint(true, TargetOffsetPointEdge.Bottom, rect);
assertEquals(-1.25, result.x, 0.1, "Target offset x not as expected");
assertEquals(2.17, result.y, 0.1, "Target offset Y not as expected");
}
public static void main(String[] args) {
TestUtils.loadLibraries();
new TargetCalculationsTest().targetOffsetTest();
}
@Test

View File

@@ -18,7 +18,13 @@
package org.photonvision;
import edu.wpi.first.cscore.CameraServerCvJNI;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.cli.*;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
@@ -54,6 +60,7 @@ public class Main {
private static final boolean isRelease = PhotonVersion.isRelease;
private static boolean isTestMode;
private static Path testModeFolder = null;
private static boolean printDebugLogs;
private static boolean handleArgs(String[] args) throws ParseException {
@@ -66,6 +73,8 @@ public class Main {
false,
"Run in test mode with 2019 and 2020 WPI field images in place of cameras");
options.addOption("p", "path", true, "Point test mode to a specific folder");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
@@ -82,11 +91,70 @@ public class Main {
if (cmd.hasOption("test-mode")) {
isTestMode = true;
logger.info("Running in test mode - Cameras will not be used");
if (cmd.hasOption("path")) {
Path p = Path.of(cmd.getOptionValue("path"));
logger.info("Loading from Path " + p.toAbsolutePath().toString());
testModeFolder = p;
}
}
}
return true;
}
private static void addTestModeFromFolder() {
ConfigManager.getInstance().load();
try {
var reflective = new ReflectivePipelineSettings();
var shape = new ColoredShapePipelineSettings();
List<VisionSource> collectedSources =
Files.list(testModeFolder)
.filter(p -> p.toFile().isFile())
.map(
p -> {
try {
var camConf =
ConfigManager.getInstance()
.getConfig()
.getCameraConfigurations()
.get(p.getFileName().toString());
if (camConf == null) {
camConf =
new CameraConfiguration(
p.getFileName().toString(), p.toAbsolutePath().toString());
camConf.FOV = TestUtils.WPI2019Image.FOV; // Good guess?
var pipeSettings = new ReflectivePipelineSettings();
pipeSettings.pipelineNickname = p.getFileName().toString();
pipeSettings.outputShowMultipleTargets = true;
pipeSettings.inputShouldShow = true;
pipeSettings.outputShouldShow = true;
var psList = new ArrayList<CVPipelineSettings>();
psList.add(reflective);
psList.add(shape);
camConf.pipelineSettings = psList;
}
return new FileVisionSource(camConf);
} catch (Exception e) {
logger.error("Couldn't load image " + p.getFileName().toString());
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
ConfigManager.getInstance().unloadCameraConfigs();
VisionModuleManager.getInstance().addSources(collectedSources).forEach(VisionModule::start);
ConfigManager.getInstance().addCameraConfigurations(collectedSources);
} catch (IOException e) {
logger.error("Path does not exist!");
System.exit(1);
}
}
private static void addTestModeSources() {
ConfigManager.getInstance().load();
@@ -182,13 +250,16 @@ public class Main {
collectedSources.add(fvs2020);
collectedSources.add(fvs2019);
ConfigManager.getInstance().unloadCameraConfigs();
VisionModuleManager.getInstance().addSources(collectedSources).forEach(VisionModule::start);
ConfigManager.getInstance().addCameraConfigurations(collectedSources);
}
public static void main(String[] args) {
try {
if (!handleArgs(args)) return;
if (!handleArgs(args)) {
System.exit(0);
}
} catch (ParseException e) {
logger.error("Failed to parse command-line options!", e);
}
@@ -235,9 +306,14 @@ public class Main {
VisionSourceManager.getInstance()
.registerLoadedConfigs(
ConfigManager.getInstance().getConfig().getCameraConfigurations().values());
VisionSourceManager.getInstance().registerTimedTask();
} else {
addTestModeSources();
if (testModeFolder == null) {
addTestModeSources();
} else {
addTestModeFromFolder();
}
}
Server.main(DEFAULT_WEBPORT);

View File

@@ -280,6 +280,8 @@ public class RequestHandler {
public static void sendMetrics(Context ctx) {
MetricsPublisher.getInstance().publish();
// TimedTaskManager.getInstance().addOneShotTask(() -> RoborioFinder.getInstance().findRios(),
// 0);
ctx.status(200);
}

View File

@@ -25,8 +25,8 @@
namespace photonlib {
namespace PhotonVersion {
const std::string versionString = "dev-v2022.1.4-2-ga22f8af0";
const std::string buildDate = "2022-1-20 10:10:04";
const std::string versionString = "${version}";
const std::string buildDate = "${date}";
const bool isRelease = !(versionString.rfind("dev", 0) == 0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB