Skip to content

Instantly share code, notes, and snippets.

@lacan
Created May 25, 2021 11:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lacan/72ae0f5d5e64d6a3379da1d228aec6e2 to your computer and use it in GitHub Desktop.
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
// 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