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.vision.camera.USBCameras;
019
020import edu.wpi.first.cscore.UsbCamera;
021import edu.wpi.first.cscore.VideoException;
022import edu.wpi.first.cscore.VideoMode;
023import edu.wpi.first.cscore.VideoProperty;
024import edu.wpi.first.math.MathUtil;
025import edu.wpi.first.util.PixelFormat;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Optional;
032import java.util.stream.Collectors;
033import org.photonvision.common.configuration.CameraConfiguration;
034import org.photonvision.common.configuration.ConfigManager;
035import org.photonvision.vision.camera.CameraQuirk;
036import org.photonvision.vision.processes.VisionSourceSettables;
037
038public class GenericUSBCameraSettables extends VisionSourceSettables {
039    // We need to remember the last exposure set when exiting
040    // auto exposure mode so we can restore it
041    protected double lastExposureRaw = -1;
042
043    // Some cameras need logic where we re-apply brightness after
044    // changing exposure
045    protected int lastBrightness = -1;
046
047    protected VideoProperty exposureAbsProp = null;
048    protected VideoProperty autoExposureProp = null;
049    protected VideoProperty wbTempProp = null;
050
051    protected double minExposure = 1;
052    protected double maxExposure = 80000;
053
054    protected double minWhiteBalanceTemp = 1;
055    protected double maxWhiteBalanceTemp = 4000;
056    protected int lastWhiteBalanceTemp = 4000;
057
058    protected static final int PROP_AUTO_EXPOSURE_ENABLED = 3;
059    protected static final int PROP_AUTO_EXPOSURE_DISABLED = 1;
060
061    protected UsbCamera camera;
062    protected CameraConfiguration configuration;
063
064    public GenericUSBCameraSettables(CameraConfiguration configuration, UsbCamera camera) {
065        super(configuration);
066
067        this.configuration = configuration;
068        this.camera = camera;
069
070        // TODO - how should this work post-refactor???
071        if (!configuration.cameraQuirks.hasQuirk(CameraQuirk.StickyFPS)) {
072            if (!videoModes.isEmpty()) {
073                setVideoMode(videoModes.get(0)); // fixes double FPS set
074            }
075        }
076    }
077
078    protected void setUpWhiteBalanceProperties() {
079        wbTempProp = findProperty("white_balance_temperature", "WhiteBalance").orElse(null);
080        if (wbTempProp != null) {
081            this.minWhiteBalanceTemp = wbTempProp.getMin();
082            this.maxWhiteBalanceTemp = wbTempProp.getMax();
083        }
084    }
085
086    protected void setUpExposureProperties() {
087        // Photonvision needs to be able to control absolute exposure. Make sure we can
088        // first.
089        var expProp =
090                findProperty(
091                        "raw_exposure_absolute", "raw_exposure_time_absolute", "exposure", "raw_Exposure");
092
093        if (expProp.isEmpty()) {
094            logger.warn("Could not find exposure property");
095            return;
096        } else {
097            exposureAbsProp = expProp.get();
098            this.minExposure = exposureAbsProp.getMin();
099            this.maxExposure = exposureAbsProp.getMax();
100        }
101
102        // Photonvision needs to be able to control auto exposure. Make sure we can
103        // first.
104        var autoExpProp = findProperty("exposure_auto", "auto_exposure");
105
106        if (expProp.isPresent()) {
107            exposureAbsProp = expProp.get();
108            this.minExposure = exposureAbsProp.getMin();
109            this.maxExposure = exposureAbsProp.getMax();
110        }
111
112        if (autoExpProp.isPresent()) {
113            autoExposureProp = autoExpProp.get();
114        }
115    }
116
117    public void setAllCamDefaults() {
118        // Common settings for all cameras to attempt to get their image
119        // as close as possible to what we want for image processing
120        softSet("image_stabilization", 0); // No image stabilization, as this will throw off odometry
121        softSet("power_line_frequency", 2); // Assume 60Hz USA
122        softSet("scene_mode", 0); // no presets
123        softSet("exposure_metering_mode", 0);
124        softSet("exposure_dynamic_framerate", 0);
125        softSet("focus_auto", 0);
126        softSet("focus_absolute", 0); // Focus into infinity
127    }
128
129    @Override
130    public void setWhiteBalanceTemp(double tempNumber) {
131        if (wbTempProp == null) {
132            // bail
133            return;
134        }
135
136        try {
137            int temp = (int) Math.round(tempNumber);
138
139            softSet("white_balance_automatic", 0);
140
141            int propVal = (int) MathUtil.clamp(temp, minWhiteBalanceTemp, maxWhiteBalanceTemp);
142
143            logger.debug(
144                    "Setting property "
145                            + wbTempProp.getName()
146                            + " to "
147                            + propVal
148                            + " (user requested "
149                            + temp
150                            + " degrees)");
151
152            wbTempProp.set(propVal);
153
154            this.lastWhiteBalanceTemp = temp;
155
156        } catch (VideoException e) {
157            logger.error("Failed to set camera exposure!", e);
158        }
159    }
160
161    @Override
162    public void setAutoWhiteBalance(boolean autoWB) {
163        logger.debug("Setting auto white balance to " + autoWB);
164
165        if (autoWB) {
166            // Seems to be a rpi-specific property?
167            softSet("white_balance_automatic", 1);
168        } else {
169            softSet("white_balance_automatic", 0);
170
171            if (wbTempProp != null) {
172                wbTempProp.set(this.lastWhiteBalanceTemp);
173            }
174        }
175    }
176
177    public void setAutoExposure(boolean cameraAutoExposure) {
178        logger.debug("Setting auto exposure to " + cameraAutoExposure);
179
180        if (!cameraAutoExposure) {
181            // Pick a bunch of reasonable setting defaults for vision processing
182            softSet("auto_exposure_bias", 0);
183            softSet("iso_sensitivity_auto", 0); // Disable auto ISO adjustment
184            softSet("iso_sensitivity", 0); // Manual ISO adjustment
185            if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_DISABLED);
186
187            // Most cameras leave exposure time absolute at the last value from their AE
188            // algorithm.
189            // Set it back to the exposure slider value
190            setExposureRaw(this.lastExposureRaw);
191
192        } else {
193            // Pick a bunch of reasonable setting to make the picture nice-for-humans
194            softSet("auto_exposure_bias", 12);
195            softSet("iso_sensitivity_auto", 1);
196            softSet("iso_sensitivity", 1); // Manual ISO adjustment by default
197            if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED);
198        }
199    }
200
201    @Override
202    public double getMinExposureRaw() {
203        return minExposure;
204    }
205
206    @Override
207    public double getMaxExposureRaw() {
208        return maxExposure;
209    }
210
211    @Override
212    public void setExposureRaw(double exposureRaw) {
213        if (exposureRaw >= 0.0) {
214            try {
215                if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_DISABLED);
216
217                int propVal = (int) MathUtil.clamp(exposureRaw, minExposure, maxExposure);
218
219                logger.debug(
220                        "Setting property "
221                                + exposureAbsProp.getName()
222                                + " to "
223                                + propVal
224                                + " (user requested "
225                                + exposureRaw
226                                + " μs)");
227
228                exposureAbsProp.set(propVal);
229
230                this.lastExposureRaw = exposureRaw;
231
232            } catch (VideoException e) {
233                logger.error("Failed to set camera exposure!", e);
234            }
235        }
236    }
237
238    @Override
239    public void setBrightness(int brightness) {
240        try {
241            camera.setBrightness(brightness);
242            this.lastBrightness = brightness;
243        } catch (VideoException e) {
244            logger.error("Failed to set camera brightness!", e);
245        }
246    }
247
248    @Override
249    public void setGain(int gain) {
250        softSet("gain_automatic", 0);
251        softSet("gain", gain);
252    }
253
254    @Override
255    public VideoMode getCurrentVideoMode() {
256        return camera
257                .getVideoMode(); // This returns the current video mode even if the camera is disconnected
258    }
259
260    @Override
261    public void setVideoModeInternal(VideoMode videoMode) {
262        try {
263            if (videoMode == null) {
264                logger.error("Got a null video mode! Doing nothing...");
265                return;
266            }
267            if (camera.setVideoMode(videoMode)) logger.debug("Failed to set video mode!");
268        } catch (Exception e) {
269            logger.error("Failed to set video mode!", e);
270        }
271    }
272
273    private void cacheVideoModes() {
274        videoModes = new HashMap<>();
275        List<VideoMode> videoModesList = new ArrayList<>();
276        try {
277            VideoMode[] modes;
278
279            modes = camera.enumerateVideoModes();
280
281            for (VideoMode videoMode : modes) {
282                // Filter grey modes
283                if (videoMode.pixelFormat == PixelFormat.kGray
284                        || videoMode.pixelFormat == PixelFormat.kUnknown) {
285                    continue;
286                }
287
288                if (configuration.cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
289                    if (videoMode.fps > 100) {
290                        continue;
291                    }
292                }
293
294                videoModesList.add(videoMode);
295            }
296        } catch (Exception e) {
297            logger.error("Exception while enumerating video modes!", e);
298            videoModesList = List.of();
299        }
300
301        // Sort by resolution
302        var sortedList =
303                videoModesList.stream()
304                        .distinct() // remove redundant video mode entries
305                        .sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
306                        .collect(Collectors.toList());
307        Collections.reverse(sortedList);
308
309        // On vendor cameras, respect blacklisted indices
310        var indexBlacklist =
311                ConfigManager.getInstance().getConfig().getHardwareConfig().blacklistedResIndices;
312        for (int badIdx : indexBlacklist) {
313            sortedList.remove(badIdx);
314        }
315
316        for (VideoMode videoMode : sortedList) {
317            videoModes.put(sortedList.indexOf(videoMode), videoMode);
318        }
319
320        // If after all that we still have no video modes, not much we can do besides
321        // throw up our hands
322        if (videoModes.isEmpty()) {
323            logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision");
324        }
325    }
326
327    @Override
328    public HashMap<Integer, VideoMode> getAllVideoModes() {
329        if (!cameraPropertiesCached) {
330            // Device hasn't connected at least once, best I can do is given up
331            logger.warn("Device hasn't connected, cannot enumerate video modes");
332            return new HashMap<>();
333        }
334
335        return videoModes;
336    }
337
338    /**
339     * Forgiving "set this property" action. Produces a debug message but skips properties if they
340     * aren't supported Errors if the property exists but the set fails.
341     *
342     * @param property
343     * @param value
344     */
345    protected void softSet(String property, int value) {
346        VideoProperty prop = camera.getProperty(property);
347        if (prop.getKind() == VideoProperty.Kind.kNone) {
348            logger.debug("No property " + property + " for " + camera.getName() + " , skipping.");
349        } else {
350            try {
351                prop.set(value);
352            } catch (VideoException e) {
353                logger.error("Failed to set " + property + " for " + camera.getName() + " !", e);
354            }
355        }
356    }
357
358    /**
359     * Returns the first property with a name in the list. Useful to find gandolf property that goes
360     * by many names in different os/releases/whatever
361     *
362     * @param options
363     * @return
364     */
365    protected Optional<VideoProperty> findProperty(String... options) {
366        VideoProperty retProp = null;
367        boolean found = false;
368        for (var option : options) {
369            retProp = camera.getProperty(option);
370            if (retProp.getKind() != VideoProperty.Kind.kNone) {
371                // got em
372                found = true;
373                break;
374            }
375        }
376
377        if (!found) {
378            logger.warn(
379                    "Expected at least one of the following properties to be available: "
380                            + Arrays.toString(options));
381            retProp = null;
382        }
383
384        return Optional.ofNullable(retProp);
385    }
386
387    @Override
388    public double getMaxWhiteBalanceTemp() {
389        return maxWhiteBalanceTemp;
390    }
391
392    @Override
393    public double getMinWhiteBalanceTemp() {
394        return minWhiteBalanceTemp;
395    }
396
397    @Override
398    public void onCameraConnected() {
399        super.onCameraConnected();
400
401        logger.info("Caching cscore properties");
402
403        // Now that our device is actually connected, we can enumerate properties/video
404        // modes
405        setUpExposureProperties();
406        setUpWhiteBalanceProperties();
407        cacheVideoModes();
408
409        setAllCamDefaults();
410
411        calculateFrameStaticProps();
412    }
413}