diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/FileVisionSource.java b/photon-core/src/main/java/org/photonvision/vision/camera/FileVisionSource.java index 76737fbf20..d2ea8fd2d8 100644 --- a/photon-core/src/main/java/org/photonvision/vision/camera/FileVisionSource.java +++ b/photon-core/src/main/java/org/photonvision/vision/camera/FileVisionSource.java @@ -23,14 +23,19 @@ import java.nio.file.Path; import java.util.HashMap; import org.photonvision.common.configuration.CameraConfiguration; +import org.photonvision.common.logging.LogGroup; +import org.photonvision.common.logging.Logger; import org.photonvision.vision.frame.FrameProvider; import org.photonvision.vision.frame.FrameStaticProperties; import org.photonvision.vision.frame.provider.FileFrameProvider; +import org.photonvision.vision.frame.provider.VideoFrameProvider; import org.photonvision.vision.processes.VisionSource; import org.photonvision.vision.processes.VisionSourceSettables; public class FileVisionSource extends VisionSource { - private final FileFrameProvider frameProvider; + private static final Logger logger = new Logger(FileVisionSource.class, LogGroup.Camera); + + private final FrameProvider frameProvider; private final FileSourceSettables settables; public FileVisionSource(CameraConfiguration cameraConfiguration) { @@ -39,13 +44,20 @@ public FileVisionSource(CameraConfiguration cameraConfiguration) { !cameraConfiguration.calibrations.isEmpty() ? cameraConfiguration.calibrations.get(0) : null; - frameProvider = - new FileFrameProvider( - // TODO - create new File/replay camera info type - Path.of(cameraConfiguration.getDevicePath()), - cameraConfiguration.FOV, - FileFrameProvider.MAX_FPS, - calibration); + + var path = Path.of(cameraConfiguration.getDevicePath()); + + if (path.endsWith(".png") || path.endsWith(".jpg") || path.endsWith(".jpeg")) { + logger.info("Using image file: " + path.toAbsolutePath()); + + frameProvider = + new FileFrameProvider( + // TODO - create new File/replay camera info type + path, cameraConfiguration.FOV, FileFrameProvider.MAX_FPS, calibration); + } else { + logger.info("Looks like a video file, using as replay: " + path.toAbsolutePath()); + frameProvider = new VideoFrameProvider(path, cameraConfiguration.FOV, calibration); + } if (getCameraConfiguration().cameraQuirks == null) getCameraConfiguration().cameraQuirks = QuirkyCamera.DefaultCamera; diff --git a/photon-core/src/main/java/org/photonvision/vision/frame/provider/FileFrameProvider.java b/photon-core/src/main/java/org/photonvision/vision/frame/provider/FileFrameProvider.java index 2b281b517f..00c82c149c 100644 --- a/photon-core/src/main/java/org/photonvision/vision/frame/provider/FileFrameProvider.java +++ b/photon-core/src/main/java/org/photonvision/vision/frame/provider/FileFrameProvider.java @@ -27,13 +27,12 @@ import org.photonvision.vision.frame.FrameProvider; import org.photonvision.vision.frame.FrameStaticProperties; import org.photonvision.vision.opencv.CVMat; -import org.photonvision.vision.opencv.Releasable; /** * A {@link FrameProvider} that will read and provide an image from a {@link java.nio.file.Path * path}. */ -public class FileFrameProvider extends CpuImageProcessor implements Releasable { +public class FileFrameProvider extends CpuImageProcessor { public static final int MAX_FPS = 10; private static int count = 0; diff --git a/photon-core/src/main/java/org/photonvision/vision/frame/provider/VideoFrameProvider.java b/photon-core/src/main/java/org/photonvision/vision/frame/provider/VideoFrameProvider.java new file mode 100644 index 0000000000..9f7316ab86 --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/vision/frame/provider/VideoFrameProvider.java @@ -0,0 +1,129 @@ +/* + * 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 . + */ + +package org.photonvision.vision.frame.provider; + +import java.nio.file.Path; +import org.opencv.videoio.VideoCapture; +import org.opencv.videoio.Videoio; +import org.photonvision.common.logging.LogGroup; +import org.photonvision.common.logging.Logger; +import org.photonvision.common.util.math.MathUtils; +import org.photonvision.vision.calibration.CameraCalibrationCoefficients; +import org.photonvision.vision.frame.FrameProvider; +import org.photonvision.vision.frame.FrameStaticProperties; +import org.photonvision.vision.opencv.CVMat; + +/** + * A {@link FrameProvider} that will read and provide an image from a {@link java.nio.file.Path + * path}. + */ +public class VideoFrameProvider extends CpuImageProcessor { + private static final Logger logger = new Logger(VideoFrameProvider.class, LogGroup.Camera); + + private final FrameStaticProperties properties; + private Path path; + private VideoCapture reader; + + private long lastGetMillis = System.currentTimeMillis(); + private final int millisDelay; + + public VideoFrameProvider(Path path, double fov, CameraCalibrationCoefficients calibration) { + this.path = path; + + String absPath = path.toAbsolutePath().toString(); + this.reader = new VideoCapture(absPath); + + if (!reader.isOpened()) { + logger.error("Failed to open video file: " + absPath); + throw new IllegalArgumentException("Cannot open video file: " + absPath); + } + + logger.info( + "Opened video file: " + + path.toAbsolutePath() + + " using backend " + + reader.getBackendName()); + + // Get FPS + var fps = reader.get(Videoio.CAP_PROP_FPS); + if (fps <= 0) { + logger.warn("Could not determine FPS, defaulting to 30"); + } + this.millisDelay = (int) (1000 / fps); + + // Figure out resolution of the video file + var rawImage = new CVMat(); + reader.read(rawImage.getMat()); + this.properties = + new FrameStaticProperties( + rawImage.getMat().width(), rawImage.getMat().height(), fov, calibration); + rawImage.release(); + } + + @Override + public CapturedFrame getInputMat() { + // sleep to match fps + if (System.currentTimeMillis() - lastGetMillis < millisDelay) { + try { + Thread.sleep(millisDelay); + } catch (InterruptedException e) { + System.err.println("FileFrameProvider interrupted - not busywaiting"); + // throw back up the stack + throw new RuntimeException(e); + } + } + lastGetMillis = System.currentTimeMillis(); + + var out = new CVMat(); + + boolean read = reader.read(out.getMat()); + if (!read) { + // loop + logger.info("Rewinding video file for next tick: " + path.toAbsolutePath()); + reader.release(); + reader = new VideoCapture(path.toAbsolutePath().toString()); + } + + return new CapturedFrame(out, properties, MathUtils.wpiNanoTime()); + } + + @Override + public String getName() { + return "FileFrameProvider-" + this.path; + } + + @Override + public void release() { + reader.release(); + } + + @Override + public boolean checkCameraConnected() { + return true; + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean hasConnected() { + return true; + } +} diff --git a/photon-server/src/main/java/org/photonvision/Main.java b/photon-server/src/main/java/org/photonvision/Main.java index 5d012d23fd..887ee5aac9 100644 --- a/photon-server/src/main/java/org/photonvision/Main.java +++ b/photon-server/src/main/java/org/photonvision/Main.java @@ -59,7 +59,6 @@ public class Main { private static boolean isTestMode = false; private static boolean isSmoketest = false; - private static Path testModeFolder = null; private static boolean printDebugLogs; private static boolean handleArgs(String[] args) throws ParseException { @@ -72,7 +71,6 @@ private static boolean handleArgs(String[] args) throws ParseException { 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"); options.addOption("n", "disable-networking", false, "Disables control device network settings"); options.addOption( "c", @@ -101,12 +99,6 @@ private static boolean handleArgs(String[] args) throws ParseException { 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(System.getProperty("PATH_PREFIX", "") + cmd.getOptionValue("path")); - logger.info("Loading from Path " + p.toAbsolutePath().toString()); - testModeFolder = p; - } } if (cmd.hasOption("disable-networking")) { @@ -127,34 +119,27 @@ private static boolean handleArgs(String[] args) throws ParseException { private static void addTestModeSources() { ConfigManager.getInstance().load(); - CameraConfiguration camConf2024 = - ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2024"); - if (camConf2024 == null || true) { - camConf2024 = - new CameraConfiguration( - PVCameraInfo.fromFileInfo( - TestUtils.getResourcesFolderPath(true) - .resolve("testimages") - .resolve(TestUtils.WPI2024Images.kSpeakerCenter_143in.path) - .toString(), - "WPI2024")); - - camConf2024.FOV = TestUtils.WPI2024Images.FOV; - // same camera as 2023 - camConf2024.calibrations.add(TestUtils.get2023LifeCamCoeffs(true)); - - var pipeline2024 = new AprilTagPipelineSettings(); - var path_split = Path.of(camConf2024.matchedCameraInfo.path()).getFileName().toString(); - pipeline2024.pipelineNickname = path_split.replace(".jpg", ""); - pipeline2024.targetModel = TargetModel.kAprilTag6p5in_36h11; - pipeline2024.tagFamily = AprilTagFamily.kTag36h11; - pipeline2024.inputShouldShow = true; - pipeline2024.solvePNPEnabled = true; - - var psList2024 = new ArrayList(); - psList2024.add(pipeline2024); - camConf2024.pipelineSettings = psList2024; - } + var camConf2024 = + new CameraConfiguration( + PVCameraInfo.fromFileInfo( + // "C:\\Users\\Matt\\Documents\\GitHub\\photonvision\\test-video\\poseest_demo.mp4", + "C:\\Users\\Matt\\Videos\\Captures\\inception.mp4", "foobar")); + + camConf2024.FOV = TestUtils.WPI2024Images.FOV; + // same camera as 2023 + camConf2024.calibrations.add(TestUtils.get2023LifeCamCoeffs(true)); + + var pipeline2024 = new AprilTagPipelineSettings(); + var path_split = Path.of(camConf2024.matchedCameraInfo.path()).getFileName().toString(); + pipeline2024.pipelineNickname = path_split.replace(".jpg", ""); + pipeline2024.targetModel = TargetModel.kAprilTag6p5in_36h11; + pipeline2024.tagFamily = AprilTagFamily.kTag36h11; + pipeline2024.inputShouldShow = true; + pipeline2024.solvePNPEnabled = true; + + var psList2024 = new ArrayList(); + psList2024.add(pipeline2024); + camConf2024.pipelineSettings = psList2024; var cameraConfigs = List.of(camConf2024); @@ -306,9 +291,7 @@ public static void main(String[] args) { .registerLoadedConfigs( ConfigManager.getInstance().getConfig().getCameraConfigurations().values()); } else { - if (testModeFolder == null) { - addTestModeSources(); - } + addTestModeSources(); } VisionSourceManager.getInstance().registerTimedTasks();