/*
* ShootOFF - Software for Laser Dry Fire Training
* Copyright (C) 2016 phrack
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.shootoff.camera;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import org.opencv.core.CvException;
import org.opencv.core.Mat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.shootoff.ObservableCloseable;
import com.shootoff.camera.autocalibration.AutoCalibrationManager;
import com.shootoff.camera.cameratypes.Camera;
import com.shootoff.camera.cameratypes.Camera.CameraState;
import com.shootoff.camera.cameratypes.CameraEventListener;
import com.shootoff.camera.cameratypes.PS3EyeCamera;
import com.shootoff.camera.cameratypes.SarxosCaptureCamera;
import com.shootoff.camera.processors.DeduplicationProcessor;
import com.shootoff.camera.recorders.RollingRecorder;
import com.shootoff.camera.recorders.ShotRecorder;
import com.shootoff.camera.shot.ShotColor;
import com.shootoff.camera.shotdetection.CameraStateListener;
import com.shootoff.camera.shotdetection.FrameProcessingShotDetector;
import com.shootoff.camera.shotdetection.JavaShotDetector;
import com.shootoff.camera.shotdetection.ShotDetector;
import com.shootoff.camera.shotdetection.ShotYieldingShotDetector;
import com.shootoff.config.Configuration;
import com.shootoff.util.TimerPool;
import com.xuggle.mediatool.IMediaWriter;
import com.xuggle.mediatool.ToolFactory;
import com.xuggle.xuggler.ICodec;
import com.xuggle.xuggler.IPixelFormat;
import com.xuggle.xuggler.IVideoPicture;
import com.xuggle.xuggler.video.ConverterFactory;
import com.xuggle.xuggler.video.IConverter;
import com.shootoff.util.SwingFXUtils;
import javafx.geometry.Bounds;
import javafx.geometry.Dimension2D;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
/**
* This class is responsible for fetching frames from its assigned camera and
* preprocessing them for shot detection. It also ensures the view showing the
* camera frames is aware of any new frames from the camera.
*
* @author phrack and dmaul
*/
public class CameraManager implements ObservableCloseable, CameraEventListener, CameraCalibrationListener {
private static final int MAXIMUM_CONSECUTIVE_CAMERA_ERRORS = 5;
private static final Logger logger = LoggerFactory.getLogger(CameraManager.class);
public static final int DEFAULT_FEED_WIDTH = 640;
public static final int DEFAULT_FEED_HEIGHT = 480;
public static final int MIN_SHOT_DETECTION_FPS = 5;
protected int feedWidth = DEFAULT_FEED_WIDTH;
protected int feedHeight = DEFAULT_FEED_HEIGHT;
protected final static int DIAGNOSTIC_MESSAGE_DURATION = 1000; // ms
protected Optional<CameraDebuggerListener> debuggerListener = Optional.empty();
protected final ShotDetector shotDetector;
private long startTime = 0;
protected final Camera camera;
private final Optional<CameraErrorView> cameraErrorView;
private Optional<CloseListener> closeListener = Optional.empty();
protected final CameraView cameraView;
protected final Configuration config = Configuration.getConfig();
private final Object projectionBoundsLock = new Object();
protected Optional<Bounds> projectionBounds = Optional.empty();
private final AtomicBoolean isStreaming = new AtomicBoolean(true);
private final AtomicBoolean isDetectionLocked = new AtomicBoolean(false);
private final AtomicBoolean isDetecting = new AtomicBoolean(true);
private final AtomicBoolean isCalibrating = new AtomicBoolean(false);
private boolean shownBrightnessWarning = false;
private boolean cropFeedToProjection = false;
private boolean limitDetectProjection = false;
protected Optional<Integer> minimumShotDimension = Optional.empty();
protected boolean recordingStream = false;
protected boolean isFirstStreamFrame = true;
protected IMediaWriter videoWriterStream;
protected long recordingStartTime;
protected boolean recordingShots = false;
protected RollingRecorder rollingRecorder;
protected Map<Shot, ShotRecorder> shotRecorders = new ConcurrentHashMap<>();
protected boolean[][] sectorStatuses;
private boolean showedFPSWarning = false;
protected AutoCalibrationManager acm = null;
private final AtomicBoolean isAutoCalibrating = new AtomicBoolean(false);
protected boolean cameraAutoCalibrated = false;
protected final DeduplicationProcessor deduplicationProcessor = new DeduplicationProcessor(this);
private CameraCalibrationListener cameraCalibrationListener;
public void setCalibrationManager(CameraCalibrationListener calibrationManager) {
cameraCalibrationListener = calibrationManager;
}
public DeduplicationProcessor getDeduplicationProcessor() {
return deduplicationProcessor;
}
public CameraManager() {
camera = null;
cameraErrorView = Optional.empty();
cameraView = null;
shotDetector = null;
}
public CameraManager(Camera cameraInterface, CameraErrorView cameraErrorView, CameraView view) {
camera = cameraInterface;
this.cameraErrorView = Optional.ofNullable(cameraErrorView);
cameraView = view;
cameraView.setCameraManager(this);
camera.setCameraEventListener(this);
shotDetector = camera.getPreferredShotDetector(this, view);
if (shotDetector == null) logger.error("No suitable shot detector found for camera {}", camera.getName());
}
public String getName() {
return camera.getName();
}
public boolean start() {
sectorStatuses = new boolean[JavaShotDetector.SECTOR_ROWS][JavaShotDetector.SECTOR_COLUMNS];
// Turn on all shot sectors by default
for (int x = 0; x < JavaShotDetector.SECTOR_COLUMNS; x++) {
for (int y = 0; y < JavaShotDetector.SECTOR_ROWS; y++) {
sectorStatuses[y][x] = true;
}
}
synchronized (camera) {
if (!camera.isOpen()) {
camera.setViewSize(new Dimension(getFeedWidth(), getFeedHeight()));
if (!camera.open()) return false;
final Dimension openDimension = camera.getViewSize();
if ((int) openDimension.getWidth() != getFeedWidth()
|| (int) openDimension.getHeight() != getFeedHeight()) {
if (openDimension.getWidth() == -1) return false;
if (logger.isWarnEnabled()) {
logger.warn(
"Camera {} dimension differs from requested dimensions, requested {} {} actual {} {}",
getName(), getFeedWidth(), getFeedHeight(), (int) openDimension.getWidth(),
(int) openDimension.getHeight());
}
setFeedResolution((int) openDimension.getWidth(), (int) openDimension.getHeight());
shotDetector.setFrameSize((int) openDimension.getWidth(), (int) openDimension.getHeight());
} else {
setFeedResolution((int) openDimension.getWidth(), (int) openDimension.getHeight());
}
}
if (logger.isDebugEnabled()) logger.debug("starting camera thread {}", camera.getName());
final String threadName = String.format("Camera %s %s", camera.getName(),
shotDetector.getClass().getSimpleName());
new Thread(camera, threadName).start();
}
if (shotDetector instanceof ShotYieldingShotDetector)
((ShotYieldingShotDetector) shotDetector).initDetecting();
setDetecting(true);
return true;
}
public boolean isSectorOn(int x, int y) {
return sectorStatuses[y][x];
}
public void setSectorStatuses(boolean[][] sectorStatuses) {
if (sectorStatuses == null) return;
this.sectorStatuses = new boolean[sectorStatuses.length][sectorStatuses[0].length];
for (int i = 0; i < sectorStatuses.length; i++) {
System.arraycopy(sectorStatuses[i], 0, this.sectorStatuses[i], 0, sectorStatuses[i].length);
}
}
public int getFeedWidth() {
return feedWidth;
}
public int getFeedHeight() {
return feedHeight;
}
// TODO: This doesn't handle potential side effects of modifying the feed
// resolution on the fly.
@Override
public void setFeedResolution(int width, int height) {
feedWidth = width;
feedHeight = height;
shotDetector.setFrameSize(width, height);
}
// Used by click-to-shoot and tests to inject a shot via the shot detector
public void injectShot(ShotColor color, double x, double y, boolean scaleShot) {
shotDetector.addShot(color, x, y, System.currentTimeMillis(), scaleShot);
}
public void clearShots() {
cameraView.clearShots();
}
public void reset() {
resetStartTime(0);
shotDetector.reset();
deduplicationProcessor.reset();
cameraView.reset();
}
private void resetStartTime(long timestamp) {
startTime = timestamp;
}
@Override
public void setOnCloseListener(CloseListener closeListener) {
this.closeListener = Optional.of(closeListener);
}
@Override
public void close() {
getCameraView().close();
setDetecting(false);
setStreaming(false);
setCameraState(CameraState.CLOSED);
camera.setCameraEventListener(null);
if (recordingStream) stopRecordingStream();
TimerPool.cancelTimer(brightnessDiagnosticFuture);
TimerPool.cancelTimer(motionDiagnosticFuture);
if (recordingCalibratedArea) stopRecordingCalibratedArea();
if (closeListener.isPresent()) closeListener.get().closing();
}
public void setStreaming(boolean isStreaming) {
this.isStreaming.set(isStreaming);
}
// Sometimes it is useful to ensure that a camera that isn't detecting
// states not detecting (e.g. to not be turned back on by reset button
public void setDetectionLockState(boolean isLocked) {
isDetectionLocked.set(isLocked);
}
List<CameraStateListener> cameraStateListeners = new ArrayList<>();
public void registerCameraStateListener(CameraStateListener csl) {
cameraStateListeners.add(csl);
}
private void setCameraState(CameraState state) {
if (camera.setState(state)) for (final CameraStateListener csl : cameraStateListeners)
csl.cameraStateChange(state);
}
public void setDetecting(boolean isDetecting) {
if (isDetectionLocked.get()) {
logger.debug("Attempted to set detection for {} to {}, but the detection state is locked.", getName(),
isDetecting);
return;
}
// Lock this to false during calibration
if (isCalibrating.get() && isDetecting) {
logger.info("Not changing detection to true during calibration");
return;
}
if (logger.isTraceEnabled())
logger.trace("setDetecting was {} now {} for {}", this.isDetecting, isDetecting, getName());
if (isDetecting && projectionBounds.isPresent())
setCameraState(CameraState.DETECTING_CALIBRATED);
else if (isDetecting)
setCameraState(CameraState.DETECTING);
else
setCameraState(CameraState.NORMAL);
this.isDetecting.set(isDetecting);
}
public void setCalibrating(final boolean isCalibrating) {
this.isCalibrating.set(isCalibrating);
if (isCalibrating) {
setCameraState(CameraState.CALIBRATING);
setDetecting(false);
}
}
public boolean isDetecting() {
return isDetecting.get();
}
public void setProjectionBounds(final Bounds projectionBounds) {
synchronized (projectionBoundsLock) {
this.projectionBounds = Optional.ofNullable(projectionBounds);
}
}
public void setCropFeedToProjection(final boolean cropFeed) {
cropFeedToProjection = cropFeed;
}
public void setLimitDetectProjection(final boolean limitDetection) {
limitDetectProjection = limitDetection;
}
public boolean isCroppingFeedToProjection() {
return cropFeedToProjection;
}
public boolean isLimitingDetectionToProjection() {
return limitDetectProjection;
}
public Optional<Bounds> getProjectionBounds() {
return projectionBounds;
}
public void startRecordingStream(File videoFile) {
if (logger.isDebugEnabled()) logger.debug("Writing Video Feed To: {}", videoFile.getAbsoluteFile());
videoWriterStream = ToolFactory.makeWriter(videoFile.getName());
videoWriterStream.addVideoStream(0, 0, ICodec.ID.CODEC_ID_H264, getFeedWidth(), getFeedHeight());
recordingStartTime = System.currentTimeMillis();
isFirstStreamFrame = true;
recordingStream = true;
}
public void stopRecordingStream() {
recordingStream = false;
videoWriterStream.close();
}
public void notifyShot(final Shot shot) {
shotRecorders.put(shot, rollingRecorder.fork());
}
public ShotRecorder getRevelantRecorder(Shot shot) {
return shotRecorders.get(shot);
}
public void startRecordingShots() {
String sessionName = null;
if (config.getSessionRecorder().isPresent()) {
sessionName = config.getSessionRecorder().get().getSessionName();
final File sessionVideoFolder = new File(System.getProperty("shootoff.home") + File.separator + "sessions"
+ File.separator + config.getSessionRecorder().get().getSessionName());
if (!sessionVideoFolder.exists() && !sessionVideoFolder.mkdirs()) {
logger.error("Could not create video folder for session: {}", sessionVideoFolder.getAbsolutePath());
}
}
String cameraName = "UNNAMED";
final Optional<String> userCameraName = config.getWebcamsUserName(camera);
if (userCameraName.isPresent()) {
cameraName = userCameraName.get();
} else {
cameraName = camera.getName();
}
setDetecting(false);
rollingRecorder = new RollingRecorder(ICodec.ID.CODEC_ID_MPEG4, ".mp4", sessionName, cameraName, this);
recordingShots = true;
}
public void stopRecordingShots() {
recordingShots = false;
for (final ShotRecorder r : shotRecorders.values())
r.close();
shotRecorders.clear();
if (rollingRecorder != null) {
rollingRecorder.close();
rollingRecorder = null;
}
setDetecting(true);
}
public Camera getCamera() {
return camera;
}
public Image getCurrentFrame() {
return SwingFXUtils.toFXImage(camera.getBufferedImage(), null);
}
public CameraView getCameraView() {
return cameraView;
}
public void setMinimumShotDimension(int minDim) {
minimumShotDimension = Optional.of(minDim);
logger.debug("Set the minimum dimension for shots to: {}", minDim);
}
public Optional<Integer> getMinimumShotDimension() {
return minimumShotDimension;
}
public void setThresholdListener(CameraDebuggerListener thresholdListener) {
debuggerListener = Optional.ofNullable(thresholdListener);
}
public Optional<CameraDebuggerListener> getDebuggerListener() {
return debuggerListener;
}
private ScheduledFuture<?> brightnessDiagnosticFuture = null;
private ScheduledFuture<?> motionDiagnosticFuture = null;
private boolean recordCalibratedArea = false;
private IMediaWriter videoWriterCalibratedArea;
private long recordingCalibratedAreaStartTime;
private boolean isFirstCalibratedAreaFrame;
private boolean recordingCalibratedArea;
public void startRecordingCalibratedArea(File videoFile, int width, int height) {
if (logger.isDebugEnabled()) logger.debug("Writing Video Feed To: {}", videoFile.getAbsoluteFile());
videoWriterCalibratedArea = ToolFactory.makeWriter(videoFile.getName());
videoWriterCalibratedArea.addVideoStream(0, 0, ICodec.ID.CODEC_ID_H264, width, height);
recordingCalibratedAreaStartTime = System.currentTimeMillis();
isFirstCalibratedAreaFrame = true;
recordingCalibratedArea = true;
}
public void stopRecordingCalibratedArea() {
recordingCalibratedArea = false;
videoWriterCalibratedArea.close();
}
@Override
public void newFrame(Frame frame) {
newFrame(frame, true);
}
@Override
public void newFrame(Frame frame, boolean shouldDedistort) {
if (!handleFrame(frame, shouldDedistort)) logger.warn("Invalid frame yielded from {}", camera.getName());
}
private int consecutiveCameraErrors = 0;
private boolean handleFrame(Frame currentFrame, boolean shouldDedistort) {
boolean cameraError = false;
if (currentFrame == null && !camera.isOpen()) {
// Camera appears to have closed
if (isStreaming.get() && cameraErrorView.isPresent()) cameraErrorView.get().showMissingCameraError(camera);
cameraError = true;
} else if (currentFrame == null && camera.isOpen()) {
// Camera appears to be open but got a null frame
logger.warn("Null frame from camera: {}", camera.getName());
cameraError = true;
} else if (currentFrame != null
&& (currentFrame.size().height != feedHeight || currentFrame.size().width != feedWidth)
&& camera.isOpen()) {
// Camera appears to be open but got an invalid size frame
logger.warn("Invalid frame size from camera: {} gave {} expecting {},{}", camera.getName(),
currentFrame.size(), feedWidth, feedHeight);
cameraError = true;
}
if (cameraError) {
consecutiveCameraErrors++;
if (consecutiveCameraErrors > MAXIMUM_CONSECUTIVE_CAMERA_ERRORS) {
if (isStreaming.get() && cameraErrorView.isPresent())
cameraErrorView.get().showMissingCameraError(camera);
camera.close();
}
return false;
} else {
consecutiveCameraErrors = 0;
}
BufferedImage currentImage = processFrame(currentFrame, shouldDedistort);
Bounds b;
synchronized (projectionBoundsLock) {
if (projectionBounds.isPresent()) {
b = projectionBounds.get();
} else {
b = null;
}
}
if (cropFeedToProjection && b != null) {
currentImage = currentImage.getSubimage((int) b.getMinX(), (int) b.getMinY(), (int) b.getWidth(),
(int) b.getHeight());
}
if (recordingShots) {
rollingRecorder.recordFrame(currentImage);
final List<Shot> removeKeys = new ArrayList<>();
for (final Entry<Shot, ShotRecorder> r : shotRecorders.entrySet()) {
if (r.getValue().isComplete()) {
r.getValue().close();
removeKeys.add(r.getKey());
} else {
r.getValue().recordFrame(currentImage);
}
}
for (final Shot s : removeKeys)
shotRecorders.remove(s);
}
if (recordingStream) {
final BufferedImage image = ConverterFactory.convertToType(currentImage, BufferedImage.TYPE_3BYTE_BGR);
final IConverter converter = ConverterFactory.createConverter(image, IPixelFormat.Type.YUV420P);
final IVideoPicture frame = converter.toPicture(image,
(System.currentTimeMillis() - recordingStartTime) * 1000);
frame.setKeyFrame(isFirstStreamFrame);
frame.setQuality(0);
isFirstStreamFrame = false;
videoWriterStream.encodeVideo(0, frame);
}
if (!config.isHeadless()) {
if (cropFeedToProjection && projectionBounds.isPresent()) {
cameraView.updateBackground(currentImage, projectionBounds);
} else {
cameraView.updateBackground(currentImage, Optional.empty());
}
}
return true;
}
protected BufferedImage processFrame(Frame currentFrame, boolean shouldDedistort) {
if (isAutoCalibrating.get()) {
acm.processFrame(currentFrame);
return currentFrame.getOriginalBufferedImage();
}
Mat submatFrameBGR = null;
Bounds projectionBounds;
synchronized (projectionBoundsLock) {
if (this.projectionBounds.isPresent()) {
projectionBounds = this.projectionBounds.get();
} else {
projectionBounds = null;
}
}
if (cameraAutoCalibrated && projectionBounds != null) {
if (shouldDedistort && acm != null) {
// MUST BE IN BGR pixel format.
currentFrame = acm.undistortFrame(currentFrame);
}
try {
submatFrameBGR = currentFrame.getOriginalMat().submat((int) projectionBounds.getMinY(),
(int) projectionBounds.getMaxY(), (int) projectionBounds.getMinX(),
(int) projectionBounds.getMaxX());
} catch (CvException e) {
logger.error("Failed to get submat for frame using projection bounds, projectionBounds = "
+ projectionBounds.toString() + ", frameSize = "
+ currentFrame.getOriginalMat().size().toString(), e);
}
if (recordingCalibratedArea) {
final BufferedImage image = ConverterFactory.convertToType(Camera.matToBufferedImage(submatFrameBGR),
BufferedImage.TYPE_3BYTE_BGR);
final IConverter converter = ConverterFactory.createConverter(image, IPixelFormat.Type.YUV420P);
final IVideoPicture frame = converter.toPicture(image,
(System.currentTimeMillis() - recordingCalibratedAreaStartTime) * 1000);
frame.setKeyFrame(isFirstCalibratedAreaFrame);
frame.setQuality(0);
isFirstCalibratedAreaFrame = false;
videoWriterCalibratedArea.encodeVideo(0, frame);
}
if (debuggerListener.isPresent()) {
debuggerListener.get().updateDebugView(Camera.matToBufferedImage(submatFrameBGR));
}
}
if ((isLimitingDetectionToProjection() || isCroppingFeedToProjection()) && projectionBounds != null) {
if (submatFrameBGR == null) {
try {
submatFrameBGR = currentFrame.getOriginalMat().submat((int) projectionBounds.getMinY(),
(int) projectionBounds.getMaxY(), (int) projectionBounds.getMinX(),
(int) projectionBounds.getMaxX());
} catch (CvException e) {
logger.error("Failed to get submat for frame to limit detection bounds, projectionBounds = "
+ projectionBounds.toString() + ", frameSize = "
+ currentFrame.getOriginalMat().size().toString(), e);
}
}
if (shotDetector instanceof FrameProcessingShotDetector) {
if (submatFrameBGR != null) {
((FrameProcessingShotDetector) shotDetector)
.processFrame(new Frame(submatFrameBGR, currentFrame.getTimestamp()), isDetecting.get());
} else {
logger.warn("Due to errors fetching frame submat, falling back to using full frame");
((FrameProcessingShotDetector) shotDetector).processFrame(currentFrame, isDetecting.get());
}
}
} else {
if (shotDetector instanceof FrameProcessingShotDetector)
((FrameProcessingShotDetector) shotDetector).processFrame(currentFrame, isDetecting.get());
}
// currentFrame is showing the colored pixels for brightness and motion,
// hence why we need to return the converted version
return currentFrame.getOriginalBufferedImage();
}
private void checkIfMinimumFPS(double cameraFPS) {
if (cameraFPS < MIN_SHOT_DETECTION_FPS && !showedFPSWarning) {
logger.warn("[{}] Current webcam FPS is {}, which is too low for reliable shot detection", camera.getName(),
getFPS());
if (cameraErrorView.isPresent()) cameraErrorView.get().showFPSWarning(camera, getFPS());
showedFPSWarning = true;
}
}
private Label brightnessDiagnosticWarning = null;
public void showBrightnessWarning() {
if (!TimerPool.isWaiting(brightnessDiagnosticFuture)) {
brightnessDiagnosticWarning = cameraView.addDiagnosticMessage("Warning: Excessive brightness", Color.RED);
} else {
// Stop the existing timer and start a new one
TimerPool.cancelTimer(brightnessDiagnosticFuture);
}
brightnessDiagnosticFuture = TimerPool.schedule(() -> {
if (brightnessDiagnosticWarning != null) {
cameraView.removeDiagnosticMessage(brightnessDiagnosticWarning);
brightnessDiagnosticWarning = null;
}
}, DIAGNOSTIC_MESSAGE_DURATION);
if (!shownBrightnessWarning) {
shownBrightnessWarning = true;
if (cameraErrorView.isPresent()) cameraErrorView.get().showBrightnessWarning(camera);
}
}
private Label motionDiagnosticWarning = null;
public void showMotionWarning() {
if (!TimerPool.isWaiting(motionDiagnosticFuture)) {
motionDiagnosticWarning = cameraView.addDiagnosticMessage(
"Warning: Excessive motion -- Try reducing the camera exposure setting", Color.RED);
} else {
// Stop the existing timer and start a new one
TimerPool.cancelTimer(motionDiagnosticFuture);
}
motionDiagnosticFuture = TimerPool.schedule(() -> {
if (motionDiagnosticWarning != null) {
cameraView.removeDiagnosticMessage(motionDiagnosticWarning);
motionDiagnosticWarning = null;
}
}, DIAGNOSTIC_MESSAGE_DURATION);
}
private void fireAutoCalibration() {
acm.reset();
}
protected void autoCalibrateSuccess(Bounds arenaBounds, Optional<Dimension2D> paperDims, long delay) {
if (isAutoCalibrating.get() && cameraCalibrationListener != null) {
isAutoCalibrating.set(false);
logger.debug("autoCalibrateSuccess {} {} {} {} paper {}", (int) arenaBounds.getMinX(),
(int) arenaBounds.getMinY(), (int) arenaBounds.getWidth(), (int) arenaBounds.getHeight(),
paperDims.isPresent());
cameraAutoCalibrated = true;
cameraCalibrationListener.calibrate(arenaBounds, paperDims, false, delay);
if (recordCalibratedArea && !recordingCalibratedArea)
startRecordingCalibratedArea(new File("calibratedArea.mp4"), (int) arenaBounds.getWidth(),
(int) arenaBounds.getHeight());
}
}
public void enableAutoCalibration(boolean calculateFrameDelay) {
if (acm == null) acm = new AutoCalibrationManager(this, camera, calculateFrameDelay);
isAutoCalibrating.set(true);
cameraAutoCalibrated = false;
fireAutoCalibration();
}
public void disableAutoCalibration() {
isAutoCalibrating.set(false);
}
@Override
public void setArenaBackground(String resourceFilename) {
if (cameraCalibrationListener == null) {
logger.error("setArenaBackground called when controller is null");
return;
}
cameraCalibrationListener.setArenaBackground(resourceFilename);
}
public void launchCameraSettings() {
if (camera instanceof SarxosCaptureCamera) {
((SarxosCaptureCamera) camera).launchCameraSettings();
} else if (camera instanceof PS3EyeCamera) {
((PS3EyeCamera) camera).launchCameraSettings();
}
}
public double getFPS() {
return camera.getFPS();
}
public int getFrameCount() {
return camera.getFrameCount();
}
public long cameraTimeToShotTime(long timestamp) {
if (startTime == 0) {
resetStartTime(timestamp);
return 0;
}
return timestamp - startTime;
}
@Override
public void newFPS(double cameraFPS) {
if (debuggerListener.isPresent()) debuggerListener.get().updateFeedData(cameraFPS);
checkIfMinimumFPS(cameraFPS);
}
@Override
public void cameraClosed() {
setCameraState(CameraState.CLOSED);
close();
}
@Override
public void calibrate(Bounds arenaBounds, Optional<Dimension2D> perspectivePaperDims, boolean calibratedFromCanvas,
long delay) {
autoCalibrateSuccess(arenaBounds, perspectivePaperDims, delay);
}
public Point undistortCoords(int x, int y) {
if (acm == null) return new Point(x, y);
return acm.undistortCoords(x, y);
}
public boolean isCalibrated() {
return projectionBounds.isPresent();
}
}