Created
May 25, 2021 11:10
-
-
Save lacan/72ae0f5d5e64d6a3379da1d228aec6e2 to your computer and use it in GitHub Desktop.
[Basic OpenCV Template Matching in QuPath] Testing OpenCV for Template Matching and NMS Suppression inside QuPath #qupath #opencv #imagesc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Basic Template Matching using OpenCV with distance based NMS | |
// Author: Olivier Burri, EPFL - SV - PTECH - BIOP | |
// Inspiration: | |
// - https://docs.opencv.org/master/d4/dc6/tutorial_py_template_matching.html | |
// - https://towardsdatascience.com/non-maximum-suppression-nms-93ce178e177c | |
// Date: 2021-05-25 | |
def downsample = 10 | |
def templateClass = "Template" | |
def detectedClass = "Detected" | |
def templateThreshold = 0.95 | |
def nmsDistance = 50 | |
// Start of Script | |
def imageData = getCurrentImageData() | |
def server = imageData.getServer() | |
// Cleanup if there was a previous run | |
removeObjects(getAnnotationObjects().findAll{it.getPathClass().equals( getPathClass( detectedClass ) )}, false) | |
// Reference to Template Object | |
def templateObj = getAnnotationObjects().find{ it.getPathClass() == getPathClass( templateClass ) } | |
// Request full image | |
def fullReq = RegionRequest.createInstance(server, downsample) | |
def fullImg = OpenCVTools.imageToMat(server.readBufferedImage(fullReq)) | |
// Convert to 32-bit | |
fullImg.convertTo(fullImg, CvType.CV_32F); | |
// Request template image | |
def templReq = RegionRequest.createInstance(server.getPath(), downsample, templateObj.getROI()) | |
def tmplImg = OpenCVTools.imageToMat(server.readBufferedImage(templReq)) | |
tmplImg.convertTo(tmplImg, CvType.CV_32F); | |
//Prepare image for template matching result | |
def size = new Size(fullImg.cols()-tmplImg.cols()+1, fullImg.rows()-tmplImg.rows()+1) | |
def result = new Mat(size, CvType.CV_32FC1); | |
// Actual Template Matching | |
opencv_imgproc.matchTemplate(fullImg, tmplImg, result, opencv_imgproc.TM_CCORR_NORMED) | |
// Coordinates of maxima | |
def points = getPointsAboveThreshold( result, templateThreshold ) | |
// Non Maximum suppression based on distance | |
points = getNMS( points, nmsDistance ) | |
// Create annotations | |
def obj = points.collect{ p-> | |
def roi = ROIs.createRectangleROI(p.p.getX() * downsample , p.p.getY() * downsample, templateObj.getROI().getBoundsWidth(), templateObj.getROI().getBoundsHeight(), null); | |
return PathObjects.createAnnotationObject(roi, getPathClass( detectedClass ) ) | |
} | |
addObjects(obj) | |
// Find all points above threshold | |
public static List<Point> getPointsAboveThreshold( def mat, def thr){ | |
def matches = [] | |
def indexer = mat.createIndexer() | |
for (int y = 0; y < mat.rows(); y++) { | |
for (int x = 0; x < mat.cols(); x++) { | |
if (indexer.get(y,x)>thr) { | |
// Add Point and Score, we will need it for matching after | |
matches.add( [p:new Point2(x, y), s:indexer.get(y,x)] ) | |
} | |
} | |
} | |
return matches | |
} | |
// Basic implementation of NMS based on distance | |
def getNMS(def boxes, def iouThr) { | |
def keep = [] | |
while( !boxes.isEmpty() ) { | |
def maxBox = boxes.max{ it.s } | |
keep.add( maxBox ) | |
boxes.remove( maxBox ) | |
boxes.removeIf{ b-> maxBox.p.distance(b.p) < iouThr } | |
} | |
return keep | |
} | |
// Imports | |
import org.bytedeco.opencv.opencv_core.* | |
import org.bytedeco.opencv.global.opencv_imgproc | |
import org.opencv.core.CvType | |
import qupath.opencv.tools.OpenCVTools | |
import qupath.lib.geom.Point2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment