Saturday, August 24, 2013

HSV Adjuster - interactive HSV colorspace application

The application I'm describing in this blog post can help you determining HSV values for objects you show to your webcam.

Here is a video:



Here is a screenshot:

screenshot of the application

Here is complete the source, lookup apps/hsvadjuster/ application.

Maybe you take the time to read a little about HSV in the wikipedia.

This application uses b103 of the early access release of JDK8 and additionally the controlsFX library written by the fxexperience team. The widget I'm using is called RangeSlider.

This time I've used fxml, if you want to use widgets like the RangeSlider don't forget to import them in the header instructions.

This post was very heavily inspired by a blog post on object detection using color separation for C++. Thanks for sharing. There you can find how to use the application to find proper lower and upper bounds for your light conditions and target colors.

For reference, I've created a gist to quickly browse through the key parts of the code:



package net.ladstatt.apps
import javafx.scene._
import org.opencv.core._
import org.opencv.imgproc.Imgproc
import javafx.application.{Platform, Application}
import javafx.scene.control._
import javafx.scene.image.ImageView
import javafx.scene.layout.BorderPane
import javafx.stage.Stage
import scala.Some
import java.util.ResourceBundle
import javafx.fxml.{FXML, Initializable}
import java.net.URL
import javafx.beans.property.SimpleObjectProperty
import net.ladstatt.jfx._
import org.controlsfx.control.{HsvSlider, RangeSlider}
object HsvAdjuster {
def main(args: Array[String]): Unit = {
Application.launch(classOf[HsvAdjuster], args: _*)
}
}
class HsvAdjuster extends Application with JfxUtils with Initializable with OpenCVUtils {
// width and height of input image
val (webCamWidth, webCamHeight) = (1280, 720)
val lowerBoundProperty = new SimpleObjectProperty[Scalar](new Scalar(0, 0, 0))
def setLowerBound(lb: Scalar) = lowerBoundProperty.set(lb)
def getLowerBound() = lowerBoundProperty.get
val upperBoundProperty = new SimpleObjectProperty[Scalar](new Scalar(255, 255, 255))
def setUpperBound(lb: Scalar) = upperBoundProperty.set(lb)
def getUpperBound() = upperBoundProperty.get
override def init(): Unit = loadNativeLibs // important to have this statement on the "right" thread
def colorSpace(conversionMethod: Int = Imgproc.COLOR_BGR2GRAY)(input: Mat): Mat = {
val colorTransformed = new Mat
Imgproc.cvtColor(input, colorTransformed, conversionMethod)
colorTransformed
}
def restrain(input: Mat): Mat = {
val dest = new Mat
val lb = getLowerBound()
val ub = getUpperBound()
Core.inRange(input, lb, ub, dest)
dest
}
val colorRanges: Map[String, (Double, Double)] = Map("ALLHue" ->((0.0, 179.0)),
"ALLSaturation" ->((0.0, 255.0)),
"ALLValue" ->((0.0, 255.0)),
"orange" ->((165.0, 179.0)),
"yellow" ->((16.0, 28.0)),
"green" ->((92.0, 103.0)),
"blue" ->((103.0, 115.0)),
"white" ->((130.0, 160.0)),
"red" ->((160.0, 179.0)))
@FXML var labelLbHue: Label = _
@FXML var labelLbSaturation: Label = _
@FXML var labelLbValue: Label = _
@FXML var labelUbHue: Label = _
@FXML var labelUbSaturation: Label = _
@FXML var labelUbValue: Label = _
@FXML var hueSlider: HsvSlider = _
@FXML var saturationSlider: HsvSlider = _
@FXML var valueSlider: HsvSlider = _
@FXML var viewPort: ImageView = _
@FXML var orangePresetButton: Button = _
@FXML var yellowPresetButton: Button = _
@FXML var greenPresetButton: Button = _
@FXML var bluePresetButton: Button = _
@FXML var whitePresetButton: Button = _
@FXML var redPresetButton: Button = _
def initPresetButton(button: Button, range: (Double, Double)) {
button.setOnAction(mkEventHandler(e => {
hueSlider.setLowValue(range._1)
hueSlider.setHighValue(range._2)
}))
}
def toGray = colorSpace(Imgproc.COLOR_BGR2GRAY) _
def toHsv = colorSpace(Imgproc.COLOR_BGR2HSV) _
override def start(stage: Stage): Unit = {
stage.setTitle("HsvAdjuster")
val imageService = new WebcamService
imageService.setOnSucceeded(
mkEventHandler(
event => {
val grabbedMat = restrain(colorSpace(Imgproc.COLOR_BGR2HSV)(event.getSource.getValue.asInstanceOf[Mat]))
Platform.runLater(
new Runnable() {
def run = {
viewPort.imageProperty.set(toImage(grabbedMat))
imageService.restart
}
}
)
}
))
imageService.start
val scene = new Scene(mk[BorderPane](mkFxmlLoader("/hsvadjuster.fxml", this)))
stage.setScene(scene)
stage.show
}
def initialize(url: URL, resourceBundle: ResourceBundle): Unit = {
def initRangeSlider(pos: Int, slider: RangeSlider, lowerLabel: Label, upperLabel: Label): Unit = {
slider.lowValueProperty().addListener(mkChangeListener[Number](
(obVal, oldVal, newVal) => {
val mutableBounds = getLowerBound.`val`
mutableBounds(pos) = newVal.doubleValue()
setLowerBound(new Scalar(mutableBounds))
lowerLabel.setText("%.2f".format(newVal.doubleValue))
}
))
slider.highValueProperty().addListener(mkChangeListener[Number](
(obVal, oldVal, newVal) => {
val mutableBounds = getUpperBound.`val`
mutableBounds(pos) = newVal.doubleValue()
setUpperBound(new Scalar(mutableBounds))
upperLabel.setText("%.2f".format(newVal.doubleValue))
}
))
}
def setRangeSlider(slider: RangeSlider)(range: (Double, Double)) {
slider.setLowValue(range._1)
slider.setHighValue(range._2)
}
def setHueSlider = setRangeSlider(hueSlider) _
def setSaturationSlider = setRangeSlider(saturationSlider) _
def setValueSlider = setRangeSlider(valueSlider) _
initRangeSlider(0, hueSlider, labelLbHue, labelUbHue)
initRangeSlider(1, saturationSlider, labelLbSaturation, labelUbSaturation)
initRangeSlider(2, valueSlider, labelLbValue, labelUbValue)
setHueSlider(colorRanges("ALLHue"))
setSaturationSlider(colorRanges("ALLSaturation"))
setValueSlider(colorRanges("ALLValue"))
initPresetButton(orangePresetButton, colorRanges("orange"))
initPresetButton(yellowPresetButton, colorRanges("yellow"))
initPresetButton(greenPresetButton, colorRanges("green"))
initPresetButton(bluePresetButton, colorRanges("blue"))
initPresetButton(whitePresetButton, colorRanges("white"))
initPresetButton(redPresetButton, colorRanges("red"))
}
}