Skip to content

Commit

Permalink
[api][desktop]: Improve Image Statistics
Browse files Browse the repository at this point in the history
* Compute statistics only when the dialog is open
* Statistics by channel
  • Loading branch information
tiagohm committed Oct 25, 2024
1 parent 0c499b4 commit 54a5121
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 147 deletions.
13 changes: 8 additions & 5 deletions api/src/main/kotlin/nebulosa/api/image/ImageController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import io.javalin.http.bodyAsClass
import nebulosa.api.connection.ConnectionService
import nebulosa.api.core.Controller
import nebulosa.api.core.location
import nebulosa.api.validators.enumOf
import nebulosa.api.validators.exists
import nebulosa.api.validators.notNull
import nebulosa.api.validators.path
import nebulosa.api.validators.range
import nebulosa.image.format.ImageChannel
import java.io.ByteArrayInputStream

class ImageController(
Expand All @@ -25,7 +26,7 @@ class ImageController(
app.put("image/analyze", ::analyze)
app.put("image/annotations", ::annotations)
app.get("image/coordinate-interpolation", ::coordinateInterpolation)
app.get("image/histogram", ::histogram)
app.post("image/statistics", ::statistics)
app.get("image/fov-cameras", ::fovCameras)
app.get("image/fov-telescopes", ::fovTelescopes)
}
Expand Down Expand Up @@ -65,10 +66,12 @@ class ImageController(
imageService.coordinateInterpolation(path)?.also(ctx::json)
}

private fun histogram(ctx: Context) {
private fun statistics(ctx: Context) {
val path = ctx.queryParam("path").notNull().path().exists()
val bitLength = ctx.queryParam("bitLength")?.toInt()?.range(8, 16) ?: 16
ctx.json(imageService.histogram(path, bitLength))
val transformation = ctx.bodyAsClass<ImageTransformation>()
val channel = ctx.queryParam("channel")?.enumOf<ImageChannel>() ?: ImageChannel.GRAY
val camera = ctx.queryParam("camera")?.ifBlank { null }?.let(connectionService::camera)
ctx.json(imageService.statistics(path, transformation, channel, camera))
}

private fun fovCameras(ctx: Context) {
Expand Down
21 changes: 10 additions & 11 deletions api/src/main/kotlin/nebulosa/api/image/ImageService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import nebulosa.api.framing.FramingService
import nebulosa.api.image.ImageAnnotation.StarDSO
import nebulosa.fits.*
import nebulosa.image.Image
import nebulosa.image.algorithms.computation.Histogram
import nebulosa.image.algorithms.computation.Statistics
import nebulosa.image.algorithms.transformation.*
import nebulosa.image.format.ImageChannel
import nebulosa.image.format.ImageHdu
import nebulosa.image.format.ImageModifier
import nebulosa.indi.device.camera.Camera
Expand Down Expand Up @@ -62,11 +62,11 @@ class ImageService(
private enum class ImageOperation {
OPEN,
SAVE,
STATISTICS,
}

private data class TransformedImage(
@JvmField val image: Image,
@JvmField val statistics: Statistics.Data? = null,
@JvmField val stretchParameters: ScreenTransformFunction.Parameters? = null,
@JvmField val instrument: Camera? = null,
)
Expand All @@ -87,7 +87,7 @@ class ImageService(
output: HttpServletResponse,
) {
val (image, calibration) = imageBucket.open(path, transformation.debayer, force = transformation.force)
val (transformedImage, statistics, stretchParameters, instrument) = image!!.transform(true, transformation, ImageOperation.OPEN, camera)
val (transformedImage, stretchParameters, instrument) = image!!.transform(true, transformation, ImageOperation.OPEN, camera)

val info = ImageInfo(
path,
Expand All @@ -101,7 +101,7 @@ class ImageService(
transformedImage.header.declination.takeIf { it.isFinite() },
calibration?.let(::ImageSolved),
transformedImage.header.mapNotNull { if (it.isCommentStyle) null else ImageHeaderItem(it.key, it.value) },
transformedImage.header.bitpix, instrument, statistics,
transformedImage.header.bitpix, instrument,
)

val format = if (transformation.useJPEG) "jpeg" else "png"
Expand Down Expand Up @@ -147,12 +147,9 @@ class ImageService(
.transform(transformedImage)
}

val statistics = if (operation == ImageOperation.OPEN) transformedImage.compute(Statistics.GRAY)
else null

var stretchParams = ScreenTransformFunction.Parameters.DEFAULT

if (enabled) {
if (enabled && operation != ImageOperation.STATISTICS) {
if (autoStretch) {
stretchParams = AdaptativeScreenTransformFunction(transformation.stretch.meanBackground).compute(transformedImage)
transformedImage = ScreenTransformFunction(stretchParams).transform(transformedImage)
Expand All @@ -166,7 +163,7 @@ class ImageService(
transformedImage = Invert.transform(transformedImage)
}

return TransformedImage(transformedImage, statistics, stretchParams, instrument)
return TransformedImage(transformedImage, stretchParams, instrument)
}

@Synchronized
Expand Down Expand Up @@ -361,8 +358,10 @@ class ImageService(
return CoordinateInterpolation(ma, md, 0, 0, width, height, delta, image.header.observationDate)
}

fun histogram(path: Path, bitLength: Int = 16): IntArray {
return imageBucket.open(path).image?.compute(Histogram(bitLength = bitLength)) ?: IntArray(0)
fun statistics(path: Path, transformation: ImageTransformation, channel: ImageChannel, camera: Camera?): Statistics.Data {
val (image) = imageBucket.open(path, transformation.debayer)
val (transformedImage) = image!!.transform(true, transformation, ImageOperation.STATISTICS, camera)
return transformedImage.compute(Statistics.CHANNELS[channel] ?: return Statistics.Data.EMPTY)
}

companion object {
Expand Down
215 changes: 113 additions & 102 deletions desktop/src/app/image/image.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -892,109 +892,120 @@
[modal]="false"
[style]="{ width: 'min-content', minWidth: '336px' }"
class="pointer-events-none">
<div
class="grid p-2"
*ngIf="imageInfo">
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="statistics.statistics.count" />
<label>Count (px)</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.mean * statistics.bitOption.rangeMax).toFixed(8)" />
<label>Mean</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.median * statistics.bitOption.rangeMax).toFixed(8)" />
<label>Median</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.variance * statistics.bitOption.rangeMax * statistics.bitOption.rangeMax).toFixed(8)" />
<label>Variance</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.avgDev * statistics.bitOption.rangeMax).toFixed(8)" />
<label>Avg Dev</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.stdDev * statistics.bitOption.rangeMax).toFixed(8)" />
<label>Std Dev</label>
</p-floatLabel>
</div>
<div class="col-3">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.minimum * statistics.bitOption.rangeMax).toFixed(8)" />
<label>Minimum</label>
</p-floatLabel>
</div>
<div class="col-3">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.maximum * statistics.bitOption.rangeMax).toFixed(8)" />
<label>Maximum</label>
</p-floatLabel>
</div>
<div class="col-6">
<p-floatLabel class="w-full">
<p-dropdown
[options]="'IMAGE_STATISTICS_BIT_OPTION' | dropdownOptions"
[(ngModel)]="statistics.bitOption"
optionLabel="name"
[autoDisplayFirst]="true"
styleClass="p-inputtext-sm border-0"
appendTo="body"
(ngModelChange)="savePreference()" />
<label>Bits</label>
</p-floatLabel>
</div>
<div class="col-12">
<neb-histogram
#histogram
class="w-full"
style="max-height: 94px" />
@if (statistics.statistics && imageInfo) {
<div class="grid p-2">
<div class="col-12 flex align-items-center">
<p-selectButton
styleClass="border-0"
[disabled]="imageInfo.mono"
[options]="'IMAGE_CHANNEL' | dropdownOptions | enumDropdown"
optionLabel="label"
optionValue="value"
[(ngModel)]="statistics.channel"
(ngModelChange)="computeStatistics()"
[multiple]="false" />
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="statistics.statistics.count" />
<label>Count (px)</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.mean * statistics.bitOption.rangeMax).toFixed(statistics.bitOption.decimalPlaces)" />
<label>Mean</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.median * statistics.bitOption.rangeMax).toFixed(statistics.bitOption.decimalPlaces)" />
<label>Median</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.variance * statistics.bitOption.rangeMax * statistics.bitOption.rangeMax).toFixed(statistics.bitOption.decimalPlaces)" />
<label>Variance</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.avgDev * statistics.bitOption.rangeMax).toFixed(statistics.bitOption.decimalPlaces)" />
<label>Avg Dev</label>
</p-floatLabel>
</div>
<div class="col-4">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.stdDev * statistics.bitOption.rangeMax).toFixed(statistics.bitOption.decimalPlaces)" />
<label>Std Dev</label>
</p-floatLabel>
</div>
<div class="col-3">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.minimum * statistics.bitOption.rangeMax).toFixed(statistics.bitOption.decimalPlaces)" />
<label>Minimum</label>
</p-floatLabel>
</div>
<div class="col-3">
<p-floatLabel>
<input
pInputText
readonly
class="p-inputtext-sm border-0 w-full"
[value]="(statistics.statistics.maximum * statistics.bitOption.rangeMax).toFixed(statistics.bitOption.decimalPlaces)" />
<label>Maximum</label>
</p-floatLabel>
</div>
<div class="col-6">
<p-floatLabel class="w-full">
<p-dropdown
[options]="'IMAGE_STATISTICS_BIT_OPTION' | dropdownOptions"
[(ngModel)]="statistics.bitOption"
optionLabel="name"
[autoDisplayFirst]="true"
styleClass="p-inputtext-sm border-0"
appendTo="body"
(ngModelChange)="savePreference()" />
<label>Bits</label>
</p-floatLabel>
</div>
<div class="col-12">
<neb-histogram
#histogram
class="w-full"
style="max-height: 94px" />
</div>
</div>
</div>
}
</p-dialog>

<p-dialog
Expand Down
Loading

0 comments on commit 54a5121

Please sign in to comment.