Skip to content

Commit

Permalink
Add White Balance plugin #68
Browse files Browse the repository at this point in the history
  • Loading branch information
jpsacha committed Jul 1, 2022
1 parent 4e8f6cc commit e59bcde
Show file tree
Hide file tree
Showing 3 changed files with 341 additions and 1 deletion.
3 changes: 2 additions & 1 deletion ijp-color-ui/src/main/resources/plugins.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Plugins>Color, "IJP Color Calculator...", ij_plugins.color.ui.converter.ColorConverterPlugin
Plugins>Color, "IJP Color Calibrator...", ij_plugins.color.ui.calibration.ColorCalibratorPlugin
Plugins>Color, "IJP Color Chart Tool...", ij_plugins.color.ui.charttool.ColorChartToolPlugin
Plugins>Color, "IJP Color Chart Tool...", ij_plugins.color.ui.charttool.ColorChartToolPlugin
Plugins>Color, "IJP White Balance...", ij_plugins.color.ui.util.WhiteBalancePlugIn
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Image/J Plugins
* Copyright (C) 2002-2022 Jarek Sacha
* Author's email: jpsacha at gmail dot com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Latest release available at https://github.com/ij-plugins/ijp-color/
*/

package ij_plugins.color.ui.util

import ij.gui.GenericDialog
import ij.measure.ResultsTable
import ij.plugin.PlugIn
import ij.process.ColorProcessor
import ij.{CompositeImage, IJ, ImagePlus}
import ij_plugins.color.util.WhiteBalance
import ij_plugins.color.util.WhiteBalance.AveragingMode

import scala.util.control.NonFatal

object WhiteBalancePlugIn {
private var averagingMode = AveragingMode.Median
private var showCorrectionFactor = false
}

class WhiteBalancePlugIn extends PlugIn {
import WhiteBalancePlugIn.*

private val TITLE = "White Balance"
private val ABOUT =
"""Performs White Balance of an RGB image.<br>
|Requires selection of a area with an expected neutral color,<br>
|preferably light gray. Avoid areas with saturated (white) values.
|""".stripMargin

override def run(arg: String): Unit = {

// Get inpout image
val imp = Option(IJ.getImage) match {
case Some(v) =>
v
case None =>
IJ.error(TITLE, "No open images")
return
}

// Check image type
val error: Option[String] = imp.getType match {
case ImagePlus.COLOR_RGB => None
case ImagePlus.GRAY8 | ImagePlus.GRAY16 | ImagePlus.GRAY32 =>
if (imp.getStackSize == 3)
None
else
Option(s"Expecting image with 3 bands, got ${imp.getStackSize}")
}

if (error.isDefined) {
IJ.error(TITLE, error.get)
return
}

val roi = Option(imp.getRoi) match {
case Some(v) => v
case None =>
IJ.error(TITLE, "ROI required")
return
}

// Ask to select options
if (!showOptionsDialog()) {
return
}

try {
val (dstImp, redMult, blueMult) = WhiteBalance.whiteBalance(imp, roi, averagingMode)
dstImp.setTitle(s"${imp.getTitle}+white balance")
dstImp.show()

if (showCorrectionFactor) {
val rt = Option(ResultsTable.getResultsTable(TITLE)).getOrElse(new ResultsTable())
rt.incrementCounter()
rt.addValue("Label", imp.getShortTitle)
rt.addValue("Red multiplier", redMult)
rt.addValue("Blue multiplier", blueMult)
rt.show(TITLE)
}
} catch {
case NonFatal(ex) =>
ex.printStackTrace()
IJ.error(TITLE, ex.getMessage)
}

}

def showOptionsDialog(): Boolean = {
val gd = new GenericDialog(TITLE, IJ.getInstance)
gd.addPanel(IJPUtils.createInfoPanel(TITLE, ABOUT))
gd.addChoice("Averaging method", AveragingMode.values.map(_.name), averagingMode.name)
gd.addCheckbox("Show correction factor", showCorrectionFactor)

gd.addHelp("https://github.com/ij-plugins/ijp-color/wiki/White-Balance")

gd.showDialog()

if (gd.wasOKed()) {
averagingMode = AveragingMode.values(gd.getNextChoiceIndex)
showCorrectionFactor = gd.getNextBoolean
true
} else
false
}

}
212 changes: 212 additions & 0 deletions ijp-color/src/main/scala/ij_plugins/color/util/WhiteBalance.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Image/J Plugins
* Copyright (C) 2002-2022 Jarek Sacha
* Author's email: jpsacha at gmail dot com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Latest release available at https://github.com/ij-plugins/ijp-color/
*/

package ij_plugins.color.util

import ij.gui.Roi
import ij.process.{ByteProcessor, ColorProcessor, ImageProcessor, ImageStatistics}
import ij.{CompositeImage, IJ, ImagePlus, ImageStack}
import ij_plugins.color.util.EnumCompanion.{WithName, WithValue}
import ij_plugins.color.util.ImageJUtils.{mergeRGB, splitRGB}

import scala.collection.immutable
import scala.collection.immutable.ListMap

/** Methods for performing white balancing of RGB color images. */
object WhiteBalance {

enum AveragingMode(val name: String) extends WithName {
case Mean extends AveragingMode("Mean")
case Median extends AveragingMode("Median")
}

/**
* White balance an image using provided ROI. Median color value within ROI is used.
*
* @param cp image to correct
* @param roi area to use for computing white balance
* @return white-balanced image
*/
def whiteBalance(cp: ColorProcessor, roi: Roi): (ColorProcessor, Double, Double) =
whiteBalance(cp = cp, roi = roi, averagingMode = AveragingMode.Median)

/**
* White balance an image using provided ROI.
*
* @param cp input colr images with an ROI over a neutral color area.
* @param averagingMode what method is used to find an average channel value within the ROI.
* @return white-balanced image.
*/
def whiteBalance(cp: ColorProcessor,
roi: Roi,
averagingMode: AveragingMode
): (ColorProcessor, Double, Double) = {
require(cp != null, "Argument 'cp' cannot be null")

val imp = toRGBStackImp(cp)
val (dstImp, redMult, blueMult) = whiteBalanceRGBStack(imp, roi, averagingMode)

val rgb = slices(dstImp).map(_.asInstanceOf[ByteProcessor]).toArray

val dstCP = mergeRGB(rgb)

(dstCP, redMult, blueMult)
}

/**
* White balance an image using provided ROI. Median color value within ROI is used.
*
* @param imp image to correct
* @param roi area to use for computing white balance
* @return white-balanced image
*/
def whiteBalance(imp: ImagePlus, roi: Roi): (ImagePlus, Double, Double) = {
whiteBalance(imp, roi, AveragingMode.Median)
}

/**
* White balance an image using provided ROI.
*
* @param imp input colr images with an ROI over a neutral color area.
* @param averagingMode what method is used to find an average channel value within the ROI.
* @return white-balanced image.
*/
def whiteBalance(imp: ImagePlus, roi: Roi, averagingMode: AveragingMode): (ImagePlus, Double, Double) = {
val compositeModeOpt = imp match {
case c: CompositeImage => Some(c.getMode)
case _ => None
}

val (dstImp1, redMult, blueMult) =
imp.getType match {
case ImagePlus.COLOR_RGB =>
val cp = imp.getProcessor.asInstanceOf[ColorProcessor]
val (dst, rm, bm) = WhiteBalance.whiteBalance(cp, roi, averagingMode)
(new ImagePlus("", dst), rm, bm)
case ImagePlus.GRAY8 | ImagePlus.GRAY16 | ImagePlus.GRAY32 =>
WhiteBalance.whiteBalanceRGBStack(imp, roi, averagingMode)
}

val dstImp2 = compositeModeOpt match {
case Some(mode) => new CompositeImage(dstImp1, mode)
case None => dstImp1
}

(dstImp2, redMult, blueMult)
}

/**
* White balance an image using provided ROI.
* Assume that the source image is a stack with 3 gray level bands corresponding to Red, Green, and Blue.
*
* @param imp input colr images with an ROI over a neutral color area.
* @param averagingMode what method is used to find an average channel value within the ROI.
* @return white-balanced image
*/
def whiteBalanceRGBStack(imp: ImagePlus, roi: Roi, averagingMode: AveragingMode): (ImagePlus, Double, Double) = {
require(imp.getStackSize == 3)
require(imp.getType == ImagePlus.GRAY8 || imp.getType == ImagePlus.GRAY16 || imp.getType == ImagePlus.GRAY32)

val stats: Seq[ImageStatistics] = measureROI(imp: ImagePlus, roi: Roi)

val v = averagingMode match {
case AveragingMode.Mean => stats.map(_.mean)
case AveragingMode.Median => stats.map(_.median)
}

val r = v(0)
val g = v(1)
val b = v(2)

if (r == 0 || g == 0 || b == 0)
throw new IllegalStateException(s"Mean value of gray channel is 0, cannot white balance rgb=($r, $g, $b)")

val redMult = g / r
val blueMult = g / b

if (IJ.debugMode) {
IJ.log(s"White Balance")
IJ.log(s" Area: ${stats.head.area}")
IJ.log(s" R: $r")
IJ.log(s" G: $g")
IJ.log(s" B: $b")
IJ.log(s" mult R: $redMult")
IJ.log(s" mult B: $blueMult")
}

val dstImp = whiteBalance(imp, redMult, blueMult)

(dstImp, redMult, blueMult)
}

/**
* White balance an image using provided multiplier for read and blue channel. Green chanel is not changed.
*
* @param imp source image
* @param redMult red channel multiplier
* @param blueMult blue channel multiplier
* @return white-balanced image
*/
def whiteBalance(imp: ImagePlus, redMult: Double, blueMult: Double): ImagePlus = {
assert(imp.getStackSize == 3)

val srcStack = imp.getStack

val ipR = srcStack.getProcessor(1).duplicate()
ipR.multiply(redMult)

val ipG = srcStack.getProcessor(2).duplicate()

val ipB = srcStack.getProcessor(3).duplicate()
ipB.multiply(blueMult)

val dstStack = new ImageStack(imp.getWidth, imp.getHeight)
dstStack.addSlice(srcStack.getSliceLabel(1), ipR)
dstStack.addSlice(srcStack.getSliceLabel(2), ipG)
dstStack.addSlice(srcStack.getSliceLabel(3), ipB)

new ImagePlus("", dstStack)
}

def measureROI(imp: ImagePlus, roi: Roi): Seq[ImageStatistics] = {
val stack = imp.getStack
for (i <- 1 to stack.size()) yield {
val ip = stack.getProcessor(i)
ip.setRoi(roi)
ip.getStatistics
}
}

def toRGBStackImp(cp: ColorProcessor): ImagePlus = {
val rgb = splitRGB(cp)
val labels = Array("Red", "Green", "Blue")
val stack = new ImageStack(cp.getWidth, cp.getHeight)
(0 until 3).foreach { i =>
stack.addSlice(labels(i), rgb(i))
}
new ImagePlus("", stack)
}

def slices(imp: ImagePlus): Seq[ImageProcessor] =
val stack = imp.getStack
(1 to stack.getSize).map(stack.getProcessor)
}

0 comments on commit e59bcde

Please sign in to comment.