Skip to content

Commit

Permalink
rewrite rknn object detector and update rknn detection pipe to respon…
Browse files Browse the repository at this point in the history
…d to model changes
  • Loading branch information
Alextopher committed Jul 2, 2024
1 parent f3dbec3 commit fe6c730
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.rknn.RknnJNI;
Expand Down Expand Up @@ -97,15 +98,36 @@ public class Model {
public final File modelFile;
public final RknnJNI.ModelVersion version;
public final List<String> labels;
public final Size inputSize;

/**
* Model constructor.
*
* @param model format `name-width-height-model.format`
* @param labels
* @throws IllegalArgumentException
*/
public Model(String model, String labels) throws IllegalArgumentException {
this.version = getModelVersion(model);
String[] parts = model.split("-");
if (parts.length != 4) {
throw new IllegalArgumentException("Invalid model file name: " + model);
}

// TODO: model 'version' need to be replaced the by the product of 'Version' x 'Format'
this.version = getModelVersion(parts[3]);

int width = Integer.parseInt(parts[1]);
int height = Integer.parseInt(parts[2]);
this.inputSize = new Size(width, height);

this.modelFile = new File(model);
try {
this.labels = Files.readAllLines(Paths.get(labels));
} catch (IOException e) {
throw new IllegalArgumentException("Error reading labels file " + labels, e);
throw new IllegalArgumentException("Failed to read labels file " + labels, e);
}

logger.info("Loaded model " + modelFile.getName());
}

public String getPath() {
Expand Down Expand Up @@ -141,7 +163,7 @@ public List<String> getModels() {
/**
* Returns the model with the given name.
*
* <p>TODO: Java 17 This should return an Optional<Model> instead of null.
* <p>TODO: Java 17 This should return an Optional Model instead of null.
*
* @param modelName The model name
* @return The model
Expand Down Expand Up @@ -214,7 +236,7 @@ public void extractModels(File modelsFolder) {
modelsFolder.mkdirs();
}

String resourcePath = "models"; // Adjust path if necessary
String resourcePath = "models";
try {
URL resourceURL = NeuralNetworkModelManager.class.getClassLoader().getResource(resourcePath);
if (resourceURL == null) {
Expand Down
101 changes: 0 additions & 101 deletions photon-core/src/main/java/org/photonvision/jni/RknnDetectorJNI.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,10 @@
package org.photonvision.jni;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import org.opencv.core.Mat;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TestUtils;
import org.photonvision.rknn.RknnJNI;
import org.photonvision.rknn.RknnJNI.RknnResult;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;

public class RknnDetectorJNI extends PhotonJNICommon {
private static final Logger logger = new Logger(RknnDetectorJNI.class, LogGroup.General);
private boolean isLoaded;
private static RknnDetectorJNI instance = null;

Expand Down Expand Up @@ -61,94 +50,4 @@ public boolean isLoaded() {
public void setLoaded(boolean state) {
isLoaded = state;
}

public static class RknnObjectDetector {
long objPointer = -1;
private List<String> labels;
private final Object lock = new Object();
private static final CopyOnWriteArrayList<RknnObjectDetector> detectors =
new CopyOnWriteArrayList<>();

static volatile boolean hook = false;

public RknnObjectDetector(NeuralNetworkModelManager.Model model) {
this(model.getPath(), model.labels, model.version);
}

public RknnObjectDetector(String modelPath, List<String> labels, RknnJNI.ModelVersion version) {
synchronized (lock) {
objPointer = RknnJNI.create(modelPath, labels.size(), version.ordinal(), -1);
detectors.add(this);
logger.debug(
"Created detector "
+ objPointer
+ " from path "
+ modelPath
+ "! Detectors: "
+ Arrays.toString(detectors.toArray()));
}
this.labels = labels;

// the kernel should probably alredy deal with this for us, but I'm gunna be paranoid anyways.
if (!hook) {
Runtime.getRuntime()
.addShutdownHook(
new Thread(
() -> {
System.err.println("Shutdown hook rknn");
for (var d : detectors) {
d.release();
}
}));
hook = true;
}
}

public List<String> getClasses() {
return labels;
}

/**
* Detect forwards using this model
*
* @param in The image to process
* @param nmsThresh Non-maximum supression threshold. Probably should not change
* @param boxThresh Minimum confidence for a box to be added. Basically just confidence
* threshold
*/
public List<NeuralNetworkPipeResult> detect(Mat in, double nmsThresh, double boxThresh) {
RknnResult[] ret;
synchronized (lock) {
// We can technically be asked to detect and the lock might be acquired _after_ release has
// been called. This would mean objPointer would be invalid which would call everything to
// explode.
if (objPointer > 0) {
ret = RknnJNI.detect(objPointer, in.getNativeObjAddr(), nmsThresh, boxThresh);
} else {
logger.warn("Detect called after destroy -- giving up");
return List.of();
}
}
if (ret == null) {
return List.of();
}
return List.of(ret).stream()
.map(it -> new NeuralNetworkPipeResult(it.rect, it.class_id, it.conf))
.collect(Collectors.toList());
}

public void release() {
synchronized (lock) {
if (objPointer > 0) {
RknnJNI.destroy(objPointer);
detectors.remove(this);
System.out.println(
"Killed " + objPointer + "! Detectors: " + Arrays.toString(detectors.toArray()));
objPointer = -1;
} else {
logger.error("RKNN Detector has already been destroyed!");
}
}
}
}
}
116 changes: 116 additions & 0 deletions photon-core/src/main/java/org/photonvision/jni/RknnObjectDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.photonvision.jni;

import java.lang.ref.Cleaner;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.opencv.core.Mat;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.rknn.RknnJNI;
import org.photonvision.vision.opencv.Releasable;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;

/**
* A class to represent an object detector using the Rknn library.
*
* <p>TODO: When we start supporting more platforms, we should consider moving most of this code
* into a common "ObjectDetector" class to define the common interface for all object detectors.
*/
public class RknnObjectDetector implements Releasable {
/** logger for the RknnObjectDetector */
private static final Logger logger = new Logger(RknnDetectorJNI.class, LogGroup.General);

/** Cleaner instance to release the detector when it is no longer needed */
private final Cleaner cleaner = Cleaner.create();

/** Pointer to the native object */
private final long objPointer;

/** Model configuration */
private final NeuralNetworkModelManager.Model model;

/**
* Returns the model used by the detector.
*/
public NeuralNetworkModelManager.Model getModel() {
return model;
}

/** Atomic boolean to ensure that the detector is only released _once_. */
private AtomicBoolean released = new AtomicBoolean(false);

/**
* Creates a new RknnObjectDetector from the given model.
*
* @param model The model to create the detector from.
*/
public RknnObjectDetector(NeuralNetworkModelManager.Model model) {
this.model = model;

// Create the detector
objPointer = RknnJNI.create(model.getPath(), model.labels.size(), model.version.ordinal(), -1);
if (objPointer <= 0) {
throw new RuntimeException("Failed to create detector from path " + model.getPath());
}

logger.debug("Created detector for model " + model.modelFile.getName());

// Register the cleaner to release the detector when it goes out of scope
cleaner.register(this, this::release);

// Set the detector to be released when the JVM exits
Runtime.getRuntime().addShutdownHook(new Thread(this::release));
}

/**
* Returns the classes that the detector can detect
*
* @return The classes
*/
public List<String> getClasses() {
return model.labels;
}

/**
* Detects objects in the given input image using the RknnDetector.
*
* @param in The input image to perform object detection on.
* @param nmsThresh The threshold value for non-maximum suppression.
* @param boxThresh The threshold value for bounding box detection.
* @return A list of NeuralNetworkPipeResult objects representing the detected objects. Returns an
* empty list if the detector is not initialized or if no objects are detected.
*/
public List<NeuralNetworkPipeResult> detect(Mat in, double nmsThresh, double boxThresh) {
if (objPointer <= 0) {
// Report error and make sure to include the model name
logger.error("Detector is not initialized! Model: " + model.modelFile.getName());
return List.of();
}

var results = RknnJNI.detect(objPointer, in.getNativeObjAddr(), nmsThresh, boxThresh);
if (results == null) {
return List.of();
}

return List.of(results).stream()
.map(it -> new NeuralNetworkPipeResult(it.rect, it.class_id, it.conf))
.toList();
}

/** Thread-safe method to release the detector. */
@Override
public void release() {
if (released.compareAndSet(false, true)) {
if (objPointer <= 0) {
logger.error(
"Detector is not initialized, and so it can't be released! Model: "
+ model.modelFile.getName());
return;
}

RknnJNI.destroy(objPointer);
logger.debug("Released detector for model " + model.modelFile.getName());
}
}
}
Loading

0 comments on commit fe6c730

Please sign in to comment.