Skip to content

Commit

Permalink
feat: improve auto intensity contrast
Browse files Browse the repository at this point in the history
  • Loading branch information
cmhulbert committed Jun 4, 2024
1 parent 11540ce commit 73e1d57
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,32 @@ import javafx.collections.ObservableList
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent.KEY_PRESSED
import net.imglib2.RandomAccessibleInterval
import net.imglib2.loops.LoopBuilder
import net.imglib2.histogram.Histogram1d
import net.imglib2.histogram.Real1dBinMapper
import net.imglib2.realtransform.AffineTransform3D
import net.imglib2.type.label.LabelMultisetType
import net.imglib2.type.label.VolatileLabelMultisetType
import net.imglib2.type.numeric.IntegerType
import net.imglib2.type.numeric.RealType
import net.imglib2.type.numeric.integer.IntType
import net.imglib2.type.numeric.integer.UnsignedLongType
import net.imglib2.type.numeric.real.DoubleType
import net.imglib2.type.volatiles.AbstractVolatileRealType
import net.imglib2.util.Intervals
import net.imglib2.util.Util
import net.imglib2.view.IntervalView
import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX
import org.janelia.saalfeldlab.fx.actions.ActionSet
import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.installActionSet
import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.removeActionSet
import org.janelia.saalfeldlab.fx.actions.painteraActionSet
import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews
import org.janelia.saalfeldlab.fx.ui.ScaleView
import org.janelia.saalfeldlab.net.imglib2.converter.ARGBColorConverter
import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions
import org.janelia.saalfeldlab.paintera.control.tools.Tool
import org.janelia.saalfeldlab.paintera.paintera
import org.janelia.saalfeldlab.paintera.state.SourceStateBackendN5
import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState
import org.janelia.saalfeldlab.util.*

Expand All @@ -44,16 +55,16 @@ object RawSourceMode : AbstractToolMode() {
}
KEY_PRESSED(KeyCode.Y) {
lateinit var viewer: ViewerPanelFX
graphic = { ScaleView().apply { styleClass += "intensity-estimate-min-max" } }
graphic = { ScaleView().apply { styleClass += "intensity-auto-min-max" } }
verify("Last focused viewer found") { paintera.baseView.lastFocusHolder.value?.viewer()?.also { viewer = it } != null }
onAction {
val rawSource = activeSourceStateProperty.get() as ConnectomicsRawState<*, *>
estimateIntensityMinMax(rawSource, viewer)
autoIntensityMinMax(rawSource, viewer)
}
}
}

fun estimateIntensityMinMax(rawSource: ConnectomicsRawState<*, *>, viewer: ViewerPanelFX) {
fun autoIntensityMinMax(rawSource: ConnectomicsRawState<*, *>, viewer: ViewerPanelFX) {
val globalToViewerTransform = AffineTransform3D().also { viewer.state.getViewerTransform(it) }
val viewerInterval = Intervals.createMinSize(0, 0, 0, viewer.width.toLong(), viewer.height.toLong(), 1L)

Expand All @@ -63,36 +74,78 @@ object RawSourceMode : AbstractToolMode() {
val sourceToGlobalTransform = rawSource.getDataSource().getSourceTransformCopy(0, scaleLevel)


val extension = Util.getTypeFromInterval(dataSource).createVariable()
try {
extension.setReal(Double.NaN)
} catch (e: Exception) {
extension.setZero()
val extension = Util.getTypeFromInterval(dataSource).createVariable().let {
when (it) {
is VolatileLabelMultisetType, is LabelMultisetType -> UnsignedLongType(0)
else -> it
}
}


val screenSource = dataSource
.extendValue(extension)
.interpolateNearestNeighbor()
.affineReal(globalToViewerTransform.concatenate(sourceToGlobalTransform))
.raster()
.interval(viewerInterval)

var min = Double.NaN
var max = Double.NaN
LoopBuilder.setImages(screenSource).forEachPixel {
val value = it.realDouble
if (!value.isNaN()) {
if (value < min || min.isNaN()) min = value
if (value > max || max.isNaN()) max = value
val converter = rawSource.converter()
val curMin = converter.minProperty().get()
val curMax = converter.maxProperty().get()

if ( curMin == curMax) {
resetIntensityMinMax(rawSource)
return
}

if (extension is IntegerType<*>)
estimateWithHistogram(IntType(), screenSource, rawSource, converter)
else
estimateWithHistogram(DoubleType(), screenSource, rawSource, converter)
}

private fun <T : RealType<T>> estimateWithHistogram(type: T, screenSource: IntervalView<RealType<*>>, rawSource: ConnectomicsRawState<*, *>, converter: ARGBColorConverter<out AbstractVolatileRealType<*, *>>) {
val binMapper = Real1dBinMapper<T>(converter.min, converter.max, 4, false)
val histogram = Histogram1d(binMapper)
val img = screenSource.convert(type) { src, target -> target.setReal(src.realDouble) }.asIterable()
histogram.countData(img)

val numPixels = screenSource.dimensionsAsLongArray().sum()


val minBinIdx = histogram.indexOfFirst { i -> i.get() > (numPixels / 5000) }
val maxBinIdx = histogram.indexOfLast { i -> i.get() > (numPixels / 5000) }


when {
minBinIdx == -1 && maxBinIdx == -1 -> resetIntensityMinMax(rawSource)
minBinIdx == maxBinIdx -> {
converter.minProperty().value = histogram.getLowerBound(minBinIdx.toLong(), type).let { type.realDouble }
converter.maxProperty().value = histogram.getUpperBound(maxBinIdx.toLong(), type).let { type.realDouble }
}

else -> {
converter.minProperty().value = histogram.getCenterValue(minBinIdx.toLong(),type).let { type.realDouble }
converter.maxProperty().value = histogram.getCenterValue(maxBinIdx.toLong(),type).let { type.realDouble }
}
}
if (!min.isNaN()) rawSource.converter().minProperty().set(min)
if (!max.isNaN()) rawSource.converter().maxProperty().set(max)
}

fun resetIntensityMinMax(rawSource: ConnectomicsRawState<*, *>) {

(rawSource.backend as? SourceStateBackendN5<*, *>)?.getMetadataState()?.let {
rawSource.converter().min = it.minIntensity
rawSource.converter().max = it.maxIntensity
return
}

val dataSource = rawSource.getDataSource().getDataSource(0, 0) as RandomAccessibleInterval<RealType<*>>
val extension = Util.getTypeFromInterval(dataSource).createVariable()
val extension = Util.getTypeFromInterval(dataSource).createVariable().let {
when (it) {
is VolatileLabelMultisetType, is LabelMultisetType -> UnsignedLongType(0)
else -> it
}
}

rawSource.converter().minProperty().set(extension.minValue)
rawSource.converter().maxProperty().set(extension.maxValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,20 @@ class RawSourceStateConverterNode(private val converter: ARGBColorConverter<*>,
HBox.setHgrow(picker, Priority.ALWAYS)
tilePane.children.add(colorPickerBox)
val resetMinMax = Button("Reset Min / Max")
val estimateMinMax = Button("Estimate Min / Max")
val autoMinMax = Button("Auto Min / Max")

listOf(resetMinMax, estimateMinMax).forEach {
listOf(resetMinMax, autoMinMax).forEach {
HBox.setHgrow(it, Priority.ALWAYS)
it.maxWidth = Double.MAX_VALUE
}

resetMinMax.onAction = EventHandler { RawSourceMode.resetIntensityMinMax(state) }
estimateMinMax.onAction = EventHandler {
autoMinMax.onAction = EventHandler {
paintera.baseView.lastFocusHolder.value?.viewer()?.let { viewer ->
RawSourceMode.estimateIntensityMinMax(state, viewer)
RawSourceMode.autoIntensityMinMax(state, viewer)
}
}
val thresholdHBox = HBox(resetMinMax, estimateMinMax)
val thresholdHBox = HBox(resetMinMax, autoMinMax)

tilePane.children.add(thresholdHBox)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.janelia.saalfeldlab.util.n5.ImagesWithTransform
import org.janelia.saalfeldlab.util.n5.N5Data
import org.janelia.saalfeldlab.util.n5.N5Helpers
import org.janelia.saalfeldlab.util.n5.metadata.N5PainteraDataMultiScaleGroup
import org.janelia.saalfeldlab.util.n5.metadata.N5PainteraLabelMultiScaleGroup
import java.util.Optional

interface MetadataState {
Expand Down Expand Up @@ -182,11 +183,12 @@ open class MultiScaleMetadataState constructor(
}
}

class PainteraDataMultiscaleMetadataState constructor(
class PainteraDataMultiscaleMetadataState (
n5ContainerState: N5ContainerState,
var painteraDataMultiscaleMetadata: N5PainteraDataMultiScaleGroup
) : MultiScaleMetadataState(n5ContainerState, painteraDataMultiscaleMetadata) {

override var maxIntensity: Double = (painteraDataMultiscaleMetadata as? N5PainteraLabelMultiScaleGroup)?.maxId?.toDouble() ?: super.maxIntensity
val dataMetadataState = MultiScaleMetadataState(n5ContainerState, painteraDataMultiscaleMetadata.dataGroupMetadata)

override fun <D : NativeType<D>, T : Volatile<D>> getData(queue: SharedQueue, priority: Int): Array<ImagesWithTransform<D, T>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ import org.janelia.saalfeldlab.paintera.serialization.SerializationHelpers.fromC
import org.janelia.saalfeldlab.paintera.serialization.SerializationHelpers.withClassInfo
import org.janelia.saalfeldlab.paintera.serialization.StatefulSerializer
import org.janelia.saalfeldlab.paintera.serialization.StatefulSerializer.DeserializerFactory
import org.janelia.saalfeldlab.paintera.state.ARGBComposite
import org.janelia.saalfeldlab.paintera.state.RawSourceStateConverterNode
import org.janelia.saalfeldlab.paintera.state.SourceState
import org.janelia.saalfeldlab.paintera.state.SourceStateWithBackend
import org.janelia.saalfeldlab.paintera.state.*
import org.janelia.saalfeldlab.paintera.state.metadata.MetadataUtils
import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState.SerializationKeys.BACKEND
import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState.SerializationKeys.COMPOSITE
Expand Down Expand Up @@ -72,7 +69,12 @@ open class ConnectomicsRawState<D, T>(
) : SourceStateWithBackend<D, T>
where D : RealType<D>, T : AbstractVolatileRealType<D, T> {

private val converter = ARGBColorConverter.InvertingImp0<T>()
private val converter = ARGBColorConverter.InvertingImp0<T>().apply {
(backend as? SourceStateBackendN5<*, *>)?.getMetadataState()?.let {
min = it.minIntensity
max = it.maxIntensity
}
}

private val source: DataSource<D, T> = backend.createSource(queue, priority, name)

Expand Down
38 changes: 29 additions & 9 deletions src/main/resources/icons/intentisty-reset.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 28 additions & 8 deletions src/main/resources/icons/intentisty-threshold.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/main/resources/style/raw-source.css

Large diffs are not rendered by default.

0 comments on commit 73e1d57

Please sign in to comment.