-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
341 additions
and
1 deletion.
There are no files selected for viewing
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
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 |
127 changes: 127 additions & 0 deletions
127
ijp-color-ui/src/main/scala/ij_plugins/color/ui/util/WhiteBalancePlugIn.scala
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
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
212
ijp-color/src/main/scala/ij_plugins/color/util/WhiteBalance.scala
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
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) | ||
} |