diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000..1d953f4bd7 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore index 6416c2fba8..8178990427 100644 --- a/.gitignore +++ b/.gitignore @@ -148,3 +148,6 @@ node_modules dist components.d.ts photon-server/src/main/resources/web/index.html + +# Direnv +.direnv \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 376e6c941c..e9ad1f0f94 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "python.testing.cwd": "photon-lib/py" + "python.testing.cwd": "photon-lib/py", + "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1f0d90b7d8..951871e0f7 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,7 @@ ext { libcameraDriverVersion = "v2025.0.4" rknnVersion = "dev-v2025.0.0-5-g666c0c6" rubikVersion = "dev-v2025.1.0-8-g067a316" + baslerVersion = "local" frcYear = "2025" mrcalVersion = "v2025.0.0"; diff --git a/photon-client/src/components/common/pv-camera-info-card.vue b/photon-client/src/components/common/pv-camera-info-card.vue index f726cc6eb0..b318ef9691 100644 --- a/photon-client/src/components/common/pv-camera-info-card.vue +++ b/photon-client/src/components/common/pv-camera-info-card.vue @@ -18,6 +18,9 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => { if (camera.PVFileCameraInfo) { return camera.PVFileCameraInfo; } + if (camera.PVBaslerCameraInfo) { + return camera.PVBaslerCameraInfo; + } return {}; }; @@ -39,6 +42,7 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => { USB Camera CSI Camera File Camera + Basler Camera Unidentified Camera Type diff --git a/photon-client/src/components/common/pv-camera-match-card.vue b/photon-client/src/components/common/pv-camera-match-card.vue index 644741bd3f..f5c505e955 100644 --- a/photon-client/src/components/common/pv-camera-match-card.vue +++ b/photon-client/src/components/common/pv-camera-match-card.vue @@ -36,6 +36,9 @@ const cameraInfoFor = (camera: PVCameraInfo): any => { if (camera.PVFileCameraInfo) { return camera.PVFileCameraInfo; } + if (camera.PVBaslerCameraInfo) { + return camera.PVBaslerCameraInfo; + } return {}; }; @@ -78,10 +81,12 @@ const cameraInfoFor = (camera: PVCameraInfo): any => { USB Camera CSI Camera File Camera + Basler Camera Unidentified Camera Type USB Camera CSI Camera File Camera + Basler Camera Unidentified Camera Type { const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length; const cameraResolutions = computed(() => - useCameraSettingsStore().currentCameraSettings.validVideoFormats.map( - (f) => `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}` - ) + useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) => { + return `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}${getBinningString(f.binning)}`; + }) ); const handleResolutionChange = (value: number) => { useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false); @@ -116,24 +116,38 @@ const interactiveCols = computed(() => /> " /> { return a.height === b.height && a.width === b.width; @@ -6,6 +6,9 @@ export const resolutionsAreEqual = (a: Resolution, b: Resolution) => { export const getResolutionString = (resolution: Resolution): string => `${resolution.width}x${resolution.height}`; +export const getBinningString = (binning?: BinningConfig): string => + binning != null && binning.mode !== "NONE" ? ` ${binning.mode} ${binning.horz}x${binning.vert}` : ""; + export const parseJsonFile = async >(file: File): Promise => { return new Promise((resolve, reject) => { const fileReader = new FileReader(); diff --git a/photon-client/src/stores/settings/CameraSettingsStore.ts b/photon-client/src/stores/settings/CameraSettingsStore.ts index 32c3f94edf..5a78039c90 100644 --- a/photon-client/src/stores/settings/CameraSettingsStore.ts +++ b/photon-client/src/stores/settings/CameraSettingsStore.ts @@ -101,6 +101,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { actions: { updateCameraSettingsFromWebsocket(data: WebsocketCameraSettingsUpdate[]) { const configuredCameras = data.reduce<{ [key: string]: UiCameraConfiguration }>((acc, d) => { + console.log("Bmode: " + d.videoFormatList[0].binningMode); acc[d.uniqueName] = { cameraPath: d.cameraPath, nickname: d.nickname, @@ -128,7 +129,16 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { horizontalFOV: v.horizontalFOV, verticalFOV: v.verticalFOV, standardDeviation: v.standardDeviation, - mean: v.mean + mean: v.mean, + ...(v.binningMode !== undefined && + v.binningHorz !== undefined && + v.binningVert !== undefined && { + binning: { + mode: v.binningMode, + horz: v.binningHorz, + vert: v.binningVert + } + }) })), completeCalibrations: d.calibrations, isCSICamera: d.isCSICamera, @@ -145,6 +155,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { hasConnected: d.hasConnected, mismatch: d.mismatch }; + return acc; }, {}); this.cameras = diff --git a/photon-client/src/types/SettingTypes.ts b/photon-client/src/types/SettingTypes.ts index 59fa0d39a8..490604de87 100644 --- a/photon-client/src/types/SettingTypes.ts +++ b/photon-client/src/types/SettingTypes.ts @@ -74,7 +74,7 @@ export interface PVCameraInfoBase { Huge hack. In Jackson, this is set based on the underlying type -- this then maps to one of the 3 subclasses here below. Not sure how to best deal with this. */ - cameraTypename: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo"; + cameraTypename: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo" | "PVBaslerCameraInfo"; } export interface PVUsbCameraInfo { @@ -103,11 +103,19 @@ export interface PVFileCameraInfo { uniquePath: string; } +export interface PVBaslerCameraInfo { + serial: string; + model: string; + + uniquePath: string; +} + // This camera info will only ever hold one of its members - the others should be undefined. export class PVCameraInfo { PVUsbCameraInfo: PVUsbCameraInfo | undefined; PVCSICameraInfo: PVCSICameraInfo | undefined; PVFileCameraInfo: PVFileCameraInfo | undefined; + PVBaslerCameraInfo: PVBaslerCameraInfo | undefined; } export interface VsmState { @@ -139,10 +147,17 @@ export interface Resolution { height: number; } +export interface BinningConfig { + mode: String; + horz: number; + vert: number; +} + export interface VideoFormat { resolution: Resolution; fps: number; pixelFormat: string; + binning?: BinningConfig; index?: number; diagonalFOV?: number; horizontalFOV?: number; @@ -220,7 +235,9 @@ export enum ValidQuirks { PiCam = "PiCam", StickyFPS = "StickyFPS", LifeCamControls = "LifeCamControls", - PsEyeControls = "PsEyeControls" + PsEyeControls = "PsEyeControls", + BaslerDaA1280Controls = "BaslerDaA1280Controls", + ManualWB = "ManualWB" } export interface QuirkyCamera { @@ -371,7 +388,9 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = { StickyFPS: false, InnoOV9281Controls: false, LifeCamControls: false, - PsEyeControls: false + PsEyeControls: false, + BaslerDaA1280Controls: false, + ManualWB: false } }, isCSICamera: false, @@ -386,7 +405,8 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = { uniquePath: "/dev/foobar2" }, PVCSICameraInfo: undefined, - PVUsbCameraInfo: undefined + PVUsbCameraInfo: undefined, + PVBaslerCameraInfo: undefined }, isConnected: true, hasConnected: true, diff --git a/photon-client/src/types/WebsocketDataTypes.ts b/photon-client/src/types/WebsocketDataTypes.ts index 4ca05d7895..361e6786ca 100644 --- a/photon-client/src/types/WebsocketDataTypes.ts +++ b/photon-client/src/types/WebsocketDataTypes.ts @@ -43,6 +43,9 @@ export type WebsocketVideoFormat = Record< verticalFOV?: number; standardDeviation?: number; mean?: number; + binningMode?: String; + binningHorz?: number; + binningVert?: number; } >; diff --git a/photon-client/src/views/CameraMatchingView.vue b/photon-client/src/views/CameraMatchingView.vue index 093cb83fb0..ccda5768c4 100644 --- a/photon-client/src/views/CameraMatchingView.vue +++ b/photon-client/src/views/CameraMatchingView.vue @@ -5,6 +5,7 @@ import { useStateStore } from "@/stores/StateStore"; import { PlaceholderCameraSettings, PVCameraInfo, + type PVBaslerCameraInfo, type PVCSICameraInfo, type PVFileCameraInfo, type PVUsbCameraInfo, @@ -221,7 +222,7 @@ const yesDeleteMySettingsText = ref(""); /** * Get the connection-type-specific camera info from the given PVCameraInfo object. */ -const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => { +const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | PVBaslerCameraInfo | any => { if (!camera) return null; if (camera.PVUsbCameraInfo) { return camera.PVUsbCameraInfo; @@ -232,6 +233,9 @@ const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICame if (camera.PVFileCameraInfo) { return camera.PVFileCameraInfo; } + if (camera.PVBaslerCameraInfo) { + return camera.PVBaslerCameraInfo; + } return {}; }; @@ -243,7 +247,8 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => { return { PVFileCameraInfo: undefined, PVCSICameraInfo: undefined, - PVUsbCameraInfo: undefined + PVUsbCameraInfo: undefined, + PVBaslerCameraInfo: undefined, }; } return ( @@ -252,7 +257,8 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => { ) || { PVFileCameraInfo: undefined, PVCSICameraInfo: undefined, - PVUsbCameraInfo: undefined + PVUsbCameraInfo: undefined, + PVBaslerCameraInfo: undefined, } ); }; @@ -477,6 +483,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => { USB Camera: CSI Camera: File Camera: + Basler Camera: Unknown Camera:  {{ cameraInfoFor(camera)?.name ?? cameraInfoFor(camera)?.baseName }} diff --git a/photon-core/build.gradle b/photon-core/build.gradle index 6e9c99c1f1..33f2aefa67 100644 --- a/photon-core/build.gradle +++ b/photon-core/build.gradle @@ -47,6 +47,9 @@ dependencies { implementation("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:linuxarm64") { transitive = false } + implementation("org.teamdeadbolts:basler_jni-linuxx64:$baslerVersion") { + transitive = false + } implementation "org.photonvision:photon-libcamera-gl-driver-java:$libcameraDriverVersion" implementation "org.photonvision:photon-mrcal-java:$mrcalVersion" diff --git a/photon-core/src/main/java/org/photonvision/jni/BaslerCameraJNI.java b/photon-core/src/main/java/org/photonvision/jni/BaslerCameraJNI.java new file mode 100644 index 0000000000..6bff716d6b --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/jni/BaslerCameraJNI.java @@ -0,0 +1,36 @@ +package org.photonvision.jni; + +import java.io.IOException; +import java.util.List; +import org.photonvision.common.util.TestUtils; + +public class BaslerCameraJNI extends PhotonJNICommon { + private boolean isLoaded; + private static BaslerCameraJNI instance = null; + + private BaslerCameraJNI() { + isLoaded = false; + } + + public static BaslerCameraJNI getInstance() { + if (instance == null) instance = new BaslerCameraJNI(); + + return instance; + } + + public static synchronized void forceLoad() throws IOException { + TestUtils.loadLibraries(); + + forceLoad(getInstance(), BaslerCameraJNI.class, List.of("baslerjni")); + } + + @Override + public boolean isLoaded() { + return isLoaded; + } + + @Override + public void setLoaded(boolean state) { + isLoaded = state; + } +} diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/CameraQuirk.java b/photon-core/src/main/java/org/photonvision/vision/camera/CameraQuirk.java index a3dace6549..96b2f7b1ae 100644 --- a/photon-core/src/main/java/org/photonvision/vision/camera/CameraQuirk.java +++ b/photon-core/src/main/java/org/photonvision/vision/camera/CameraQuirk.java @@ -58,4 +58,10 @@ public enum CameraQuirk { ArduOV9782, /** Camera has odd exposure range, and supports gain control */ See3Cam_24CUG, + + /** Quirks to tell the difference between basler cameras */ + BaslerDaA1280Controls, + + /** Other Basler Quirks */ + ManualWB, } diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/CameraType.java b/photon-core/src/main/java/org/photonvision/vision/camera/CameraType.java index c080f366e5..8d186832e0 100644 --- a/photon-core/src/main/java/org/photonvision/vision/camera/CameraType.java +++ b/photon-core/src/main/java/org/photonvision/vision/camera/CameraType.java @@ -20,5 +20,6 @@ public enum CameraType { UsbCamera, ZeroCopyPicam, + BaslerCamera, FileCamera // special case for File-based vision sources } diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java b/photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java index 96244b0787..5c58187988 100644 --- a/photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java +++ b/photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java @@ -27,13 +27,16 @@ import edu.wpi.first.cscore.UsbCameraInfo; import java.util.Arrays; import java.util.Objects; +import org.teamdeadbolts.basler.BaslerJNI; +import org.teamdeadbolts.basler.BaslerJNI.CameraModel; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) @JsonIgnoreProperties(ignoreUnknown = true) @JsonSubTypes({ @JsonSubTypes.Type(value = PVCameraInfo.PVUsbCameraInfo.class), @JsonSubTypes.Type(value = PVCameraInfo.PVCSICameraInfo.class), - @JsonSubTypes.Type(value = PVCameraInfo.PVFileCameraInfo.class) + @JsonSubTypes.Type(value = PVCameraInfo.PVFileCameraInfo.class), + @JsonSubTypes.Type(value = PVCameraInfo.PVBaslerCameraInfo.class) }) public sealed interface PVCameraInfo { /** @@ -289,6 +292,73 @@ public String toString() { } } + @JsonTypeName("PVBaslerCameraInfo") + public static final class PVBaslerCameraInfo implements PVCameraInfo { + + public final String serial; + public final BaslerJNI.CameraModel model; + + public PVBaslerCameraInfo( + @JsonProperty("serial") String serial, @JsonProperty("model") BaslerJNI.CameraModel model) { + this.serial = serial; + this.model = model; + } + + @Override + public String path() { + return this.serial; + } + + @Override + public String name() { + return this.model.getFriendlyName(); + } + + @Override + public String uniquePath() { + return path(); + } + + @Override + public String[] otherPaths() { + return new String[] {}; + } + + @Override + public CameraType type() { + return CameraType.BaslerCamera; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof PVBaslerCameraInfo info)) return false; + + return this.model.equals(info.model) && this.serial.equals(info.serial); + } + + @Override + public int hashCode() { + return Objects.hash(model, serial); + } + + @Override + public String toString() { + return "PVBaslerCameraInfo[type=" + + type() + + ", model=" + + model.toString() + + ", serial='" + + serial + + "']"; + } + + public BaslerJNI.CameraModel getModel() { + return this.model; + } + } + public static PVCameraInfo fromUsbCameraInfo(UsbCameraInfo info) { return new PVUsbCameraInfo(info); } @@ -300,4 +370,8 @@ public static PVCameraInfo fromCSICameraInfo(String path, String baseName) { public static PVCameraInfo fromFileInfo(String path, String baseName) { return new PVFileCameraInfo(path, baseName); } + + public static PVCameraInfo fromBaslerCameraInfo(String serial, CameraModel model) { + return new PVBaslerCameraInfo(serial, model); + } } diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/QuirkyCamera.java b/photon-core/src/main/java/org/photonvision/vision/camera/QuirkyCamera.java index c500592227..6a3602c6b8 100644 --- a/photon-core/src/main/java/org/photonvision/vision/camera/QuirkyCamera.java +++ b/photon-core/src/main/java/org/photonvision/vision/camera/QuirkyCamera.java @@ -87,7 +87,10 @@ public class QuirkyCamera { CameraQuirk.ArduOV9782Controls), // Innomaker OV9281 new QuirkyCamera( - 0x0c45, 0x636d, "USB Camera", "Innomaker OV9281", CameraQuirk.InnoOV9281Controls)); + 0x0c45, 0x636d, "USB Camera", "Innomaker OV9281", CameraQuirk.InnoOV9281Controls), + + // Basler daA1280-54uc + new QuirkyCamera(-1, -1, "Basler daA1280-54uc", CameraQuirk.BaslerDaA1280Controls, CameraQuirk.ManualWB)); public static final QuirkyCamera DefaultCamera = new QuirkyCamera(0, 0, ""); public static final QuirkyCamera ZeroCopyPiCamera = diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/BaslerCameraSource.java b/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/BaslerCameraSource.java new file mode 100644 index 0000000000..8057080695 --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/BaslerCameraSource.java @@ -0,0 +1,125 @@ +package org.photonvision.vision.camera.baslerCameras; + +import edu.wpi.first.cscore.VideoMode; +import org.photonvision.common.configuration.CameraConfiguration; +import org.photonvision.common.logging.LogGroup; +import org.photonvision.common.logging.Logger; +import org.photonvision.vision.camera.CameraQuirk; +import org.photonvision.vision.camera.CameraType; +import org.photonvision.vision.camera.QuirkyCamera; +import org.photonvision.vision.frame.FrameProvider; +import org.photonvision.vision.frame.provider.BaslerFrameProvider; +import org.photonvision.vision.pipe.impl.PixelBinPipe; +import org.photonvision.vision.processes.VisionSource; +import org.photonvision.vision.processes.VisionSourceSettables; + +public class BaslerCameraSource extends VisionSource { + static final Logger logger = new Logger(BaslerCameraSource.class, LogGroup.Camera); + + private final GenericBaslerCameraSettables settables; + private BaslerFrameProvider frameProvider; + + private void onCameraConnected() { + settables.onCameraConnected(); + } + + public BaslerCameraSource(CameraConfiguration cameraConfiguration) { + super(cameraConfiguration); + if (cameraConfiguration.matchedCameraInfo.type() != CameraType.BaslerCamera) { + throw new IllegalArgumentException( + "BaslerCameraSource only accepts CameraConfigurations with type BaslerCamera"); + } + + this.getCameraConfiguration().cameraQuirks = + QuirkyCamera.getQuirkyCamera(-1, -1, cameraConfiguration.matchedCameraInfo.name()); + + this.settables = createSettables(cameraConfiguration); + // this.settables.setupVideoModes(); + this.frameProvider = new BaslerFrameProvider(this.settables, this::onCameraConnected); + // logger.debug(QuirkyCamera.getQuirkyCamera(-1, -1, + // cameraConfiguration.matchedCameraInfo.name()).toString()); + // this.getCameraConfiguration().cameraQuirks.quirks.put(CameraQuirk.Gain, true); + // this.getCameraConfiguration().cameraQuirks.quirks.put(CameraQuirk.AwbRedBlueGain, true); // + // Not really correct + } + + protected GenericBaslerCameraSettables createSettables(CameraConfiguration config) { + var quirks = getCameraConfiguration().cameraQuirks; + + GenericBaslerCameraSettables settables; + + if (quirks.hasQuirk(CameraQuirk.BaslerDaA1280Controls)) { + logger.info("Using Basler DaA1280 Settables"); + settables = new BaslerDaA1280CameraSettables(config); + } else { + logger.debug("Using generic basler settables"); + settables = new GenericBaslerCameraSettables(config); + } + + return settables; + } + + @Override + public void release() { + this.frameProvider.release(); + this.frameProvider = null; + } + + @Override + public FrameProvider getFrameProvider() { + return frameProvider; + } + + @Override + public VisionSourceSettables getSettables() { + return settables; + } + + @Override + public boolean isVendorCamera() { + return false; + } + + @Override + public boolean hasLEDs() { + return false; + } + + @Override + public void remakeSettables() { + // TODO: implement + } + + public static class BaslerVideoMode extends VideoMode { + public static class BinningConfig { + public final PixelBinPipe.PixelBinParams.BinMode mode; + public final int horz; + public final int vert; + + public BinningConfig(PixelBinPipe.PixelBinParams.BinMode mode, int horz, int vert) { + this.mode = mode; + this.horz = horz; + this.vert = vert; + } + + @Override + public String toString() { + return "BinningConfig[mode=" + + this.mode + + ",horz=" + + this.horz + + ",vert=" + + this.vert + + "]"; + } + } + + public final BinningConfig binningConfig; + + public BaslerVideoMode( + int pixelFormat, int width, int height, int fps, BinningConfig binningConfig) { + super(pixelFormat, width, height, fps); + this.binningConfig = binningConfig; + } + } +} diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/BaslerDaA1280CameraSettables.java b/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/BaslerDaA1280CameraSettables.java new file mode 100644 index 0000000000..e8397f8e1b --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/BaslerDaA1280CameraSettables.java @@ -0,0 +1,49 @@ +package org.photonvision.vision.camera.baslerCameras; + +import edu.wpi.first.util.PixelFormat; +import org.photonvision.common.configuration.CameraConfiguration; +import org.photonvision.vision.camera.CameraQuirk; +import org.photonvision.vision.camera.baslerCameras.BaslerCameraSource.BaslerVideoMode; +import org.photonvision.vision.camera.baslerCameras.BaslerCameraSource.BaslerVideoMode.BinningConfig; +import org.photonvision.vision.pipe.impl.PixelBinPipe.PixelBinParams.BinMode; + +public class BaslerDaA1280CameraSettables extends GenericBaslerCameraSettables { + + protected BaslerDaA1280CameraSettables(CameraConfiguration configuration) { + super(configuration); + + this.maxExposure = 1000; + this.maxGain = 18; + + this.getConfiguration().cameraQuirks.quirks.put(CameraQuirk.Gain, true); + this.getConfiguration().cameraQuirks.quirks.put(CameraQuirk.AwbRedBlueGain, true); + } + + @Override + protected void setupVideoModes() { + videoModes.put( + 0, + new BaslerVideoMode( + PixelFormat.kBGR.getValue(), 1280, 960, 43, new BinningConfig(BinMode.NONE, 0, 0))); + videoModes.put( + 1, + new BaslerVideoMode( + PixelFormat.kUYVY.getValue(), 1280, 960, 52, new BinningConfig(BinMode.NONE, 0, 0))); + videoModes.put( + 2, + new BaslerVideoMode( + PixelFormat.kBGR.getValue(), + 1280 / 2, + 960 / 2, + 43, + new BinningConfig(BinMode.SUM, 2, 2))); + videoModes.put( + 3, + new BaslerVideoMode( + PixelFormat.kBGR.getValue(), + 1280 / 2, + 960 / 2, + 43, + new BinningConfig(BinMode.AVERAGE, 2, 2))); + } +} diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/GenericBaslerCameraSettables.java b/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/GenericBaslerCameraSettables.java new file mode 100644 index 0000000000..5c340f8bfb --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/vision/camera/baslerCameras/GenericBaslerCameraSettables.java @@ -0,0 +1,245 @@ +package org.photonvision.vision.camera.baslerCameras; + +import edu.wpi.first.cscore.VideoMode; +import edu.wpi.first.math.MathUtil; + +import java.util.Arrays; +import java.util.HashMap; +import org.photonvision.common.configuration.CameraConfiguration; +import org.photonvision.common.util.math.MathUtils; +import org.photonvision.vision.camera.PVCameraInfo.PVBaslerCameraInfo; +import org.photonvision.vision.camera.baslerCameras.BaslerCameraSource.BaslerVideoMode; +import org.photonvision.vision.processes.VisionSourceSettables; +import org.teamdeadbolts.basler.BaslerJNI; + +public class GenericBaslerCameraSettables extends VisionSourceSettables { + public long ptr = 0; + public String serial; + + public final Object LOCK = new Object(); + + protected BaslerVideoMode currentVideoMode; + + protected double minExposure = -1; + protected double maxExposure = 1000; + + protected double minGain = 1; + protected double maxGain = 18; + + protected double minBrightness = -1; + protected double maxBrightness = 1; + + protected double minWBGain = 0; + protected double maxWBGain = 6; + + protected double lastExposure = -1; + protected int lastGain = -1; + protected double[] lastWBValues = new double[] {-1, 1, -1}; + + protected PVBaslerCameraInfo info; + + protected GenericBaslerCameraSettables(CameraConfiguration configuration) { + super(configuration); + if (!(configuration.matchedCameraInfo instanceof PVBaslerCameraInfo)) { + throw new IllegalArgumentException( + "Cannot create Basler Camera Settables from non basler info"); + } + + this.info = (PVBaslerCameraInfo) configuration.matchedCameraInfo; + this.serial = configuration.getDevicePath(); + } + + @Override + public void setExposureRaw(double exposureRaw) { + this.lastExposure = exposureRaw; + logger.debug("Setting exposure to " + exposureRaw); + boolean success = BaslerJNI.setExposure(ptr, exposureRaw * 1000); + if (!success) { + BaslerCameraSource.logger.warn("Failed to set exposure to " + exposureRaw); + } + } + + @Override + public void setAutoExposure(boolean cameraAutoExposure) { + logger.debug("Setting auto exposure to " + cameraAutoExposure); + + boolean success = BaslerJNI.setAutoExposure(ptr, cameraAutoExposure); + if (!success) { + BaslerCameraSource.logger.warn("Failed to set auto exposure to " + cameraAutoExposure); + } + if (!cameraAutoExposure) setExposureRaw(this.lastExposure); + } + + @Override + public void setWhiteBalanceTemp(double temp) { + throw new RuntimeException("Dont do this, use ratios insted"); + } + + @Override + public void setAutoWhiteBalance(boolean autowb) { + logger.debug("Setting auto white balance to " + autowb); + boolean success = BaslerJNI.setAutoWhiteBalance(ptr, autowb); + if (!success) { + BaslerCameraSource.logger.warn("Failed to set auto white balance to " + autowb); + } + + if (!autowb && lastWBValues[0] != -1 && lastWBValues[2] != -1) { + BaslerJNI.setWhiteBalance(ptr, lastWBValues); + } + } + + @Override + public void setBrightness(int brightness) { + logger.debug("Setting brightness to " + brightness); + + double scaledBrightness = + MathUtil.clamp( + MathUtils.map(brightness, 0, 101, minBrightness, maxBrightness), + minBrightness, + maxBrightness); + boolean success = BaslerJNI.setBrightness(ptr, scaledBrightness); + if (!success) { + logger.warn("Failed to set brightness to " + brightness + " (" + scaledBrightness + ")"); + } + } + + @Override + public void setGain(int gain) { + logger.debug("Setting gain to " + gain); + + // double min = BaslerJNI.getMinGain(ptr) + 1.0; // No divide by 0 + // double max = BaslerJNI.getMaxGain(ptr); + this.lastGain = gain; + boolean success = + BaslerJNI.setGain( + ptr, + MathUtil.clamp(MathUtils.map(gain, 0.0, 101.0, minGain, maxGain), minGain, maxGain)); + if (!success) { + logger.warn("Failed to set gain to " + gain); + } + } + + @Override + public void setRedGain(int red) { + double scaledRed = + MathUtil.clamp(MathUtils.map(red, 0.0, 101, minWBGain, maxWBGain), minWBGain, maxWBGain); + lastWBValues[0] = scaledRed; + BaslerJNI.setWhiteBalance(ptr, lastWBValues); + } + + @Override + public void setBlueGain(int blue) { + double scaledBlue = + MathUtil.clamp(MathUtils.map(blue, 0.0, 101, minWBGain, maxWBGain), minWBGain, maxWBGain); + lastWBValues[2] = scaledBlue; + logger.debug("WB: " + Arrays.toString(lastWBValues)); + BaslerJNI.setWhiteBalance(ptr, lastWBValues); + } + + @Override + public BaslerVideoMode getCurrentVideoMode() { + return currentVideoMode; + } + + public void setVideoMode(VideoMode mode) { + var bMode = (BaslerVideoMode) mode; + logger.info( + "Setting video mode to " + + "FPS: " + + mode.fps + + " Width: " + + mode.width + + " Height: " + + mode.height + + " Pixel Format: " + + mode.pixelFormat + + " Binning config: " + + bMode.binningConfig); + setVideoModeInternal(mode); + calculateFrameStaticProps(); + } + + @Override + protected void setVideoModeInternal(VideoMode videoMode) { + var mode = (BaslerVideoMode) videoMode; + synchronized (LOCK) { + if (ptr != 0) { + logger.debug("Stopping camera"); + if (!BaslerJNI.stopCamera(ptr)) { + logger.warn("Failed to stop camera when changing video mode"); + } + + logger.debug("Destroying camera"); + if (!BaslerJNI.destroyCamera(ptr)) { + logger.warn("Failed to destroy camera when changing video mode"); + } + } + + logger.debug("Creating camera"); + + ptr = BaslerJNI.createCamera(serial); + if (!BaslerJNI.setPixelFormat(ptr, mode.pixelFormat.getValue())) { + logger.warn("Failed to set pixel format"); + return; + } + if (this.lastGain != -1) { + this.setGain(this.lastGain); + } + if (this.lastExposure != -1) { + this.setExposureRaw(this.lastExposure); + } + + if (ptr == 0) { + logger.error("Failed to create camera when changing video mode"); + return; + } + + logger.debug("Starting camera"); + if (!BaslerJNI.startCamera(ptr)) { + logger.error("Failed to start camera when changing video mode"); + BaslerJNI.destroyCamera(ptr); + ptr = 0; + return; + } + } + + this.currentVideoMode = mode; + } + + @Override + public HashMap getAllVideoModes() { + return videoModes; + } + + @Override + public double getMinWhiteBalanceTemp() { + return 1; + } + + @Override + public double getMaxWhiteBalanceTemp() { + return 2; + } + + @Override + public double getMinExposureRaw() { + return minExposure; + } + + @Override + public double getMaxExposureRaw() { + return maxExposure; + } + + @Override + public void onCameraConnected() { + super.onCameraConnected(); + setupVideoModes(); + if (!videoModes.isEmpty()) this.currentVideoMode = (BaslerVideoMode) videoModes.get(0); + else logger.warn("Video modes empty"); + + calculateFrameStaticProps(); + } + + protected void setupVideoModes() {} +} diff --git a/photon-core/src/main/java/org/photonvision/vision/frame/provider/BaslerFrameProvider.java b/photon-core/src/main/java/org/photonvision/vision/frame/provider/BaslerFrameProvider.java new file mode 100644 index 0000000000..61f9ffb530 --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/vision/frame/provider/BaslerFrameProvider.java @@ -0,0 +1,103 @@ +package org.photonvision.vision.frame.provider; + +import edu.wpi.first.util.PixelFormat; +import edu.wpi.first.util.RawFrame; +import org.opencv.core.Mat; +import org.photonvision.common.logging.LogGroup; +import org.photonvision.common.logging.Logger; +import org.photonvision.common.util.math.MathUtils; +import org.photonvision.vision.camera.baslerCameras.BaslerCameraSource.BaslerVideoMode; +import org.photonvision.vision.camera.baslerCameras.GenericBaslerCameraSettables; +import org.photonvision.vision.opencv.CVMat; +import org.photonvision.vision.pipe.impl.PixelBinPipe; +import org.photonvision.vision.pipe.impl.PixelBinPipe.PixelBinParams; +import org.photonvision.vision.pipe.impl.PixelBinPipe.PixelBinParams.BinMode; +import org.teamdeadbolts.basler.BaslerJNI; + +public class BaslerFrameProvider extends CpuImageProcessor { + private final GenericBaslerCameraSettables settables; + + static final Logger logger = new Logger(BaslerFrameProvider.class, LogGroup.Camera); + + private PixelBinPipe pixelBinPipe = new PixelBinPipe(); + + private Runnable connectedCallback; + + public BaslerFrameProvider(GenericBaslerCameraSettables settables, Runnable connectedCallback) { + this.settables = settables; + this.connectedCallback = connectedCallback; + + // var vidMode = settables.getCurrentVideoMode(); + // settables.setVideoMode(vidMode); + + BaslerJNI.startCamera(settables.ptr); + } + + @Override + public String getName() { + return "BaslerCameraFrameProvider-" + this.settables.serial; + } + + @Override + public void release() { + BaslerJNI.stopCamera(settables.ptr); + BaslerJNI.destroyCamera(settables.ptr); + BaslerJNI.cleanUp(); + } + + @Override + public boolean isConnected() { + var serials = BaslerJNI.getConnectedCameras(); + for (String serial : serials) { + if (serial.equals(settables.serial)) { + return true; + } + } + + return false; + } + + @Override + public boolean checkCameraConnected() { + boolean connected = isConnected(); + if (connected && !cameraPropertiesCached) { + logger.info("Camera connected! running callback"); + onCameraConnected(); + } + + return connected; + } + + @Override + CapturedFrame getInputMat() { + if (!cameraPropertiesCached && isConnected()) { + onCameraConnected(); + } + var cameraMode = settables.getCurrentVideoMode(); + var frame = new RawFrame(); + frame.setInfo(cameraMode.width, cameraMode.height, cameraMode.width * 3, PixelFormat.kBGR); + + CVMat ret; + var start = MathUtils.wpiNanoTime(); + BaslerJNI.awaitNewFrame(settables.ptr); + Mat mat = new Mat(BaslerJNI.takeFrame(settables.ptr)); + BaslerVideoMode.BinningConfig binningConfig = + this.settables.getCurrentVideoMode().binningConfig; + if (binningConfig.mode != BinMode.NONE) { + pixelBinPipe.setParams( + new PixelBinParams(binningConfig.mode, binningConfig.horz, binningConfig.vert)); + pixelBinPipe.run(mat); + } + + ret = new CVMat(mat, frame); + + return new CapturedFrame( + ret, settables.getFrameStaticProperties(), start); // TODO: Timestamping is kinda off rn + } + + @Override + public void onCameraConnected() { + super.onCameraConnected(); + this.connectedCallback.run(); + } +} diff --git a/photon-core/src/main/java/org/photonvision/vision/pipe/impl/PixelBinPipe.java b/photon-core/src/main/java/org/photonvision/vision/pipe/impl/PixelBinPipe.java new file mode 100644 index 0000000000..902bdcb307 --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/vision/pipe/impl/PixelBinPipe.java @@ -0,0 +1,32 @@ +package org.photonvision.vision.pipe.impl; + +import org.opencv.core.Mat; +import org.photonvision.vision.pipe.MutatingPipe; +import org.teamdeadbolts.basler.BaslerJNI; + +public class PixelBinPipe extends MutatingPipe { + + @Override + protected Void process(Mat in) { + switch (params.mode) { + case AVERAGE: + BaslerJNI.avgBin(in, params.binHorz(), params.binVert()); + break; + case SUM: + BaslerJNI.sumBin(in, params.binHorz(), params.binVert()); + break; + case NONE: + break; + } + + return null; + } + + public static record PixelBinParams(BinMode mode, int binHorz, int binVert) { + public enum BinMode { + SUM, + AVERAGE, + NONE, + } + } +} diff --git a/photon-core/src/main/java/org/photonvision/vision/pipeline/AprilTagPipeline.java b/photon-core/src/main/java/org/photonvision/vision/pipeline/AprilTagPipeline.java index 3e98f7e9e1..ec8f2c296b 100644 --- a/photon-core/src/main/java/org/photonvision/vision/pipeline/AprilTagPipeline.java +++ b/photon-core/src/main/java/org/photonvision/vision/pipeline/AprilTagPipeline.java @@ -136,6 +136,7 @@ protected CVPipelineResult process(Frame frame, AprilTagPipelineSettings setting CVPipeResult> tagDetectionPipeResult = aprilTagDetectionPipe.run(frame.processedImage); + sumPipeNanosElapsed += tagDetectionPipeResult.nanosElapsed; List detections = tagDetectionPipeResult.output; diff --git a/photon-core/src/main/java/org/photonvision/vision/pipeline/CVPipelineSettings.java b/photon-core/src/main/java/org/photonvision/vision/pipeline/CVPipelineSettings.java index a512e1fcf9..a93a0ca86c 100644 --- a/photon-core/src/main/java/org/photonvision/vision/pipeline/CVPipelineSettings.java +++ b/photon-core/src/main/java/org/photonvision/vision/pipeline/CVPipelineSettings.java @@ -49,7 +49,7 @@ public class CVPipelineSettings implements Cloneable { // Currently only used by a few cameras (notably the zero-copy Pi Camera driver) with the Gain // quirk public int cameraGain = 75; - // Currently only used by the zero-copy Pi Camera driver + // Currently only used by the zero-copy Pi Camera driver and some Basler Cameras public int cameraRedGain = 11; public int cameraBlueGain = 20; public int cameraVideoModeIndex = 0; @@ -61,6 +61,7 @@ public class CVPipelineSettings implements Cloneable { public boolean cameraAutoWhiteBalance = false; public double cameraWhiteBalanceTemp = 4000; + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/photon-core/src/main/java/org/photonvision/vision/pipeline/OutputStreamPipeline.java b/photon-core/src/main/java/org/photonvision/vision/pipeline/OutputStreamPipeline.java index 366ef1a0a2..32fabdc894 100644 --- a/photon-core/src/main/java/org/photonvision/vision/pipeline/OutputStreamPipeline.java +++ b/photon-core/src/main/java/org/photonvision/vision/pipeline/OutputStreamPipeline.java @@ -144,6 +144,7 @@ public CVPipelineResult process( } // Draw 2D Crosshair on output + var draw2dCrosshairResultOnInput = draw2dCrosshairPipe.run(Pair.of(inMat, targetsToDraw)); sumPipeNanosElapsed += pipeProfileNanos[3] = draw2dCrosshairResultOnInput.nanosElapsed; diff --git a/photon-core/src/main/java/org/photonvision/vision/processes/VisionModule.java b/photon-core/src/main/java/org/photonvision/vision/processes/VisionModule.java index f3ebab44b8..379081ffa1 100644 --- a/photon-core/src/main/java/org/photonvision/vision/processes/VisionModule.java +++ b/photon-core/src/main/java/org/photonvision/vision/processes/VisionModule.java @@ -46,6 +46,7 @@ import org.photonvision.vision.camera.CameraQuirk; import org.photonvision.vision.camera.CameraType; import org.photonvision.vision.camera.QuirkyCamera; +import org.photonvision.vision.camera.baslerCameras.BaslerCameraSource.BaslerVideoMode; import org.photonvision.vision.camera.csi.LibcameraGpuSource; import org.photonvision.vision.frame.Frame; import org.photonvision.vision.frame.consumer.FileSaveFrameConsumer; @@ -125,6 +126,16 @@ public VisionModule(PipelineManager pipelineManager, VisionSource visionSource) }); } + // Basler stuff + if (cameraQuirks.hasQuirk(CameraQuirk.ManualWB)) { + pipelineManager.userPipelineSettings.forEach( + it -> { + it.cameraWhiteBalanceTemp = -1; + if (it.cameraRedGain == -1) it.cameraRedGain = 11; + if (it.cameraBlueGain == -1) it.cameraBlueGain = 20; + }); + } + this.pipelineManager = pipelineManager; this.visionSource = visionSource; changeSubscriber = new VisionModuleChangeSubscriber(this); @@ -481,13 +492,19 @@ boolean setPipeline(int index) { if (pipelineSettings.cameraBlueGain == -1) pipelineSettings.cameraBlueGain = 20; settables.setRedGain(Math.max(0, pipelineSettings.cameraRedGain)); settables.setBlueGain(Math.max(0, pipelineSettings.cameraBlueGain)); + + if (cameraQuirks.hasQuirk(CameraQuirk.ManualWB)) { + settables.setAutoWhiteBalance(pipelineSettings.cameraAutoWhiteBalance); + } } else { pipelineSettings.cameraRedGain = -1; pipelineSettings.cameraBlueGain = -1; // All other cameras (than picams) should support AWB temp settables.setWhiteBalanceTemp(pipelineSettings.cameraWhiteBalanceTemp); - settables.setAutoWhiteBalance(pipelineSettings.cameraAutoWhiteBalance); + + if (!cameraQuirks.hasQuirk(CameraQuirk.BaslerDaA1280Controls)) + settables.setAutoWhiteBalance(pipelineSettings.cameraAutoWhiteBalance); } setVisionLEDs(pipelineSettings.ledMode); @@ -590,7 +607,13 @@ public UICameraConfiguration toUICameraConfig() { ? "kPicam" : k.getValue().pixelFormat.toString()) .substring(1)); // Remove the k prefix - + if (k.getValue() instanceof BaslerVideoMode) { + var bMode = (BaslerVideoMode) k.getValue(); + logger.debug("Have basler binning: " + bMode.binningConfig.mode); + internalMap.put("binningMode", bMode.binningConfig.mode.toString()); + internalMap.put("binningHorz", bMode.binningConfig.horz); + internalMap.put("binningVert", bMode.binningConfig.vert); + } temp.put(k.getKey(), internalMap); } diff --git a/photon-core/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java b/photon-core/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java index 3299082582..47cda6b075 100644 --- a/photon-core/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java +++ b/photon-core/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java @@ -45,7 +45,10 @@ import org.photonvision.vision.camera.FileVisionSource; import org.photonvision.vision.camera.PVCameraInfo; import org.photonvision.vision.camera.USBCameras.USBCameraSource; +import org.photonvision.vision.camera.baslerCameras.BaslerCameraSource; import org.photonvision.vision.camera.csi.LibcameraGpuSource; +import org.teamdeadbolts.basler.BaslerJNI; +import org.teamdeadbolts.basler.BaslerJNI.CameraModel; /** * This class manages starting up VisionModules for serialized devices ({@link @@ -311,6 +314,16 @@ protected List getConnectedCameras() { }) .forEach(cameraInfos::add); } + if (BaslerJNI.isSupported()) { + Stream.of(BaslerJNI.getConnectedCameras()) + .map( + serial -> { + CameraModel model = BaslerJNI.getCameraModel(serial); + return PVCameraInfo.fromBaslerCameraInfo(serial, model); + }) + .forEach(cameraInfos::add); + ; + } // FileVisionSources are a bit quirky. They aren't enumerated by the above, but I still want my // UI to look like it ought to work @@ -503,6 +516,7 @@ protected VisionSource loadVisionSourceFromCamConfig(CameraConfiguration configu case UsbCamera -> new USBCameraSource(configuration); case ZeroCopyPicam -> new LibcameraGpuSource(configuration); case FileCamera -> new FileVisionSource(configuration); + case BaslerCamera -> new BaslerCameraSource(configuration); }; if (source.getFrameProvider() == null) { diff --git a/photon-server/src/main/java/org/photonvision/Main.java b/photon-server/src/main/java/org/photonvision/Main.java index 130bd25ba8..863c9b0a21 100644 --- a/photon-server/src/main/java/org/photonvision/Main.java +++ b/photon-server/src/main/java/org/photonvision/Main.java @@ -38,6 +38,7 @@ import org.photonvision.common.logging.PvCSCoreLogger; import org.photonvision.common.networking.NetworkManager; import org.photonvision.common.util.TestUtils; +import org.photonvision.jni.BaslerCameraJNI; import org.photonvision.jni.LibraryLoader; import org.photonvision.jni.RknnDetectorJNI; import org.photonvision.jni.RubikDetectorJNI; @@ -229,6 +230,19 @@ public static void main(String[] args) { } catch (IOException e) { logger.error("Failed to load libcamera-JNI!", e); } + + try { + if (Platform.getCurrentPlatform() == Platform.LINUX_64) { + BaslerCameraJNI.forceLoad(); + if (BaslerCameraJNI.getInstance().isLoaded()) { + logger.info("BaslerCameraJNI loaded successfully."); + } else { + logger.error("Failed to load basler JNI!"); + } + } + } catch (IOException e) { + logger.error("Failed to load basler JNI!", e); + } try { if (Platform.isRK3588()) { RknnDetectorJNI.forceLoad(); diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000000..945602b75c --- /dev/null +++ b/shell.nix @@ -0,0 +1,84 @@ +{ pkgs ? import {} }: +let + ade = pkgs.stdenv.mkDerivation rec { + pname = "ade"; + version = "0.1.2e"; + + src = pkgs.fetchFromGitHub { + owner = "opencv"; + repo = "ade"; + rev = "v${version}"; + hash = "sha256-1z5ChmXyanEghBLpopJlRIjOMu+GFAON0X8K2ZhYVlA="; + }; + + nativeBuildInputs = [ pkgs.cmake ]; + + cmakeFlags = [ + "-DBUILD_SHARED_LIBS=OFF" + ]; + }; + + + opencv4100 = pkgs.opencv4.overrideAttrs (oldAttr: rec { + version = "4.10.0"; + src = pkgs.fetchFromGitHub { + owner = "opencv"; + repo = "opencv"; + rev = version; + hash = "sha256-s+KvBrV/BxrxEvPhHzWCVFQdUQwhUdRJyb0wcGDFpeo=" ; + }; + + buildInputs = (oldAttr.buildInputs or []) ++ [ ade ]; + + nativeBuildInputs = + oldAttr.nativeBuildInputs + ++ (with pkgs; [ + ant + openjdk + python3 + python3Packages.numpy + ]); + + cmakeFlags = + oldAttr.cmakeFlags + ++ [ + "-DBUILD_JAVA=ON" + "-DBUILD_opencv_dnn=OFF" + "-DBUILD_opencv_gapi=ON" + "-DWITH_ADE=ON" + "-Dade_DIR=${ade}/lib/cmake/ade" + ]; + + postInstall = (oldAttr.postInstall or "") + '' + cd $out/lib + for lib in libopencv_*.so.4.10.0; do + if [ -f "$lib" ]; then + base=$(basename "$lib" .so.4.10.0) + ln -sf "$lib" "$base.so.4.10" + fi + done + ''; + }); + + buildInputs = with pkgs; [ + openjdk17 + cmake + opencv4100 + clang + lapack + suitesparse + pnpm + re2 + ]; +in +pkgs.mkShell { + buildInputs = buildInputs; + + shellHook = '' + + export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath buildInputs}:$LD_LIBRARY_PATH + + export LD_LIBRARY_PATH=${opencv4100}/share/java/opencv4:$LD_LIBRARY_PATH + export JAVA_HOME=${pkgs.openjdk17} + ''; +} \ No newline at end of file