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.processes; 019 020import java.lang.reflect.Field; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Comparator; 024import java.util.List; 025import org.photonvision.common.configuration.CameraConfiguration; 026import org.photonvision.common.configuration.ConfigManager; 027import org.photonvision.common.dataflow.DataChangeService; 028import org.photonvision.common.dataflow.events.OutgoingUIEvent; 029import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration; 030import org.photonvision.common.logging.LogGroup; 031import org.photonvision.common.logging.Logger; 032import org.photonvision.vision.pipeline.*; 033 034@SuppressWarnings({"rawtypes", "unused"}) 035public class PipelineManager { 036 private static final Logger logger = new Logger(PipelineManager.class, LogGroup.VisionModule); 037 038 public static final int DRIVERMODE_INDEX = -1; 039 public static final int CAL_3D_INDEX = -2; 040 041 protected final List<CVPipelineSettings> userPipelineSettings; 042 protected final Calibrate3dPipeline calibration3dPipeline; 043 protected final DriverModePipeline driverModePipeline = new DriverModePipeline(); 044 045 /** Index of the currently active pipeline. Defaults to 0. */ 046 private int currentPipelineIndex = DRIVERMODE_INDEX; 047 048 /** The currently active pipeline. */ 049 private CVPipeline currentUserPipeline = driverModePipeline; 050 051 /** 052 * Index of the last active user-created pipeline. <br> 053 * <br> 054 * Used only when switching from any of the built-in pipelines back to a user-created pipeline. 055 */ 056 private int lastUserPipelineIdx; 057 058 /** 059 * Creates a PipelineManager with a DriverModePipeline, a Calibration3dPipeline, and all provided 060 * pipelines. 061 */ 062 PipelineManager( 063 DriverModePipelineSettings driverSettings, 064 List<CVPipelineSettings> userPipelines, 065 int defaultIndex) { 066 this.userPipelineSettings = new ArrayList<>(userPipelines); 067 // This is to respect the default res idx for vendor cameras 068 069 this.driverModePipeline.setSettings(driverSettings); 070 071 if (userPipelines.isEmpty()) addPipeline(PipelineType.AprilTag); 072 073 calibration3dPipeline = new Calibrate3dPipeline(); 074 075 // We know that at this stage, VisionRunner hasn't yet started so we're good to 076 // do this from 077 // this thread 078 this.setIndex(defaultIndex); 079 updatePipelineFromRequested(); 080 } 081 082 public PipelineManager(CameraConfiguration config) { 083 this(config.driveModeSettings, config.pipelineSettings, config.currentPipelineIndex); 084 } 085 086 /** 087 * Get the settings for a pipeline by index. 088 * 089 * @param index Index of pipeline whose settings need getting. 090 * @return The gotten settings of the pipeline whose index was provided. 091 */ 092 public CVPipelineSettings getPipelineSettings(int index) { 093 if (index < 0) { 094 switch (index) { 095 case DRIVERMODE_INDEX: 096 return driverModePipeline.getSettings(); 097 case CAL_3D_INDEX: 098 return calibration3dPipeline.getSettings(); 099 } 100 } 101 102 for (var setting : userPipelineSettings) { 103 if (setting.pipelineIndex == index) return setting; 104 } 105 return null; 106 } 107 108 /** 109 * Get the settings for a pipeline by index. 110 * 111 * @param index Index of pipeline whose nickname needs getting. 112 * @return the nickname of the pipeline whose index was provided. 113 */ 114 public String getPipelineNickname(int index) { 115 if (index < 0) { 116 switch (index) { 117 case DRIVERMODE_INDEX: 118 return driverModePipeline.getSettings().pipelineNickname; 119 case CAL_3D_INDEX: 120 return calibration3dPipeline.getSettings().pipelineNickname; 121 } 122 } 123 124 for (var setting : userPipelineSettings) { 125 if (setting.pipelineIndex == index) return setting.pipelineNickname; 126 } 127 return null; 128 } 129 130 /** 131 * Gets a list of nicknames for all user pipelines 132 * 133 * @return The list of nicknames for all user pipelines 134 */ 135 public List<String> getPipelineNicknames() { 136 List<String> ret = new ArrayList<>(); 137 for (var p : userPipelineSettings) { 138 ret.add(p.pipelineNickname); 139 } 140 return ret; 141 } 142 143 /** 144 * Gets the index of the currently active pipeline 145 * 146 * @return The index of the currently active pipeline 147 */ 148 public int getCurrentPipelineIndex() { 149 return currentPipelineIndex; 150 } 151 152 /** 153 * Get the currently active pipeline. 154 * 155 * @return The currently active pipeline. 156 */ 157 public CVPipeline getCurrentPipeline() { 158 updatePipelineFromRequested(); 159 if (currentPipelineIndex < 0) { 160 switch (currentPipelineIndex) { 161 case CAL_3D_INDEX: 162 return calibration3dPipeline; 163 case DRIVERMODE_INDEX: 164 return driverModePipeline; 165 } 166 } 167 168 // Just return the current user pipeline, we're not on aa built-in one 169 return currentUserPipeline; 170 } 171 172 /** 173 * Get the currently active pipelines settings 174 * 175 * @return The currently active pipelines settings 176 */ 177 public CVPipelineSettings getCurrentPipelineSettings() { 178 return getPipelineSettings(currentPipelineIndex); 179 } 180 181 private volatile int requestedIndex = 0; 182 183 /** 184 * Grab the currently requested pipeline index. The VisionRunner may not have changed over to this 185 * pipeline yet. 186 */ 187 public int getRequestedIndex() { 188 return requestedIndex; 189 } 190 191 /** 192 * Internal method for setting the active pipeline. <br> 193 * <br> 194 * All externally accessible methods that intend to change the active pipeline MUST go through 195 * here to ensure all proper steps are taken. 196 * 197 * @param newIndex Index of pipeline to be active 198 */ 199 private void setPipelineInternal(int newIndex) { 200 requestedIndex = newIndex; 201 } 202 203 /** 204 * Based on a requested pipeline index, create/destroy pipelines as necessary. We do this as a 205 * side effect of the main thread that calls getCurrentPipeline to avoid race conditions between 206 * server threads and the VisionRunner TODO: this should be refactored. Shame Java doesn't have 207 * RAII 208 */ 209 private void updatePipelineFromRequested() { 210 int newIndex = requestedIndex; 211 if (newIndex == currentPipelineIndex) { 212 // nothing to do, probably no change -- give up 213 return; 214 } 215 216 if (newIndex < 0 && currentPipelineIndex >= 0) { 217 // Transitioning to a built-in pipe, save off the current user one 218 lastUserPipelineIdx = currentPipelineIndex; 219 } 220 221 if (userPipelineSettings.size() - 1 < newIndex) { 222 logger.warn("User attempted to set index to non-existent pipeline!"); 223 return; 224 } 225 226 currentPipelineIndex = newIndex; 227 228 if (newIndex >= 0) { 229 recreateUserPipeline(); 230 } 231 232 DataChangeService.getInstance() 233 .publishEvent( 234 new OutgoingUIEvent<>( 235 "fullsettings", 236 UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig()))); 237 } 238 239 /** 240 * Recreate the current user pipeline with the current pipeline index. Useful to force a 241 * recreation after changing pipeline type 242 */ 243 private void recreateUserPipeline() { 244 // Cleanup potential old native resources before swapping over from a user pipeline 245 if (currentUserPipeline != null && !(currentPipelineIndex < 0)) { 246 currentUserPipeline.release(); 247 } 248 249 var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex); 250 switch (desiredPipelineSettings.pipelineType) { 251 case Reflective -> { 252 logger.debug("Creating Reflective pipeline"); 253 currentUserPipeline = 254 new ReflectivePipeline((ReflectivePipelineSettings) desiredPipelineSettings); 255 } 256 case ColoredShape -> { 257 logger.debug("Creating ColoredShape pipeline"); 258 currentUserPipeline = 259 new ColoredShapePipeline((ColoredShapePipelineSettings) desiredPipelineSettings); 260 } 261 case AprilTag -> { 262 logger.debug("Creating AprilTag pipeline"); 263 currentUserPipeline = 264 new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings); 265 } 266 case Aruco -> { 267 logger.debug("Creating Aruco Pipeline"); 268 currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings); 269 } 270 case ObjectDetection -> { 271 logger.debug("Creating ObjectDetection Pipeline"); 272 currentUserPipeline = 273 new ObjectDetectionPipeline((ObjectDetectionPipelineSettings) desiredPipelineSettings); 274 } 275 case Calib3d, DriverMode -> {} 276 } 277 } 278 279 /** 280 * Enters or exits calibration mode based on the parameter. <br> 281 * <br> 282 * Exiting returns to the last used user pipeline. 283 * 284 * @param wantsCalibration True to enter calibration mode, false to exit calibration mode. 285 */ 286 public void setCalibrationMode(boolean wantsCalibration) { 287 if (!wantsCalibration) calibration3dPipeline.finishCalibration(); 288 setPipelineInternal(wantsCalibration ? CAL_3D_INDEX : lastUserPipelineIdx); 289 } 290 291 /** 292 * Enters or exits driver mode based on the parameter. <br> 293 * <br> 294 * Exiting returns to the last used user pipeline. 295 * 296 * @param state True to enter driver mode, false to exit driver mode. 297 */ 298 public void setDriverMode(boolean state) { 299 setPipelineInternal(state ? DRIVERMODE_INDEX : lastUserPipelineIdx); 300 } 301 302 /** 303 * Returns whether driver mode is active. 304 * 305 * @return Whether driver mode is active. 306 */ 307 public boolean getDriverMode() { 308 return currentPipelineIndex == DRIVERMODE_INDEX; 309 } 310 311 public static final Comparator<CVPipelineSettings> PipelineSettingsIndexComparator = 312 Comparator.comparingInt(o -> o.pipelineIndex); 313 314 /** 315 * Sorts the pipeline list by index, and reassigns their indexes to match the new order. <br> 316 * <br> 317 * I don't like this, but I have no other ideas, and it works so 318 */ 319 private void reassignIndexes() { 320 userPipelineSettings.sort(PipelineSettingsIndexComparator); 321 for (int i = 0; i < userPipelineSettings.size(); i++) { 322 userPipelineSettings.get(i).pipelineIndex = i; 323 } 324 } 325 326 public CVPipelineSettings addPipeline(PipelineType type) { 327 return addPipeline(type, "New Pipeline"); 328 } 329 330 public CVPipelineSettings addPipeline(PipelineType type, String nickname) { 331 var added = createSettingsForType(type, nickname); 332 if (added == null) { 333 logger.error("Cannot add null pipeline!"); 334 return null; 335 } 336 addPipelineInternal(added); 337 reassignIndexes(); 338 return added; 339 } 340 341 private CVPipelineSettings createSettingsForType(PipelineType type, String nickname) { 342 switch (type) { 343 case Reflective -> { 344 var added = new ReflectivePipelineSettings(); 345 added.pipelineNickname = nickname; 346 return added; 347 } 348 case ColoredShape -> { 349 var added = new ColoredShapePipelineSettings(); 350 added.pipelineNickname = nickname; 351 return added; 352 } 353 case AprilTag -> { 354 var added = new AprilTagPipelineSettings(); 355 added.pipelineNickname = nickname; 356 return added; 357 } 358 case Aruco -> { 359 var added = new ArucoPipelineSettings(); 360 added.pipelineNickname = nickname; 361 return added; 362 } 363 case ObjectDetection -> { 364 var added = new ObjectDetectionPipelineSettings(); 365 added.pipelineNickname = nickname; 366 return added; 367 } 368 case Calib3d, DriverMode -> { 369 logger.error("Got invalid pipeline type: " + type); 370 return null; 371 } 372 } 373 374 // This can never happen, this is here to satisfy the compiler. 375 throw new IllegalStateException("Got impossible pipeline type: " + type); 376 } 377 378 private void addPipelineInternal(CVPipelineSettings settings) { 379 settings.pipelineIndex = userPipelineSettings.size(); 380 userPipelineSettings.add(settings); 381 reassignIndexes(); 382 } 383 384 /** 385 * Remove a pipeline settings at the given index and return the new current index 386 * 387 * @param index The idx to remove 388 */ 389 private int removePipelineInternal(int index) { 390 userPipelineSettings.remove(index); 391 currentPipelineIndex = Math.min(index, userPipelineSettings.size() - 1); 392 reassignIndexes(); 393 return currentPipelineIndex; 394 } 395 396 public void setIndex(int index) { 397 this.setPipelineInternal(index); 398 } 399 400 public int removePipeline(int index) { 401 if (index < 0) { 402 return currentPipelineIndex; 403 } 404 // TODO should we block/lock on a mutex? 405 return removePipelineInternal(index); 406 } 407 408 public void renameCurrentPipeline(String newName) { 409 getCurrentPipelineSettings().pipelineNickname = newName; 410 } 411 412 /** 413 * Duplicate a pipeline at a given index 414 * 415 * @param index the index of the target pipeline 416 * @return The new index 417 */ 418 public int duplicatePipeline(int index) { 419 var settings = userPipelineSettings.get(index); 420 var newSettings = settings.clone(); 421 newSettings.pipelineNickname = 422 createUniqueName(settings.pipelineNickname, userPipelineSettings); 423 newSettings.pipelineIndex = Integer.MAX_VALUE; 424 logger.debug("Duplicating pipe " + index + " to " + newSettings.pipelineNickname); 425 userPipelineSettings.add(newSettings); 426 reassignIndexes(); 427 428 // Now we look for the index of the new pipeline and return it 429 return userPipelineSettings.indexOf(newSettings); 430 } 431 432 private static String createUniqueName( 433 String nickname, List<CVPipelineSettings> existingSettings) { 434 StringBuilder uniqueName = new StringBuilder(nickname); 435 while (true) { 436 String finalUniqueName = uniqueName.toString(); // To get around lambda capture 437 var conflictingName = 438 existingSettings.stream().anyMatch(it -> it.pipelineNickname.equals(finalUniqueName)); 439 440 if (!conflictingName) { 441 // If no conflict, we're done 442 return uniqueName.toString(); 443 } else { 444 // Otherwise, we need to add a suffix to the name 445 // If the string doesn't already end in "([0-9]*)", we'll add it 446 // If it does, we'll increment the number in the suffix 447 448 if (uniqueName.toString().matches(".*\\([0-9]*\\)")) { 449 // Because java strings are immutable, we have to do this curstedness 450 // This is like doing "New pipeline (" + 2 + ")" 451 452 var parenStart = uniqueName.toString().lastIndexOf('('); 453 var parenEnd = uniqueName.length() - 1; 454 var number = Integer.parseInt(uniqueName.substring(parenStart + 1, parenEnd)) + 1; 455 456 uniqueName = new StringBuilder(uniqueName.substring(0, parenStart + 1) + number + ")"); 457 } else { 458 uniqueName.append(" (1)"); 459 } 460 } 461 } 462 } 463 464 private static List<Field> getAllFields(Class base) { 465 List<Field> ret = new ArrayList<>(); 466 ret.addAll(List.of(base.getDeclaredFields())); 467 var superclazz = base.getSuperclass(); 468 if (superclazz != null) { 469 ret.addAll(getAllFields(superclazz)); 470 } 471 472 return ret; 473 } 474 475 public void changePipelineType(int newType) { 476 // Find the PipelineType proposed 477 // To do this we look at all the PipelineType entries and look for one with 478 // matching 479 // base indexes 480 PipelineType type = 481 Arrays.stream(PipelineType.values()) 482 .filter(it -> it.baseIndex == newType) 483 .findAny() 484 .orElse(null); 485 if (type == null) { 486 logger.error("Could not match type " + newType + " to a PipelineType!"); 487 return; 488 } 489 490 if (type.baseIndex == getCurrentPipelineSettings().pipelineType.baseIndex) { 491 logger.debug( 492 "Not changing settings as " 493 + type 494 + " and " 495 + getCurrentPipelineSettings().pipelineType 496 + " are identical!"); 497 return; 498 } 499 500 var idx = currentPipelineIndex; 501 if (idx < 0) { 502 logger.error("Cannot replace non-user pipeline!"); 503 return; 504 } 505 506 // The settings we used to have 507 var oldSettings = userPipelineSettings.get(idx); 508 509 var name = getCurrentPipelineSettings().pipelineNickname; 510 // Dummy settings to copy common fields over 511 var newSettings = createSettingsForType(type, name); 512 513 // Copy all fields from AdvancedPipelineSettings/its superclasses from old to new 514 try { 515 for (Field field : getAllFields(AdvancedPipelineSettings.class)) { 516 if (field.isAnnotationPresent(SuppressSettingCopy.class)) { 517 // Skip fields that are annotated with SuppressSettingCopy 518 continue; 519 } 520 Object value = field.get(oldSettings); 521 logger.debug("setting " + field.getName() + " to " + value); 522 field.set(newSettings, value); 523 } 524 } catch (Exception e) { 525 logger.error("Couldn't copy old settings", e); 526 } 527 528 logger.info("Adding new pipe of type " + type + " at idx " + idx); 529 530 userPipelineSettings.set(idx, newSettings); 531 532 setPipelineInternal(idx); 533 reassignIndexes(); 534 recreateUserPipeline(); 535 } 536}