Skip to content

Commit 2f17a31

Browse files
committed
Replace AprilTagPlugin with WPILib's apriltag support, keep backwards compatibility
1 parent c215ac1 commit 2f17a31

27 files changed

Lines changed: 684 additions & 220 deletions

EOCV-Sim/build.gradle

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,37 @@ dependencies {
113113
api wpilibTools.deps.wpilibOpenCvJava(opencvVersion)
114114
wpilibNatives wpilibTools.deps.wpilibOpenCv(opencvVersion)
115115

116-
// use cscore for webcams
116+
// WPILib AprilTag replaces AprilTagDesktop
117+
api wpilibTools.deps.wpilibJava("apriltag")
118+
wpilibNatives wpilibTools.deps.wpilib("apriltag")
119+
120+
// WPILib cscore replaces steve
117121
implementation wpilibTools.deps.wpilibJava("cscore")
118122
wpilibNatives wpilibTools.deps.wpilib("cscore")
123+
119124
// cscore depends on these
120125
implementation wpilibTools.deps.wpilibJava("wpinet")
121126
wpilibNatives wpilibTools.deps.wpilib("wpinet")
127+
122128
implementation wpilibTools.deps.wpilibJava("wpiutil")
123129
wpilibNatives wpilibTools.deps.wpilib("wpiutil")
124130

131+
// needed for apriltags
132+
implementation wpilibTools.deps.wpilibJava("wpimath")
133+
wpilibNatives wpilibTools.deps.wpilib("wpimath")
134+
135+
// we don't use this at all... But if we don't have it in the classpath,
136+
// we can't use WPILib's Geometry classes for apriltags.
137+
implementation "us.hebi.quickbuf:quickbuf-runtime:$quickbufVersion"
138+
125139
implementation "org.slf4j:slf4j-api:$slf4j_version"
126140
implementation "org.apache.logging.log4j:log4j-api:$log4j_version"
127141
implementation 'org.apache.logging.log4j:log4j-core:2.25.4'
128142
implementation "org.apache.logging.log4j:log4j-slf4j2-impl:$log4j_version"
129143

130144
implementation "info.picocli:picocli:$picocli_version"
131-
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
132-
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2'
145+
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
146+
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"
133147
implementation "io.github.classgraph:classgraph:$classgraph_version"
134148

135149
implementation "com.formdev:flatlaf:$flatlaf_version"
@@ -186,7 +200,6 @@ public final class Build {
186200
public static final String packagePlatform = "${wpilibTools.currentPlatform.platformName}";
187201
188202
public static final String opencvVersion = "$opencvVersion";
189-
public static final String apriltagPluginVersion = "$apriltag_plugin_version";
190203
public static final String paperVisionVersion = "$papervision_version";
191204
}
192205
"""

EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,4 +461,4 @@ class Visualizer : PhaseOrchestrableBase(), KoinComponent {
461461
}
462462
}
463463
}
464-
464+

EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
/*
2-
* Copyright (c) 2026 Sebastian Erives
3-
* Licensed under the MIT License.
4-
*/
5-
1+
/*
2+
* Copyright (c) 2026 Sebastian Erives
3+
* Licensed under the MIT License.
4+
*/
5+
66
package com.github.serivesmejia.eocvsim.gui.dialog.source
77

88
import com.github.serivesmejia.eocvsim.gui.Visualizer
@@ -196,7 +196,6 @@ class CreateCameraSource : KoinComponent {
196196
val info = cameraInfos.getOrNull(index) ?: return
197197

198198
try {
199-
200199
val cam = UsbCamera(info.name, info.dev)
201200

202201
modes = cam.enumerateVideoModes()

EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ abstract class InputSource : Comparable<InputSource>, KoinComponent {
5353
abstract fun onPause()
5454
abstract fun onResume()
5555

56-
open fun setSize(size: Size) {}
57-
58-
open fun getSize() = Size()
56+
abstract val sourceSize: Size
5957

6058
open fun update(): Mat? = null
6159

@@ -77,4 +75,4 @@ abstract class InputSource : Comparable<InputSource>, KoinComponent {
7775
}
7876

7977
}
80-
78+

EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.kt

Lines changed: 51 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,25 @@ class CameraSource : InputSource, KoinComponent {
3535
@JvmStatic var currentWebcamIndex = -1
3636
}
3737

38-
override val hasSlowInitialization: Boolean get() = true
38+
override val hasSlowInitialization = true
3939

40-
@JsonProperty @JvmField var cameraPortIndex: Int = -1
41-
@JsonProperty @JvmField var exactPortMatch: Boolean = false
40+
@JsonProperty @JvmField var cameraPortIndex = -1
41+
@JsonProperty @JvmField var exactPortMatch = false
4242
@JsonProperty @JvmField var vendorId: Int? = null
4343
@JsonProperty @JvmField var productId: Int? = null
44-
@Transient var webcamName: String = ""
44+
@Transient var webcamName = ""
4545

4646
@JsonProperty @JvmField var videoMode: VideoMode? = null
4747

4848
@Transient var camera: UsbCamera? = null
4949
private set
5050

5151
@Transient private var cvSink: CvSink? = null
52-
5352
@Transient private var lastFrame = Mat()
54-
5553
@Transient private var initialized = false
56-
5754
@Transient var isLegacyByIndex = false
55+
@Transient private var capTimeNanos = 0L
5856

59-
@Transient private var capTimeNanos: Long = 0
6057
private val configManager: ConfigManager by inject()
6158
private val logger by loggerForThis()
6259

@@ -96,94 +93,65 @@ class CameraSource : InputSource, KoinComponent {
9693
this.isLegacyByIndex = true
9794
}
9895

99-
override fun setSize(size: Size) {
100-
// deprecated concept now; derived from VideoMode
101-
}
102-
103-
override fun getSize(): Size =
104-
videoMode?.let { Size(it.width.toDouble(), it.height.toDouble()) } ?: Size()
96+
override val sourceSize: Size
97+
get() = videoMode?.let { Size(it.width.toDouble(), it.height.toDouble()) } ?: Size()
10598

10699
override fun init(): Boolean {
107100
if (initialized) return false
108101
initialized = true
109102

103+
val cameras by lazy { UsbCamera.enumerateUsbCameras() }
104+
110105
val matchedInfo = when {
111-
exactPortMatch -> {
112-
val infos = UsbCamera.enumerateUsbCameras()
113-
infos.firstOrNull {
114-
cameraPortIndex >= 0 &&
106+
exactPortMatch -> cameras.firstOrNull {
107+
cameraPortIndex >= 0 &&
115108
it.dev == cameraPortIndex &&
116109
vendorId != null && productId != null &&
117110
it.vendorId == vendorId &&
118111
it.productId == productId
119-
} ?: run {
120-
logger.error("Camera not found on the same connection: $cameraPortIndex")
112+
} ?: run {
113+
logger.error("Camera not found on the same connection: $cameraPortIndex")
114+
return false
115+
}
116+
117+
cameraPortIndex >= 0 -> cameras.firstOrNull { it.dev == cameraPortIndex }
118+
?: run {
119+
logger.error("Camera not found on port: $cameraPortIndex")
121120
return false
122121
}
123-
}
124122

125-
cameraPortIndex >= 0 -> {
126-
val infos = UsbCamera.enumerateUsbCameras()
127-
infos.firstOrNull { it.dev == cameraPortIndex }
128-
?: run {
129-
logger.error("Camera not found on port: $cameraPortIndex")
130-
return false
131-
}
123+
vendorId != null && productId != null -> cameras.firstOrNull {
124+
it.vendorId == vendorId && it.productId == productId
125+
} ?: run {
126+
logger.error("Camera not found by VID/PID: $vendorId:$productId")
127+
return false
132128
}
133129

134-
vendorId != null && productId != null -> {
135-
val infos = UsbCamera.enumerateUsbCameras()
136-
infos.firstOrNull {
137-
it.vendorId == vendorId && it.productId == productId
138-
} ?: run {
139-
logger.error("Camera not found by VID/PID: ${vendorId}:${productId}")
130+
webcamName.isNotEmpty() -> cameras.firstOrNull { it.name == webcamName }
131+
?: run {
132+
logger.error("Camera not found: $webcamName")
140133
return false
141134
}
142-
}
143-
144-
webcamName.isNotEmpty() -> {
145-
val infos = UsbCamera.enumerateUsbCameras()
146-
infos.firstOrNull { it.name == webcamName }
147-
?: run {
148-
logger.error("Camera not found: $webcamName")
149-
return false
150-
}
151-
}
152135

153136
else -> null
154137
}
155138

156-
val cam = if (matchedInfo != null) {
139+
camera = if (matchedInfo != null) {
157140
webcamName = matchedInfo.name
158141
UsbCamera(matchedInfo.name, matchedInfo.dev)
159142
} else {
160143
UsbCamera("$cameraPortIndex", cameraPortIndex)
161144
}
162145

163-
camera = cam
164-
165-
val desiredMode = videoMode
166-
167-
if (desiredMode != null) {
168-
cam.videoMode = desiredMode
169-
} else {
170-
val mode = cam.videoMode
171-
cam.videoMode = mode
172-
}
173-
174-
val mode = cam.videoMode
146+
camera!!.videoMode = videoMode ?: camera!!.videoMode
175147

176-
logger.info(
177-
"Camera started: ${matchedInfo?.name ?: webcamName.ifEmpty { "Camera $cameraPortIndex" }} ${mode?.stringify()}"
178-
)
148+
logger.info("Camera started: ${matchedInfo?.name ?: webcamName.ifEmpty { "Camera $cameraPortIndex" }} ${camera!!.videoMode?.stringify()}")
179149

180150
cvSink = CvSink("eocvsim_sink_$cameraPortIndex", PixelFormat.BGR).also {
181-
it.source = cam
151+
it.source = camera
182152
}
183153

184-
val ok = cvSink!!.grabFrame(lastFrame, configManager.config.webcamOpenTimeoutSec)
185-
186-
if (ok == 0L || lastFrame.empty()) {
154+
if (cvSink!!.grabFrame(lastFrame, configManager.config.webcamOpenTimeoutSec) == 0L || lastFrame.empty()) {
187155
logger.error("Failed to open camera: ${cvSink!!.error}")
188156
return false
189157
}
@@ -194,61 +162,51 @@ class CameraSource : InputSource, KoinComponent {
194162

195163
override fun reset() {
196164
if (!initialized) return
197-
198-
cvSink?.close()
199-
cvSink = null
200-
201-
camera?.close()
202-
camera = null
203-
165+
teardown()
204166
lastFrame.release()
205-
206167
initialized = false
207168
}
208169

209-
override fun close() {
210-
cvSink?.close()
211-
camera?.close()
212-
currentWebcamIndex = -1
213-
}
170+
override fun close() = teardown()
214171

215172
override fun update(): Mat {
216173
if (isPaused) return lastFrame
217174

218-
val grabTime = cvSink?.grabFrame(lastFrame, configManager.config.webcamNewFrameTimeoutSec) ?: 0L
175+
capTimeNanos = cvSink?.grabFrame(lastFrame, configManager.config.webcamNewFrameTimeoutSec) ?: 0L
219176

220-
if(lastFrame.empty()) {
221-
return lastFrame
177+
if (!lastFrame.empty()) {
178+
Imgproc.cvtColor(lastFrame, lastFrame, Imgproc.COLOR_BGR2RGBA)
222179
}
223180

224-
capTimeNanos = grabTime
225-
Imgproc.cvtColor(lastFrame, lastFrame, Imgproc.COLOR_BGR2RGBA)
226181
return lastFrame
227182
}
228183

229184
override fun onPause() {
230185
cvSink?.grabFrame(lastFrame, configManager.config.webcamNewFrameTimeoutSec)
231-
cvSink?.close()
232-
camera?.close()
233-
currentWebcamIndex = -1
186+
teardown()
234187
}
235188

236189
override fun onResume() {
237190
InputSourceInitializer.runWithTimeout(this) { init() }
238191
}
239192

193+
private fun teardown() {
194+
cvSink?.close()
195+
cvSink = null
196+
camera?.close()
197+
camera = null
198+
currentWebcamIndex = -1
199+
}
200+
240201
override fun internalCloneSource(): InputSource =
241-
if (isLegacyByIndex) {
242-
CameraSource(cameraPortIndex, videoMode)
243-
} else {
244-
CameraSource(webcamName, cameraPortIndex, exactPortMatch, vendorId, productId, videoMode)
245-
}
202+
if (isLegacyByIndex) CameraSource(cameraPortIndex, videoMode)
203+
else CameraSource(webcamName, cameraPortIndex, exactPortMatch, vendorId, productId, videoMode)
246204

247-
override val fileFilters: FileFilter? get() = null
248-
override val captureTimeNanos: Long get() = capTimeNanos
205+
override val fileFilters: FileFilter? = null
206+
override val captureTimeNanos get() = capTimeNanos
249207

250208
override fun toString() =
251209
"CameraSource($webcamName, port=$cameraPortIndex, exactPortMatch=$exactPortMatch, vid=$vendorId, pid=$productId, ${videoMode?.stringify()})"
252210

253211
private fun VideoMode.stringify() = "${width}x${height}@${fps} $pixelFormat"
254-
}
212+
}

EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/HttpSource.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.github.deltacv.common.util.loggerForThis
1414
import org.koin.core.component.KoinComponent
1515
import org.koin.core.component.inject
1616
import org.opencv.core.Mat
17+
import org.opencv.core.Size
1718
import org.opencv.imgproc.Imgproc
1819
import org.wpilib.vision.camera.CvSink
1920
import org.wpilib.vision.camera.HttpCamera
@@ -110,11 +111,14 @@ class HttpSource @JvmOverloads constructor (
110111

111112
override fun internalCloneSource(): InputSource = HttpSource(url)
112113

114+
override val sourceSize: Size
115+
get() = cvSink?.directMat?.size() ?: Size()
116+
113117
override val fileFilters: FileFilter? get() = null
114118
override val captureTimeNanos: Long get() = capTimeNanos
115119

116120
override fun toString(): String {
117121
return "HttpSource($url)"
118122
}
119123
}
120-
124+

0 commit comments

Comments
 (0)