From 96a1e6e718acddba1eb8b1146983e3a6809b98c0 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 5 Jun 2024 18:53:05 -0300 Subject: [PATCH] [api][desktop]: Implement PixInsight Pixel Math --- .../calibration/CalibrationFrameService.kt | 37 +++++--- desktop/src/app/image/image.component.html | 61 ++++++------ desktop/src/app/image/image.component.ts | 53 ++++++----- desktop/src/shared/types/image.types.ts | 11 ++- nebulosa-pixinsight/build.gradle.kts | 1 + .../livestacking/PixInsightLiveStacker.kt | 89 ++++++++++++++---- .../script/AbstractPixInsightScript.kt | 17 +--- .../pixinsight/script/PixInsightAlign.kt | 27 ++++-- .../pixinsight/script/PixInsightCalibrate.kt | 31 ++++-- .../script/PixInsightDetectStars.kt | 30 +++--- .../pixinsight/script/PixInsightPixelMath.kt | 71 ++++++++++++++ .../pixinsight/script/PixInsightStartup.kt | 2 +- .../src/main/resources/pixinsight/Align.js | 20 +++- .../main/resources/pixinsight/Calibrate.js | 40 +++++--- .../main/resources/pixinsight/DetectStars.js | 20 +++- .../main/resources/pixinsight/PixelMath.js | 94 +++++++++++++++++++ .../src/test/kotlin/PixInsightScriptTest.kt | 6 ++ .../test/kotlin/PixInsightStarDetectorTest.kt | 3 + 18 files changed, 460 insertions(+), 153 deletions(-) create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt index 25ecd51d6..012a7dbd7 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt @@ -31,26 +31,37 @@ class CalibrationFrameService( fun calibrate(name: String, image: Image, createNew: Boolean = false): Image { return synchronized(image) { val darkFrame = findBestDarkFrames(name, image).firstOrNull() - val biasFrame = findBestBiasFrames(name, image).firstOrNull() + val biasFrame = if (darkFrame == null) findBestBiasFrames(name, image).firstOrNull() else null val flatFrame = findBestFlatFrames(name, image).firstOrNull() - if (darkFrame != null || biasFrame != null || flatFrame != null) { + val darkImage = darkFrame?.path?.fits()?.use(Image::open) + val biasImage = biasFrame?.path?.fits()?.use(Image::open) + var flatImage = flatFrame?.path?.fits()?.use(Image::open) + + if (darkImage != null || biasImage != null || flatImage != null) { var transformedImage = if (createNew) image.clone() else image - if (biasFrame != null) { - val calibrationImage = biasFrame.path!!.fits().use(Image::open) - transformedImage = transformedImage.transform(BiasSubtraction(calibrationImage)) + // If not using dark frames. + if (biasImage != null) { + // Subtract Master Bias from Flat Frames. + if (flatImage != null) { + flatImage = flatImage.transform(BiasSubtraction(biasImage)) + LOG.info("bias frame subtraction applied to flat frame. frame={}", biasFrame) + } + + // Subtract the Master Bias frame. + transformedImage = transformedImage.transform(BiasSubtraction(biasImage)) LOG.info("bias frame subtraction applied. frame={}", biasFrame) - } else { + } else if (darkFrame == null) { LOG.info( "no bias frames found. width={}, height={}, bin={}, gain={}", image.width, image.height, image.header.binX, image.header.gain ) } - if (darkFrame != null) { - val calibrationImage = darkFrame.path!!.fits().use(Image::open) - transformedImage = transformedImage.transform(DarkSubtraction(calibrationImage)) + // Subtract Master Dark frame. + if (darkImage != null) { + transformedImage = transformedImage.transform(DarkSubtraction(darkImage)) LOG.info("dark frame subtraction applied. frame={}", darkFrame) } else { LOG.info( @@ -59,9 +70,9 @@ class CalibrationFrameService( ) } - if (flatFrame != null) { - val calibrationImage = flatFrame.path!!.fits().use(Image::open) - transformedImage = transformedImage.transform(FlatCorrection(calibrationImage)) + // Divide the Dark-subtracted Light frame by the Master Flat frame to correct for variations in the optical path. + if (flatImage != null) { + transformedImage = transformedImage.transform(FlatCorrection(flatImage)) LOG.info("flat frame correction applied. frame={}", flatFrame) } else { LOG.info( @@ -177,7 +188,7 @@ class CalibrationFrameService( name: String, width: Int, height: Int, binX: Int, binY: Int, filter: String? ): List { - // TODO: Generate master from matched frames. + // TODO: Generate master from matched frames. (Subtract the master bias frame from each flat frame) return calibrationFrameRepository .flatFrames(name, filter, width, height, binX) } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index a0056fee1..e9acad941 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -10,9 +10,9 @@ - - + {{ (a.star ?? a.dso ?? a.minorPlanet) | skyObject:'name' }} @@ -85,71 +85,76 @@ - + -
- +
- +
-
-
-
+
-
-
+
- +
-
+
- +
-
+
-
-
- Simbad +
+ Simbad
- - - - + + + +
@@ -231,18 +236,18 @@
- - - -
- + @@ -499,7 +504,7 @@
- + diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 82ff751a2..68709727e 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -16,9 +16,9 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' -import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from '../../shared/types/atlas.types' +import { Angle, EquatorialCoordinateJ2000 } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' -import { DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, StarDetectionDialog } from '../../shared/types/image.types' +import { AnnotationInfoDialog, DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, StarDetectionDialog } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' @@ -92,15 +92,22 @@ export class ImageComponent implements AfterViewInit, OnDestroy { readonly annotation: ImageAnnotationDialog = { showDialog: false, + running: false, + visible: false, useStarsAndDSOs: true, useMinorPlanets: false, minorPlanetsMagLimit: 18.0, - useSimbad: false + useSimbad: false, + data: [] + } + + readonly annotationInfo: AnnotationInfoDialog = { + showDialog: false } - detecting = false readonly starDetection: StarDetectionDialog = { showDialog: false, + running: false, type: 'ASTAP', minSNR: 0, visible: false, @@ -122,7 +129,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { readonly solver: ImageSolverDialog = { showDialog: false, - solving: false, + running: false, blind: true, centerRA: '', centerDEC: '', @@ -132,11 +139,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } crossHair = false - annotations: ImageAnnotation[] = [] - annotating = false - showAnnotationInfoDialog = false - annotationInfo?: AstronomicalObject & Partial - annotationIsVisible = false readonly fitsHeaders: ImageFITSHeadersDialog = { showDialog: false, @@ -341,7 +343,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, toggle: (event) => { event.originalEvent?.stopImmediatePropagation() - this.annotationIsVisible = event.checked + this.annotation.visible = event.checked }, } @@ -752,8 +754,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private clearOverlay() { - this.annotations = [] - this.annotationIsVisible = false + this.annotation.data = [] + this.annotation.visible = false this.annotationMenuItem.toggleable = false this.starDetection.stars = [] @@ -775,15 +777,14 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } async detectStars() { - this.detecting = true - const options = this.preference.starDetectionRequest(this.starDetection.type).get() options.minSNR = this.starDetection.minSNR try { + this.starDetection.running = true this.starDetection.stars = await this.api.detectStars(this.imagePath!, options) } finally { - this.detecting = false + this.starDetection.running = false } let hfd = 0 @@ -906,21 +907,21 @@ export class ImageComponent implements AfterViewInit, OnDestroy { async annotateImage() { try { - this.annotating = true - this.annotations = await this.api.annotationsOfImage(this.imagePath!, this.annotation.useStarsAndDSOs, + this.annotation.running = true + this.annotation.data = await this.api.annotationsOfImage(this.imagePath!, this.annotation.useStarsAndDSOs, this.annotation.useMinorPlanets, this.annotation.minorPlanetsMagLimit, this.annotation.useSimbad) - this.annotationIsVisible = true - this.annotationMenuItem.toggleable = this.annotations.length > 0 - this.annotationMenuItem.toggled = this.annotationMenuItem.toggleable + this.annotation.visible = this.annotation.data.length > 0 + this.annotationMenuItem.toggleable = this.annotation.visible + this.annotationMenuItem.toggled = this.annotation.visible this.annotation.showDialog = false } finally { - this.annotating = false + this.annotation.running = false } } showAnnotationInfo(annotation: ImageAnnotation) { - this.annotationInfo = annotation.star ?? annotation.dso ?? annotation.minorPlanet - this.showAnnotationInfoDialog = true + this.annotationInfo.info = annotation.star ?? annotation.dso ?? annotation.minorPlanet + this.annotationInfo.showDialog = true } private disableAutoStretch() { @@ -1037,7 +1038,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } async solveImage() { - this.solver.solving = true + this.solver.running = true try { const solver = this.preference.plateSolverRequest(this.solver.type).get() @@ -1049,7 +1050,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } catch { this.updateImageSolved(this.imageInfo?.solved) } finally { - this.solver.solving = false + this.solver.running = false if (this.solver.solved.solved) { this.retrieveCoordinateInterpolation() diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index 1aa7d5442..4960661d5 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -209,7 +209,7 @@ export interface ImageStretchDialog { export interface ImageSolverDialog { showDialog: boolean - solving: boolean + running: boolean blind: boolean centerRA: Angle centerDEC: Angle @@ -259,10 +259,13 @@ export interface ImageTransformation { export interface ImageAnnotationDialog { showDialog: boolean + running: boolean + visible: boolean useStarsAndDSOs: boolean useMinorPlanets: boolean minorPlanetsMagLimit: number useSimbad: boolean + data: ImageAnnotation[] } export interface ROISelected { @@ -275,6 +278,7 @@ export interface ROISelected { export interface StarDetectionDialog { showDialog: boolean + running: boolean type: StarDetectorType minSNR: number visible: boolean @@ -282,3 +286,8 @@ export interface StarDetectionDialog { computed: Omit & { minFlux: number, maxFlux: number } selected: DetectedStar } + +export interface AnnotationInfoDialog { + showDialog: boolean + info?: AstronomicalObject & Partial +} diff --git a/nebulosa-pixinsight/build.gradle.kts b/nebulosa-pixinsight/build.gradle.kts index 4c7d645c6..1819f5ff6 100644 --- a/nebulosa-pixinsight/build.gradle.kts +++ b/nebulosa-pixinsight/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { api(project(":nebulosa-star-detection")) api(project(":nebulosa-livestacking")) api(libs.bundles.jackson) + api(libs.apache.codec) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt index 036a5adca..f1bed390f 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt @@ -1,12 +1,11 @@ package nebulosa.pixinsight.livestacking import nebulosa.livestacking.LiveStacker -import nebulosa.pixinsight.script.PixInsightIsRunning -import nebulosa.pixinsight.script.PixInsightScript -import nebulosa.pixinsight.script.PixInsightScriptRunner -import nebulosa.pixinsight.script.PixInsightStartup +import nebulosa.pixinsight.script.* import java.nio.file.Path import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.moveTo +import kotlin.io.path.name data class PixInsightLiveStacker( private val runner: PixInsightScriptRunner, @@ -15,42 +14,94 @@ data class PixInsightLiveStacker( private val flat: Path? = null, private val bias: Path? = null, private val use32Bits: Boolean = false, + private val slot: Int = PixInsightScript.DEFAULT_SLOT, ) : LiveStacker { private val running = AtomicBoolean() + private val stacking = AtomicBoolean() override val isRunning get() = running.get() - override val isStacking: Boolean - get() = TODO("Not yet implemented") + override val isStacking + get() = stacking.get() + + @Volatile private var stackCount = 0 + @Volatile private var referencePath: Path? = null + @Volatile private var stackedPath: Path? = null @Synchronized override fun start() { - val isPixInsightRunning = PixInsightIsRunning(PixInsightScript.DEFAULT_SLOT).runSync(runner) + if (!running.get()) { + val isPixInsightRunning = PixInsightIsRunning(slot).use { it.runSync(runner) } - if (!isPixInsightRunning) { - try { - check(PixInsightStartup(PixInsightScript.DEFAULT_SLOT).runSync(runner)) - } catch (e: Throwable) { - throw IllegalStateException("Unable to start PixInsight") - } + if (!isPixInsightRunning) { + try { + check(PixInsightStartup(slot).use { it.runSync(runner) }) + } catch (e: Throwable) { + throw IllegalStateException("unable to start PixInsight") + } - running.set(true) + running.set(true) + } } } @Synchronized override fun add(path: Path): Path? { - return null + var targetPath = path + + if (running.get()) { + stacking.set(true) + + // Calibrate. + val calibratedPath = if (dark == null && flat == null && bias == null) null else { + PixInsightCalibrate(slot, targetPath, dark, flat, if (dark == null) bias else null).use { + val outputPath = it.runSync(runner).outputImage ?: return@use null + val destinationPath = Path.of("$workingDirectory", outputPath.name) + outputPath.moveTo(destinationPath, true) + } + } + + if (calibratedPath != null) { + targetPath = calibratedPath + } + + // TODO: Debayer, Resample? + + if (stackCount > 0) { + // Align. + val alignedPath = PixInsightAlign(slot, referencePath!!, targetPath).use { + val outputPath = it.runSync(runner).outputImage ?: return@use null + val destinationPath = Path.of("$workingDirectory", outputPath.name) + outputPath.moveTo(destinationPath, true) + } + + if (alignedPath != null) { + targetPath = alignedPath + } + + // Stack. + } else { + referencePath = targetPath + } + + stackedPath = targetPath + stackCount++ + + stacking.set(false) + } + + return stackedPath } @Synchronized override fun stop() { - + running.set(false) + stackCount = 0 + referencePath = null + stackedPath = null } - override fun close() { - - } + override fun close() = Unit } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt index 76cb56794..1519e89ff 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -5,7 +5,9 @@ import com.fasterxml.jackson.module.kotlin.kotlinModule import nebulosa.common.exec.CommandLine import nebulosa.common.exec.LineReadListener import nebulosa.common.json.PathDeserializer +import nebulosa.common.json.PathSerializer import nebulosa.log.loggerFor +import org.apache.commons.codec.binary.Hex import java.nio.file.Path import java.util.concurrent.CompletableFuture @@ -48,27 +50,18 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen @JvmStatic private val KOTLIN_MODULE = kotlinModule() .addDeserializer(Path::class.java, PathDeserializer) + .addSerializer(PathSerializer) @JvmStatic internal val OBJECT_MAPPER = jsonMapper { addModule(KOTLIN_MODULE) } @JvmStatic - internal fun parameterize(slot: Int, scriptPath: Path, vararg parameters: Any?): String { + internal fun execute(slot: Int, scriptPath: Path, data: Any): String { return buildString { if (slot > 0) append("$slot:") - append("\"$scriptPath,") - - parameters.forEachIndexed { i, parameter -> - if (i > 0) append(',') - - if (parameter is Path) append("'$parameter'") - else if (parameter is CharSequence) append("'$parameter'") - else if (parameter != null) append("$parameter") - else append('0') - } - + append(Hex.encodeHexString(OBJECT_MAPPER.writeValueAsBytes(data))) append('"') } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt index 2a40a49bc..f7306ee27 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt @@ -13,9 +13,16 @@ data class PixInsightAlign( private val slot: Int, private val referencePath: Path, private val targetPath: Path, -) : AbstractPixInsightScript() { +) : AbstractPixInsightScript() { - data class Result( + private data class Input( + @JvmField val referencePath: Path, + @JvmField val targetPath: Path, + @JvmField val outputDirectory: Path, + @JvmField val statusPath: Path, + ) + + data class Output( @JvmField val outputImage: Path? = null, @JvmField val outputMaskImage: Path? = null, @JvmField val totalPairMatches: Int = 0, @@ -40,39 +47,39 @@ data class PixInsightAlign( companion object { - @JvmStatic val FAILED = Result() + @JvmStatic val FAILED = Output() } } private val outputDirectory = Files.createTempDirectory("pi-align-") private val scriptPath = Files.createTempFile("pi-", ".js") - private val outputPath = Files.createTempFile("pi-", ".txt") + private val statusPath = Files.createTempFile("pi-", ".txt") init { resource("pixinsight/Align.js")!!.transferAndClose(scriptPath.outputStream()) } - override val arguments = listOf("-x=${parameterize(slot, scriptPath, referencePath, targetPath, outputDirectory, outputPath)}") + override val arguments = listOf("-x=${execute(slot, scriptPath, Input(referencePath, targetPath, outputDirectory, statusPath))}") - override fun processOnComplete(exitCode: Int): Result { + override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { repeat(5) { - val text = outputPath.readText() + val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { - return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) } Thread.sleep(1000) } } - return Result.FAILED + return Output.FAILED } override fun close() { scriptPath.deleteIfExists() - outputPath.deleteIfExists() + statusPath.deleteIfExists() outputDirectory.deleteRecursively() } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt index b8e781707..2ed40a104 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -17,48 +17,59 @@ data class PixInsightCalibrate( private val bias: Path? = null, private val compress: Boolean = false, private val use32Bit: Boolean = false, -) : AbstractPixInsightScript() { +) : AbstractPixInsightScript() { - data class Result( + private data class Input( + @JvmField val targetPath: Path, + @JvmField val outputDirectory: Path, + @JvmField val statusPath: Path, + @JvmField val masterDark: Path? = null, + @JvmField val masterFlat: Path? = null, + @JvmField val masterBias: Path? = null, + @JvmField val compress: Boolean = false, + @JvmField val use32Bit: Boolean = false, + ) + + data class Output( @JvmField val outputImage: Path? = null, ) { companion object { - @JvmStatic val FAILED = Result() + @JvmStatic val FAILED = Output() } } private val outputDirectory = Files.createTempDirectory("pi-calibrate-") private val scriptPath = Files.createTempFile("pi-", ".js") - private val outputPath = Files.createTempFile("pi-", ".txt") + private val statusPath = Files.createTempFile("pi-", ".txt") init { resource("pixinsight/Calibrate.js")!!.transferAndClose(scriptPath.outputStream()) } override val arguments = - listOf("-x=${parameterize(slot, scriptPath, targetPath, outputDirectory, outputPath, dark, flat, bias, compress, use32Bit)}") + listOf("-x=${execute(slot, scriptPath, Input(targetPath, outputDirectory, statusPath, dark, flat, bias, compress, use32Bit))}") - override fun processOnComplete(exitCode: Int): Result { + override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { repeat(5) { - val text = outputPath.readText() + val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { - return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) } Thread.sleep(1000) } } - return Result.FAILED + return Output.FAILED } override fun close() { scriptPath.deleteIfExists() - outputPath.deleteIfExists() + statusPath.deleteIfExists() outputDirectory.deleteRecursively() } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt index 5a64f52e1..01ed54d94 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt @@ -16,14 +16,22 @@ data class PixInsightDetectStars( private val minSNR: Double = 0.0, private val invert: Boolean = false, private val timeout: Duration = Duration.ZERO, -) : AbstractPixInsightScript() { +) : AbstractPixInsightScript() { - @Suppress("ArrayInDataClass") - data class Result(@JvmField val stars: Array = emptyArray()) { + private data class Input( + @JvmField val targetPath: Path, + @JvmField val statusPath: Path, + @JvmField val minSNR: Double = 0.0, + @JvmField val invert: Boolean = false, + ) + + data class Output( + @JvmField val stars: List = emptyList(), + ) { companion object { - @JvmStatic val FAILED = Result() + @JvmStatic val FAILED = Output() } } @@ -44,25 +52,25 @@ data class PixInsightDetectStars( ) : ImageStar private val scriptPath = Files.createTempFile("pi-", ".js") - private val outputPath = Files.createTempFile("pi-", ".txt") + private val statusPath = Files.createTempFile("pi-", ".txt") init { resource("pixinsight/DetectStars.js")!!.transferAndClose(scriptPath.outputStream()) } - override val arguments = listOf("-x=${parameterize(slot, scriptPath, targetPath, outputPath, minSNR, invert)}") + override val arguments = listOf("-x=${execute(slot, scriptPath, Input(targetPath, statusPath, minSNR, invert))}") - override fun processOnComplete(exitCode: Int): Result { + override fun processOnComplete(exitCode: Int): Output { val timeoutInMillis = timeout.toMillis() if (exitCode == 0) { val startTime = System.currentTimeMillis() repeat(600) { - val text = outputPath.readText() + val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { - return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) } if (timeoutInMillis == 0L || System.currentTimeMillis() - startTime < timeoutInMillis) { @@ -73,11 +81,11 @@ data class PixInsightDetectStars( } } - return Result.FAILED + return Output.FAILED } override fun close() { scriptPath.deleteIfExists() - outputPath.deleteIfExists() + statusPath.deleteIfExists() } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt new file mode 100644 index 000000000..cd207907e --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt @@ -0,0 +1,71 @@ +package nebulosa.pixinsight.script + +import nebulosa.io.resource +import nebulosa.io.transferAndClose +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.deleteIfExists +import kotlin.io.path.outputStream +import kotlin.io.path.readText + +data class PixInsightPixelMath( + private val slot: Int, + private val inputPaths: List, + private val outputPath: Path, + private val expressionRK: String? = null, + private val expressionG: String? = null, + private val expressionB: String? = null, +) : AbstractPixInsightScript() { + + private data class Input( + @JvmField val outputDirectory: Path, + @JvmField val statusPath: Path, + @JvmField val inputPaths: List, + @JvmField val outputPath: Path, + @JvmField val expressionRK: String? = null, + @JvmField val expressionG: String? = null, + @JvmField val expressionB: String? = null, + ) + + data class Output( + @JvmField val stackedImage: Path? = null, + ) { + + companion object { + + @JvmStatic val FAILED = Output() + } + } + + private val outputDirectory = Files.createTempDirectory("pi-pixelmath-") + private val scriptPath = Files.createTempFile("pi-", ".js") + private val statusPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/PixelMath.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = + listOf("-x=${execute(slot, scriptPath, Input(outputDirectory, statusPath, inputPaths, outputPath, expressionRK, expressionG, expressionB))}") + + override fun processOnComplete(exitCode: Int): Output? { + if (exitCode == 0) { + repeat(5) { + val text = statusPath.readText() + + if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) + } + + Thread.sleep(1000) + } + } + + return Output.FAILED + } + + override fun close() { + scriptPath.deleteIfExists() + statusPath.deleteIfExists() + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt index ef009556f..e410a4ea9 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt @@ -17,7 +17,7 @@ data class PixInsightStartup(private val slot: Int) : AbstractPixInsightScript 0) "-n=$slot" else "-n") + override val arguments = listOf("-r=${execute(0, scriptPath, outputPath)}", if (slot > 0) "-n=$slot" else "-n") override fun beforeRun() { var count = 0 diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js index 6638b4b23..0d28d88de 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js @@ -1,8 +1,20 @@ +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + function alignment() { - const referencePath = jsArguments[0] - const targetPath = jsArguments[1] - const outputDirectory = jsArguments[2] - const statusPath = jsArguments[3] + const input = decodeParams(jsArguments[0]) + + const referencePath = input.referencePath + const targetPath = input.targetPath + const outputDirectory = input.outputDirectory + const statusPath = input.statusPath console.writeln("referencePath=" + referencePath) console.writeln("targetPath=" + targetPath) diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js index 4ac72ab8d..c5798cf75 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -1,12 +1,24 @@ +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + function calibrate() { - const targetPath = jsArguments[0] - const outputDirectory = jsArguments[1] - const statusPath = jsArguments[2] - const masterDark = jsArguments[3] - const masterFlat = jsArguments[4] - const masterBias = jsArguments[5] - const compress = jsArguments[6].toLowerCase() === 'true' - const use32Bit = jsArguments[7].toLowerCase() === 'true' + const input = decodeParams(jsArguments[0]) + + const targetPath = input.targetPath + const outputDirectory = input.outputDirectory + const statusPath = input.statusPath + const masterDark = input.masterDark + const masterFlat = input.masterFlat + const masterBias = input.masterBias + const compress = input.compress + const use32Bit = input.use32Bit console.writeln("targetPath=" + targetPath) console.writeln("outputDirectory=" + outputDirectory) @@ -40,12 +52,12 @@ function calibrate() { [false, 0, 0, 0, 0, 0, 0, 0, 0], [false, 0, 0, 0, 0, 0, 0, 0, 0] ] - P.masterBiasEnabled = masterBias !== "0" - P.masterBiasPath = masterBias - P.masterDarkEnabled = masterDark !== "0" - P.masterDarkPath = masterDark - P.masterFlatEnabled = masterFlat !== "0" - P.masterFlatPath = masterFlat + P.masterBiasEnabled = !!masterBias + P.masterBiasPath = masterBias || "" + P.masterDarkEnabled = !!masterDark + P.masterDarkPath = masterDark || "" + P.masterFlatEnabled = !!masterFlat + P.masterFlatPath = masterFlat || "" P.calibrateBias = false P.calibrateDark = false P.calibrateFlat = false diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js index 60864248f..129407d7e 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js @@ -795,11 +795,23 @@ function computeHfr(image, s) { s.hfr = b > 0.0 ? a / b : 0.0 } +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + function detectStars() { - const targetPath = jsArguments[0] - const statusPath = jsArguments[1] - const minSNR = parseFloat(jsArguments[2]) - const invert = jsArguments[3].toLowerCase() === 'true' + const input = decodeParams(jsArguments[0]) + + const targetPath = input.targetPath + const statusPath = input.statusPath + const minSNR = input.minSNR + const invert = input.invert console.writeln("targetPath=" + targetPath) console.writeln("statusPath=" + statusPath) diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js new file mode 100644 index 000000000..c4d199db4 --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js @@ -0,0 +1,94 @@ +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + +function pixelMath() { + const input = decodeParams(jsArguments[0]) + + const outputDirectory = input.outputDirectory + const statusPath = input.statusPath + const inputPaths = input.inputPaths + const outputPath = input.outputPath + let expressionRK = input.expressionRK + let expressionG = input.expressionG + let expressionB = input.expressionB + + console.writeln("outputDirectory=" + outputDirectory) + console.writeln("statusPath=" + statusPath) + console.writeln("inputPaths=" + inputPaths) + console.writeln("outputPath=" + outputPath) + + const windows = [] + + for(let i = 0; i < input.inputPaths.length; i++) { + windows.push(ImageWindow.open(input.inputPaths[i])[0]) + } + + for(let i = 0; i < windows.length; i++) { + if (expressionRK) { + expressionRK = expressionRK.replace("{{" + i + "}}", windows[i].mainView.id) + } + if (expressionG) { + expressionG = expressionG.replace("{{" + i + "}}", windows[i].mainView.id) + } + if (expressionB) { + expressionB = expressionB.replace("{{" + i + "}}", windows[i].mainView.id) + } + } + + console.writeln("expressionRK=" + expressionRK) + console.writeln("expressionG=" + expressionG) + console.writeln("expressionB=" + expressionB) + + var P = new PixelMath + P.expression = expressionRK || "" + P.expression1 = expressionG || "" + P.expression2 = expressionB || "" + P.expression3 = "" + P.useSingleExpression = false + P.symbols = "" + P.clearImageCacheAndExit = false + P.cacheGeneratedImages = false + P.generateOutput = true + P.singleThreaded = false + P.optimization = true + P.use64BitWorkingImage = false + P.rescale = false + P.rescaleLower = 0 + P.rescaleUpper = 1 + P.truncate = true + P.truncateLower = 0 + P.truncateUpper = 1 + P.createNewImage = false + P.showNewImage = false + P.newImageId = "" + P.newImageWidth = 0 + P.newImageHeight = 0 + P.newImageAlpha = false + P.newImageColorSpace = PixelMath.prototype.SameAsTarget + P.newImageSampleFormat = PixelMath.prototype.SameAsTarget + + P.executeOn(windows[0].mainView) + + windows[0].saveAs(outputPath, false, false, false, false) + + for(let i = 0; i < windows.length; i++) { + windows[i].forceClose() + } + + console.writeln("stacking finished") + + const json = { + stackedImage: outputPath, + } + + File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") +} + +pixelMath() diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index c0b43c404..6d7a06d96 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -7,6 +7,7 @@ import io.kotest.matchers.shouldBe import nebulosa.pixinsight.script.* import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.test.NonGitHubOnlyCondition +import java.nio.file.Files import java.nio.file.Path @EnabledIf(NonGitHubOnlyCondition::class) @@ -47,5 +48,10 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { .map { it.hfd } .average() shouldBe (20.88 plusOrMinus 1e-2) } + "pixel math" { + val outputPath = Files.createTempFile("pi-stacked-", ".fits") + PixInsightPixelMath(PixInsightScript.UNSPECIFIED_SLOT, listOf(PI_01_LIGHT, PI_02_LIGHT), outputPath, "{{0}} + {{1}}") + .use { it.runSync(runner).also(::println).stackedImage.shouldNotBeNull().shouldExist() } + } } } diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt index e16408ef5..d14437843 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt @@ -1,9 +1,12 @@ +import io.kotest.core.annotation.EnabledIf import io.kotest.matchers.collections.shouldHaveSize import nebulosa.pixinsight.script.PixInsightScriptRunner import nebulosa.pixinsight.star.detection.PixInsightStarDetector import nebulosa.test.AbstractFitsAndXisfTest +import nebulosa.test.NonGitHubOnlyCondition import java.nio.file.Path +@EnabledIf(NonGitHubOnlyCondition::class) class PixInsightStarDetectorTest : AbstractFitsAndXisfTest() { init {