Skip to content

Commit

Permalink
[Calibrator] Allow use of custom charts #38
Browse files Browse the repository at this point in the history
  • Loading branch information
Jarek-Sacha committed Aug 18, 2021
1 parent 0545e0f commit f33e048
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<StackPane xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1"
<StackPane xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ij_plugins.color.ui.calibration.ColorCalibratorUIController">
<children>
<GridPane fx:id="rootGridPane" hgap="10.0" vgap="10.0">
Expand Down Expand Up @@ -59,7 +59,7 @@
<MenuItem mnemonicParsing="false" text="Action 2"/>
</items>
</SplitMenuButton>
<HBox prefWidth="200.0" GridPane.columnSpan="2147483647" GridPane.rowIndex="4">
<HBox prefWidth="200.0" GridPane.columnSpan="2147483647" GridPane.rowIndex="5">
<children>
<Label id="ijp-separator" text="Actual Chart"/>
<Separator id="ijp-separator" valignment="TOP" HBox.hgrow="ALWAYS">
Expand All @@ -72,11 +72,11 @@
<Insets bottom="5.0" top="10.0"/>
</padding>
</HBox>
<Label id="ijp-label" text="Chip margin %" GridPane.rowIndex="5"/>
<Label id="ijp-label" text="Chip margin %" GridPane.rowIndex="6"/>
<Spinner fx:id="marginsSpinner" max="100" maxWidth="1.7976931348623157E308" min="0"
GridPane.columnIndex="1" GridPane.rowIndex="5"/>
GridPane.columnIndex="1" GridPane.rowIndex="6"/>

<HBox prefWidth="200.0" GridPane.columnSpan="2147483647" GridPane.rowIndex="6">
<HBox prefWidth="200.0" GridPane.columnSpan="2147483647" GridPane.rowIndex="7">
<children>
<Label id="ijp-separator" text="Calibration"/>
<Separator id="ijp-separator" valignment="TOP" HBox.hgrow="ALWAYS">
Expand All @@ -90,33 +90,33 @@
</padding>
</HBox>
<Label id="ijp-label" alignment="CENTER_RIGHT" text="Reference" GridPane.halignment="RIGHT"
GridPane.rowIndex="7" GridPane.valignment="CENTER"/>
GridPane.rowIndex="8" GridPane.valignment="CENTER"/>
<ChoiceBox fx:id="referenceColorSpaceChoiceBox" maxWidth="1.7976931348623157E308" prefWidth="150.0"
GridPane.columnIndex="1" GridPane.rowIndex="7"/>
GridPane.columnIndex="1" GridPane.rowIndex="8"/>
<CheckBox fx:id="enableExtraInfoCB" mnemonicParsing="false" text="Show extra info"
GridPane.columnIndex="1" GridPane.rowIndex="9"/>
<Label id="ijp-label" text="Mapping method" GridPane.rowIndex="8"/>
GridPane.columnIndex="1" GridPane.rowIndex="10"/>
<Label id="ijp-label" text="Mapping method" GridPane.rowIndex="9"/>
<ChoiceBox fx:id="mappingMethodChoiceBox" maxWidth="1.7976931348623157E308" prefWidth="150.0"
GridPane.columnIndex="1" GridPane.rowIndex="8"/>
GridPane.columnIndex="1" GridPane.rowIndex="9"/>
<Button fx:id="suggestCalibrationOptionsButton" mnemonicParsing="false" text="Sugget Options"
GridPane.columnIndex="2" GridPane.rowIndex="7"/>
GridPane.columnIndex="2" GridPane.rowIndex="8"/>
<Button id="ijp-button" fx:id="calibrateButton" contentDisplay="CENTER"
maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Calibrate"
GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="10">
GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="11">
<GridPane.margin>
<Insets bottom="5.0" top="5.0"/>
</GridPane.margin>
</Button>
<Button fx:id="applyToCurrentImageButton" maxWidth="1.7976931348623157E308" mnemonicParsing="false"
text="Apply" GridPane.columnIndex="1" GridPane.rowIndex="12">
text="Apply" GridPane.columnIndex="1" GridPane.rowIndex="13">
<padding>
<Insets bottom="5.0" top="5.0"/>
</padding>
<GridPane.margin>
<Insets bottom="5.0" top="5.0"/>
</GridPane.margin>
</Button>
<HBox prefWidth="200.0" GridPane.columnSpan="2147483647" GridPane.rowIndex="11">
<HBox prefWidth="200.0" GridPane.columnSpan="2147483647" GridPane.rowIndex="12">
<children>
<Label id="ijp-separator" text="Apply to Another Image"/>
<Separator id="ijp-separator" valignment="TOP" HBox.hgrow="ALWAYS">
Expand All @@ -130,7 +130,12 @@
</padding>
</HBox>
<Button fx:id="helpButton" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Help"
GridPane.columnIndex="2" GridPane.rowIndex="12"/>
GridPane.columnIndex="2" GridPane.rowIndex="13"/>
<Label id="ijp-label" text="Info" GridPane.halignment="RIGHT" GridPane.rowIndex="4"
GridPane.valignment="CENTER"/>
<Label id="ijp-label" fx:id="chartInfoLabel" text="???" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Button fx:id="editChartButton" disable="true" maxWidth="1.7976931348623157E308" mnemonicParsing="false"
text="Edit Chart" GridPane.columnIndex="2" GridPane.rowIndex="4"/>
</children>
<rowConstraints>
<RowConstraints minHeight="10.0"/>
Expand All @@ -146,6 +151,7 @@
<RowConstraints minHeight="10.0"/>
<RowConstraints minHeight="10.0"/>
<RowConstraints minHeight="10.0"/>
<RowConstraints minHeight="10.0"/>
</rowConstraints>
<columnConstraints>
<ColumnConstraints/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class ColorCalibratorUIController(
private val imageTitleLabel: Label,
private val chartTypeChoiceBox: ChoiceBox[ColorChartType],
private val renderReferenceChartSplitButton: SplitMenuButton,
private val chartInfoLabel: Label,
private val editChartButton: Button,
private val marginsSpinner: Spinner[java.lang.Integer],
private val referenceColorSpaceChoiceBox: ChoiceBox[ReferenceColorSpace],
private val enableExtraInfoCB: CheckBox,
Expand Down Expand Up @@ -91,6 +93,13 @@ class ColorCalibratorUIController(
}
)

renderReferenceChartSplitButton.disable <== !model.referenceChartDefined

editChartButton.onAction = _ => model.onEditChart()
editChartButton.disable <== !model.referenceChartEditEnabled

chartInfoLabel.text <== model.chartInfoText

// Actual chart
marginsSpinner.valueFactory = new IntegerSpinnerValueFactory(0, 49) {
value = model.chipMarginPercent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@

package ij_plugins.color.ui.calibration

import ij.gui.GenericDialog
import ij.measure.ResultsTable
import ij.plugin.BrowserLauncher
import ij.{ImagePlus, Prefs}
import ij_plugins.color.calibration.chart.{ColorChartType, ColorCharts, GridColorChart, ReferenceColorSpace}
import ij_plugins.color.calibration.regression.MappingMethod
import ij_plugins.color.calibration.{CorrectionRecipe, renderReferenceChart}
import ij_plugins.color.converter.ReferenceWhite
import ij_plugins.color.ui.calibration.tasks.{ApplyToCurrentImageTask, CalibrateTask, SuggestCalibrationOptionsTask}
import ij_plugins.color.ui.util.LiveChartROI
import javafx.beans.property.ReadOnlyBooleanProperty
Expand All @@ -36,6 +38,7 @@ import org.scalafx.extras.{BusyWorker, ShowMessage, onFX}
import scalafx.beans.property._
import scalafx.stage.Window

import java.io.File
import java.util.concurrent.Future

object ColorCalibratorUIModel {
Expand Down Expand Up @@ -124,16 +127,27 @@ class ColorCalibratorUIModel(val image: ImagePlus, parentWindow: Window) extends
referenceChartType.onChange { (_, _, _) =>
recreateReferenceChart()
}
val chipMarginPercent = new ObjectProperty[Integer](this, "chipMargin", 20)
chipMarginPercent.onChange { (_, _, _) =>
recreateReferenceChart()
}

// This is a derived value that needs to be updated when UI selections change: `referenceChartType` and `chipMarginPercent`
private val referenceChartOptionWrapper = new ReadOnlyObjectWrapper[Option[GridColorChart]](this, "chart", None)
val referenceChartOption: ReadOnlyObjectProperty[Option[GridColorChart]] =
referenceChartOptionWrapper.readOnlyProperty

private val referenceChartDefinedWrapper = new ReadOnlyBooleanWrapper()
val referenceChartDefined: ReadOnlyBooleanProperty = referenceChartDefinedWrapper.readOnlyProperty

private val referenceChartEditEnabledWrapper = new ReadOnlyBooleanWrapper()
val referenceChartEditEnabled: ReadOnlyBooleanProperty = referenceChartEditEnabledWrapper.readOnlyProperty

private var customChartOption: Option[GridColorChart] = None

val chartInfoText = new StringProperty("???")

val chipMarginPercent = new ObjectProperty[Integer](this, "chipMargin", 20)
chipMarginPercent.onChange { (_, _, _) =>
recreateReferenceChart()
}

val mappingMethod = new ObjectProperty[MappingMethod](this, "mappingMethod", MappingMethod.LinearCrossBand)
val clipReferenceRGB = new BooleanProperty(this, "clipReferenceRGB", true)
val showExtraInfo = new BooleanProperty(this, "showExtraInfo", false)
Expand All @@ -155,14 +169,29 @@ class ColorCalibratorUIModel(val image: ImagePlus, parentWindow: Window) extends
p.getReadOnlyProperty
}

// Initialize reference chart
recreateReferenceChart()

def recreateReferenceChart(): Unit = {
val chartOpt =
ColorCharts
.withColorChartType(referenceChartType.value)
.map(c => c.copyWithNewChipMargin(chipMarginPercent.value / 100d))
(
if (referenceChartType.value != ColorChartType.Custom) {
ColorCharts.withColorChartType(referenceChartType.value)
} else {
customChartOption
}
).map(c => c.copyWithNewChipMargin(chipMarginPercent.value / 100d))

referenceChartOptionWrapper.value = chartOpt

referenceChartDefinedWrapper.value = referenceChartOptionWrapper.value.isDefined

referenceChartEditEnabledWrapper.value = referenceChartType.value == ColorChartType.Custom

chartInfoText.value =
referenceChartOptionWrapper.value
.map(c => s"${c.nbColumns} x ${c.nbRows}, ${c.refWhite}")
.getOrElse("Chart not defined")
}

private def currentChart: GridColorChart = {
Expand Down Expand Up @@ -230,6 +259,61 @@ class ColorCalibratorUIModel(val image: ImagePlus, parentWindow: Window) extends
}
}

def onEditChart(): Unit = {

val _nbRows = customChartOption.map(_.nbRows).getOrElse(5)
val _nbColumns = customChartOption.map(_.nbColumns).getOrElse(6)
val _refWhite = customChartOption.map(_.refWhite).getOrElse(ReferenceWhite.D50)
val _defaultPath = ""

val gd = new GenericDialog("Edit Custom Reference Chart") {
addMessage("Chart Layout")
addNumericField("Rows", _nbRows, 0, 3, "")
addNumericField("Columns", _nbColumns, 0, 3, "")
addChoice("Reference White", ReferenceWhite.values.map(_.toString).toArray, _refWhite.toString)
addFileField("Reference values file", _defaultPath)
}

gd.showDialog()

if (gd.wasOKed()) {
val nbRows = {
val v = gd.getNextNumber
math.max(1, math.round(v).toInt)
}

val nbCols = {
val v = gd.getNextNumber
math.max(1, math.round(v).toInt)
}

val refWhite: ReferenceWhite = {
val v = gd.getNextChoice
ReferenceWhite.withName(v)
}

val filePath = gd.getNextString
val file = new File(filePath)

val chips = ColorCharts.loadReferenceValues(file)

val chart = new GridColorChart(
s"Custom - ${file.getName}",
nbColumns = nbCols,
nbRows = nbRows,
chips = chips,
chipMargin = 0.2,
refWhite = refWhite
)

customChartOption = Option(chart)
recreateReferenceChart()
} else {
//
}

}

def onSuggestCalibrationOptions(): Unit = busyWorker.doTask("onSuggestCalibrationOptions") {
new SuggestCalibrationOptionsTask(currentChart, image, Option(parentWindow))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ case object ColorChartType extends Enum[ColorChartType] {

case object ImageScienceColorGaugeMatte extends ColorChartType("Image Science ColorGauge Matte")

case object Custom extends ColorChartType("Custom")

/** All refined reference color spaces. */
val values: immutable.IndexedSeq[ColorChartType] = findValues
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,21 @@

package ij_plugins.color.calibration.chart

import ij.measure.ResultsTable
import ij_plugins.color.converter.ColorTriple.Lab
import ij_plugins.color.converter.ReferenceWhite
import ij_plugins.color.converter.{ColorTriple, ReferenceWhite}

import java.io.File

/** Enumeration of some common color charts. */
object ColorCharts {

/**
* GretagMacbeth ColorChecker with values measured by Robin D. Myers, average of two charts manufactured 2002-10.
*
* Illuminant D65.
* [[http://www.rmimaging.com/spectral_library/Reflective/Charts-Calibration/ColorChecker_2002-10_averaged.ss3.zip]]
*/
* GretagMacbeth ColorChecker with values measured by Robin D. Myers, average of two charts manufactured 2002-10.
*
* Illuminant D65.
* [[http://www.rmimaging.com/spectral_library/Reflective/Charts-Calibration/ColorChecker_2002-10_averaged.ss3.zip]]
*/
val GretagMacbethColorChecker = new GridColorChart(
ColorChartType.GretagMacbethColorChecker.name,
6,
Expand Down Expand Up @@ -162,4 +165,51 @@ object ColorCharts {
require(colorChartType != null, "'colorChartType' cannot be null.")
values.find(_.name == colorChartType.name)
}

/**
* Load chart reference values from a CSV file represented in CIE L*a*b* color space.
*
* The file is expected to have at least 4 columns: "SAMPLE_NAME", "LAB_L", "LAB_A", "LAB_B".
* Any additional columns will be ignored.
*
* Example of a file with 4 chips:
*
* {{{
* SAMPLE_NAME,LAB_L,LAB_A,LAB_B
* 1,38.675,12.907,14.358,19.306
* 2,65.750,19.811,17.790,26.626
* 3,50.373,-3.646,-22.360,22.656
* 4,43.697,-13.342,22.858,26.466
* }}}
*
* @return list of tuples representing chip name and reference value in CIE L*a*b*
*/
def loadReferenceValues(file: File): List[(String, ColorTriple.Lab)] = {
require(file.exists(), "File must exist: " + file.getCanonicalPath)
val rt = ResultsTable.open(file.getCanonicalPath)

val headingName = "SAMPLE_NAME"
val headingL = "LAB_L"
val headingA = "LAB_A"
val headingB = "LAB_B"

require(rt.columnExists(headingName))
require(rt.columnExists(headingL))
require(rt.columnExists(headingA))
require(rt.columnExists(headingB))

val chips =
(0 until rt.size())
.map { r =>
val name = rt.getStringValue(headingName, r)
val l = rt.getValue(headingL, r)
val a = rt.getValue(headingA, r)
val b = rt.getValue(headingB, r)

(name, ColorTriple.Lab(l, a, b))
}
.toList

chips
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,21 @@ final class GridColorChart(
require(chipMargin >= 0 && chipMargin < 0.5, "Margin value must at least 0 but less than 0.5, got " + chipMargin)
require(nbColumns > 0)
require(nbRows > 0)
require(nbColumns * nbRows == chips.size)
require(
nbColumns * nbRows == chips.size,
"Number of chips must equal number of columns times number of rows. " +
s"Expecting ${nbColumns * nbRows}, got ${chips.size}."
)
require(chips.size == enabled.size)
require(refWhite != null)
require(alignmentTransform != null)

/**
* Construct chart with all chips enabled.
*
* @param name chart's name
* @param nbColumns number of columns
* @param nbRows number of rows
* Construct chart with all chips enabled.
*
* @param name chart's name
* @param nbColumns number of columns
* @param nbRows number of rows
* @param chips chip names and CIE L*a*b* / D65 color values, row by row, starting at (0,0) or top left corner.
*/
def this(
Expand Down
Loading

0 comments on commit f33e048

Please sign in to comment.