From 9ba998a4a3a86e7a18cf2bc4931b6c93c1131a8e Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 5 Jun 2024 21:34:22 -0300 Subject: [PATCH] [api][desktop]: Support PixInsight Live Stacking --- .../api/livestacking/LiveStackerType.kt | 1 + .../api/livestacking/LiveStackingRequest.kt | 18 +++++ desktop/src/app/camera/camera.component.html | 8 ++- desktop/src/app/camera/camera.component.ts | 13 ++-- .../src/app/settings/settings.component.ts | 2 + desktop/src/shared/pipes/dropdown-options.ts | 2 +- desktop/src/shared/types/camera.types.ts | 3 +- .../livestacking/PixInsightLiveStacker.kt | 68 ++++++++++++------- .../script/AbstractPixInsightScript.kt | 20 +++++- .../src/main/resources/pixinsight/Align.js | 2 +- 10 files changed, 102 insertions(+), 35 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt index 5e1d6426e..d5f5f6c09 100644 --- a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt @@ -2,4 +2,5 @@ package nebulosa.api.livestacking enum class LiveStackerType { SIRIL, + PIXINSIGHT, } diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt index 9f26e144b..629772d60 100644 --- a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt @@ -3,6 +3,11 @@ package nebulosa.api.livestacking import com.fasterxml.jackson.databind.annotation.JsonDeserialize import nebulosa.api.beans.converters.angle.DegreesDeserializer import nebulosa.livestacking.LiveStacker +import nebulosa.pixinsight.livestacking.PixInsightLiveStacker +import nebulosa.pixinsight.script.PixInsightIsRunning +import nebulosa.pixinsight.script.PixInsightScript +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.pixinsight.script.PixInsightStartup import nebulosa.siril.livestacking.SirilLiveStacker import org.jetbrains.annotations.NotNull import java.nio.file.Files @@ -15,8 +20,10 @@ data class LiveStackingRequest( @JvmField @field:NotNull val executablePath: Path? = null, @JvmField val dark: Path? = null, @JvmField val flat: Path? = null, + @JvmField val bias: Path? = null, @JvmField @field:JsonDeserialize(using = DegreesDeserializer::class) val rotate: Double = 0.0, @JvmField val use32Bits: Boolean = false, + @JvmField val slot: Int = PixInsightScript.DEFAULT_SLOT, ) : Supplier { override fun get(): LiveStacker { @@ -24,6 +31,17 @@ data class LiveStackingRequest( return when (type) { LiveStackerType.SIRIL -> SirilLiveStacker(executablePath!!, workingDirectory, dark, flat, rotate, use32Bits) + LiveStackerType.PIXINSIGHT -> { + val runner = PixInsightScriptRunner(executablePath!!) + + if (!PixInsightIsRunning(slot).use { it.runSync(runner) }) { + if (!PixInsightStartup(slot).use { it.runSync(runner) }) { + throw IllegalStateException("unable to start PixInsight") + } + } + + PixInsightLiveStacker(runner, workingDirectory, dark, flat, bias, use32Bits, slot) + } } } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 793d9b3b2..7a5d3b84f 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -255,7 +255,7 @@
- @@ -263,7 +263,7 @@
32-bits (slower) -
@@ -282,5 +282,9 @@
+
+ +
\ No newline at end of file diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 4dd5aa290..809b09a6f 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -543,10 +543,15 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { } async startCapture() { - await this.openCameraImage() - await this.api.cameraSnoop(this.camera, this.equipment) - await this.api.cameraStartCapture(this.camera, this.makeCameraStartCapture()) - this.preference.equipmentForDevice(this.camera).set(this.equipment) + try { + this.running = true + await this.openCameraImage() + await this.api.cameraSnoop(this.camera, this.equipment) + await this.api.cameraStartCapture(this.camera, this.makeCameraStartCapture()) + this.preference.equipmentForDevice(this.camera).set(this.equipment) + } catch { + this.running = false + } } abortCapture() { diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index e999409a5..25ffcd1c6 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -65,6 +65,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.starDetectors.set('PIXINSIGHT', preference.starDetectionRequest('PIXINSIGHT').get()) this.liveStackers.set('SIRIL', preference.liveStackingRequest('SIRIL').get()) + this.liveStackers.set('PIXINSIGHT', preference.liveStackingRequest('PIXINSIGHT').get()) } async ngAfterViewInit() { } @@ -134,5 +135,6 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.preference.starDetectionRequest('PIXINSIGHT').set(this.starDetectors.get('PIXINSIGHT')) this.preference.liveStackingRequest('SIRIL').set(this.liveStackers.get('SIRIL')) + this.preference.liveStackingRequest('PIXINSIGHT').set(this.liveStackers.get('PIXINSIGHT')) } } \ No newline at end of file diff --git a/desktop/src/shared/pipes/dropdown-options.ts b/desktop/src/shared/pipes/dropdown-options.ts index f0e819e55..fc5431fee 100644 --- a/desktop/src/shared/pipes/dropdown-options.ts +++ b/desktop/src/shared/pipes/dropdown-options.ts @@ -21,7 +21,7 @@ export class DropdownOptionsPipe implements PipeTransform { case 'PLATE_SOLVER': return ['ASTAP', 'ASTROMETRY_NET_ONLINE'] case 'AUTO_FOCUS_FITTING_MODE': return ['TRENDLINES', 'PARABOLIC', 'TREND_PARABOLIC', 'HYPERBOLIC', 'TREND_HYPERBOLIC'] case 'AUTO_FOCUS_BACKLASH_COMPENSATION_MODE': return ['NONE', 'ABSOLUTE', 'OVERSHOOT'] - case 'LIVE_STACKER': return ['SIRIL'] + case 'LIVE_STACKER': return ['SIRIL', 'PIXINSIGHT'] case 'SCNR_PROTECTION_METHOD': return ['MAXIMUM_MASK', 'ADDITIVE_MASK', 'AVERAGE_NEUTRAL', 'MAXIMUM_NEUTRAL', 'MINIMUM_NEUTRAL'] case 'IMAGE_FORMAT': return ['FITS', 'XISF', 'PNG', 'JPG'] case 'IMAGE_BITPIX': return ['BYTE', 'SHORT', 'INTEGER', 'FLOAT', 'DOUBLE'] diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index b7526cb8f..01e2010c7 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -13,7 +13,7 @@ export type AutoSubFolderMode = 'OFF' | 'NOON' | 'MIDNIGHT' export type ExposureMode = 'SINGLE' | 'FIXED' | 'LOOP' -export type LiveStackerType = 'SIRIL' +export type LiveStackerType = 'SIRIL' | 'PIXINSIGHT' export enum ExposureTimeUnit { MINUTE = 'm', @@ -293,6 +293,7 @@ export interface LiveStackingRequest { executablePath: string, dark?: string, flat?: string, + bias?: string, rotate: number, use32Bits: boolean, } 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 f1bed390f..b6529f84a 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt @@ -1,11 +1,13 @@ package nebulosa.pixinsight.livestacking import nebulosa.livestacking.LiveStacker +import nebulosa.log.loggerFor import nebulosa.pixinsight.script.* import java.nio.file.Path import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.copyTo +import kotlin.io.path.deleteIfExists import kotlin.io.path.moveTo -import kotlin.io.path.name data class PixInsightLiveStacker( private val runner: PixInsightScriptRunner, @@ -27,8 +29,11 @@ data class PixInsightLiveStacker( get() = stacking.get() @Volatile private var stackCount = 0 - @Volatile private var referencePath: Path? = null - @Volatile private var stackedPath: Path? = null + + private val referencePath = Path.of("$workingDirectory", "reference.fits") + private val calibratedPath = Path.of("$workingDirectory", "calibrated.fits") + private val alignedPath = Path.of("$workingDirectory", "aligned.fits") + private val stackedPath = Path.of("$workingDirectory", "stacked.fits") @Synchronized override fun start() { @@ -41,9 +46,10 @@ data class PixInsightLiveStacker( } catch (e: Throwable) { throw IllegalStateException("unable to start PixInsight") } - - running.set(true) } + + stackCount = 0 + running.set(true) } } @@ -51,15 +57,15 @@ data class PixInsightLiveStacker( override fun add(path: Path): Path? { var targetPath = path - if (running.get()) { + return 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) + PixInsightCalibrate(slot, targetPath, dark, flat, if (dark == null) bias else null).use { s -> + val outputPath = s.runSync(runner).outputImage ?: return@use null + LOG.info("live stacking calibrated. count={}, image={}", stackCount, outputPath) + outputPath.moveTo(calibratedPath, true) } } @@ -71,10 +77,10 @@ data class PixInsightLiveStacker( 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) + val alignedPath = PixInsightAlign(slot, referencePath, targetPath).use { s -> + val outputPath = s.runSync(runner).outputImage ?: return@use null + LOG.info("live stacking aligned. count={}, image={}", stackCount, alignedPath) + outputPath.moveTo(alignedPath, true) } if (alignedPath != null) { @@ -82,26 +88,42 @@ data class PixInsightLiveStacker( } // Stack. + val expressionRK = "({{0}} * $stackCount + {{1}}) / ${stackCount + 1}" + PixInsightPixelMath(slot, listOf(stackedPath, targetPath), stackedPath, expressionRK).use { s -> + s.runSync(runner).stackedImage?.also { + LOG.info("live stacking finished. count={}, image={}", stackCount, it) + stackCount++ + } + } } else { - referencePath = targetPath + targetPath.copyTo(referencePath, true) + targetPath.copyTo(stackedPath, true) + stackCount = 1 } - stackedPath = targetPath - stackCount++ - stacking.set(false) - } - return stackedPath + stackedPath + } else { + path + } } @Synchronized override fun stop() { running.set(false) stackCount = 0 - referencePath = null - stackedPath = null } - override fun close() = Unit + override fun close() { + referencePath.deleteIfExists() + calibratedPath.deleteIfExists() + alignedPath.deleteIfExists() + // stackedPath.deleteIfExists() + } + + companion object { + + @JvmStatic val LOG = loggerFor() + } } 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 1519e89ff..1a530b94a 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -21,6 +21,8 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen protected abstract fun processOnComplete(exitCode: Int): T? + protected open fun waitOnComplete() = Unit + final override fun run(runner: PixInsightScriptRunner) = runner.run(this) final override fun startCommandLine(commandLine: CommandLine) { @@ -28,6 +30,8 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen try { LOG.info("PixInsight script finished. done={}, exitCode={}", isDone, exitCode, exception) + waitOnComplete() + if (isDone) return@whenComplete else if (exception != null) completeExceptionally(exception) else complete(processOnComplete(exitCode)) @@ -57,11 +61,21 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen } @JvmStatic - internal fun execute(slot: Int, scriptPath: Path, data: Any): String { + internal fun execute(slot: Int, scriptPath: Path, data: Any?): String { return buildString { if (slot > 0) append("$slot:") - append("\"$scriptPath,") - append(Hex.encodeHexString(OBJECT_MAPPER.writeValueAsBytes(data))) + append("\"$scriptPath") + + if (data != null) { + append(',') + + when (data) { + is Path, is CharSequence -> append("'$data'") + is Number -> append("$data") + else -> append(Hex.encodeHexString(OBJECT_MAPPER.writeValueAsBytes(data))) + } + } + append('"') } } diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js index 0d28d88de..ffd042246 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js @@ -79,7 +79,7 @@ function alignment() { P.outputDirectory = outputDirectory P.outputExtension = ".fits" P.outputPrefix = "" - P.outputPostfix = "_sa" + P.outputPostfix = "_a" P.maskPostfix = "_m" P.distortionMapPostfix = "_dm" P.outputSampleFormat = StarAlignment.prototype.SameAsTarget