From 23569ff24f8573925fea44303229fb7b4673c67f Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 11 Jul 2024 16:14:10 -0300 Subject: [PATCH 1/5] [api]: Implement PixInsight Stacker --- .../calibration/CalibrationFrameService.kt | 16 +--- .../cameras/CameraCaptureNamingFormatter.kt | 2 +- .../nebulosa/api/cameras/CameraCaptureTask.kt | 3 +- .../nebulosa/indi/device/camera/FrameType.kt | 17 +++- nebulosa-pixinsight/build.gradle.kts | 2 + .../livestacker/PixInsightLiveStacker.kt | 48 ++++------- .../script/AbstractPixInsightScript.kt | 2 +- .../pixinsight/script/PixInsightCalibrate.kt | 8 +- .../script/PixInsightLRGBCombination.kt | 78 ++++++++++++++++++ .../stacker/PixInsightAutoStacker.kt | 50 ++++++++++++ .../pixinsight/stacker/PixInsightStacker.kt | 34 ++++++++ .../src/main/resources/pixinsight/Align.js | 2 +- .../main/resources/pixinsight/Calibrate.js | 2 +- .../resources/pixinsight/LRGBCombination.js | 80 +++++++++++++++++++ .../test/kotlin/PixInsightAutoStackerTest.kt | 42 ++++++++++ .../src/test/kotlin/PixInsightScriptTest.kt | 44 +++++++--- nebulosa-stacker/build.gradle.kts | 16 ++++ .../kotlin/nebulosa/stacker/AutoStacker.kt | 8 ++ .../main/kotlin/nebulosa/stacker/Stacker.kt | 15 ++++ settings.gradle.kts | 1 + 20 files changed, 405 insertions(+), 65 deletions(-) create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLRGBCombination.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/LRGBCombination.js create mode 100644 nebulosa-pixinsight/src/test/kotlin/PixInsightAutoStackerTest.kt create mode 100644 nebulosa-stacker/build.gradle.kts create mode 100644 nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt create mode 100644 nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt index 4dc82f92c..5939b03d3 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt @@ -6,8 +6,8 @@ import nebulosa.image.algorithms.transformation.correction.BiasSubtraction import nebulosa.image.algorithms.transformation.correction.DarkSubtraction import nebulosa.image.algorithms.transformation.correction.FlatCorrection import nebulosa.image.format.ImageHdu -import nebulosa.image.format.ReadableHeader import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.camera.FrameType.Companion.frameType import nebulosa.log.loggerFor import nebulosa.xisf.isXisf import nebulosa.xisf.xisf @@ -100,7 +100,7 @@ class CalibrationFrameService( } fun upload(name: String, path: Path): List { - val files = if (path.isRegularFile() && path.isFits) listOf(path) + val files = if (path.isRegularFile()) listOf(path) else if (path.isDirectory()) path.listDirectoryEntries("*.{fits,fit,xisf}").filter { it.isRegularFile() } else return emptyList() @@ -220,17 +220,5 @@ class CalibrationFrameService( companion object { @JvmStatic private val LOG = loggerFor() - - @JvmStatic val ReadableHeader.frameType - get() = frame?.let { - if (it.contains("LIGHT", true)) FrameType.LIGHT - else if (it.contains("DARK", true)) FrameType.DARK - else if (it.contains("FLAT", true)) FrameType.FLAT - else if (it.contains("BIAS", true)) FrameType.BIAS - else null - } - - inline val Path.isFits - get() = "$this".let { it.endsWith(".fits") || it.endsWith(".fit") } } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureNamingFormatter.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureNamingFormatter.kt index 6b8a8a0a7..ab590b6f0 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureNamingFormatter.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureNamingFormatter.kt @@ -1,11 +1,11 @@ package nebulosa.api.cameras -import nebulosa.api.calibration.CalibrationFrameService.Companion.frameType import nebulosa.common.concurrency.atomic.Incrementer import nebulosa.fits.* import nebulosa.image.format.ReadableHeader import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.camera.FrameType.Companion.frameType import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.mount.Mount diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index bd8dad2db..3b6abfdc3 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -32,6 +32,7 @@ import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicBoolean import kotlin.io.path.copyTo import kotlin.io.path.exists +import kotlin.io.path.extension data class CameraCaptureTask( @JvmField val camera: Camera, @@ -334,7 +335,7 @@ data class CameraCaptureTask( sendEvent(CameraCaptureState.STACKING) liveStacker!!.add(path)?.let { - val stackedPath = Path.of("${path.parent}", "STACKED.fits") + val stackedPath = Path.of("${path.parent}", "STACKED.${it.extension}") it.copyTo(stackedPath, true) stackedPath } diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/FrameType.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/FrameType.kt index f6bb25e2b..33e481bc7 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/FrameType.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/FrameType.kt @@ -1,8 +1,23 @@ package nebulosa.indi.device.camera +import nebulosa.fits.frame +import nebulosa.image.format.ReadableHeader + enum class FrameType(@JvmField val description: String) { LIGHT("Light"), DARK("Dark"), FLAT("Flat"), - BIAS("Bias"), + BIAS("Bias"); + + companion object { + + @JvmStatic val ReadableHeader.frameType + get() = frame?.let { + if (it.contains("LIGHT", true)) LIGHT + else if (it.contains("DARK", true)) DARK + else if (it.contains("FLAT", true)) FLAT + else if (it.contains("BIAS", true)) BIAS + else null + } + } } diff --git a/nebulosa-pixinsight/build.gradle.kts b/nebulosa-pixinsight/build.gradle.kts index a82b0f0a5..73c3ed243 100644 --- a/nebulosa-pixinsight/build.gradle.kts +++ b/nebulosa-pixinsight/build.gradle.kts @@ -7,10 +7,12 @@ dependencies { api(project(":nebulosa-common")) api(project(":nebulosa-math")) api(project(":nebulosa-stardetector")) + api(project(":nebulosa-stacker")) api(project(":nebulosa-livestacker")) api(libs.bundles.jackson) api(libs.apache.codec) implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-image")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacker/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacker/PixInsightLiveStacker.kt index 3348915ff..0e44d456d 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacker/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacker/PixInsightLiveStacker.kt @@ -2,12 +2,15 @@ package nebulosa.pixinsight.livestacker import nebulosa.livestacker.LiveStacker import nebulosa.log.loggerFor -import nebulosa.pixinsight.script.* +import nebulosa.pixinsight.script.PixInsightIsRunning +import nebulosa.pixinsight.script.PixInsightScript +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.pixinsight.script.PixInsightStartup +import nebulosa.pixinsight.stacker.PixInsightStacker 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 data class PixInsightLiveStacker( private val runner: PixInsightScriptRunner, @@ -16,7 +19,7 @@ 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, + private val slot: Int = PixInsightScript.UNSPECIFIED_SLOT, ) : LiveStacker { private val running = AtomicBoolean() @@ -30,9 +33,10 @@ data class PixInsightLiveStacker( @Volatile private var stackCount = 0 + private val stacker = PixInsightStacker(runner, workingDirectory, slot) 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 calibratedPath = Path.of("$workingDirectory", "calibrated.xisf") + private val alignedPath = Path.of("$workingDirectory", "aligned.xisf") private val stackedPath = Path.of("$workingDirectory", "stacked.fits") @Synchronized @@ -60,41 +64,23 @@ data class PixInsightLiveStacker( return if (running.get()) { stacking.set(true) - // Calibrate. - val calibrated = if (dark == null && flat == null && bias == null) false else { - PixInsightCalibrate(slot, workingDirectory, targetPath, dark, flat, if (dark == null) bias else null).use { s -> - val outputPath = s.runSync(runner).outputImage ?: return@use false - LOG.info("live stacking calibrated. count={}, output={}", stackCount, outputPath) - outputPath.moveTo(calibratedPath, true) - true - } - } - - if (calibrated) { + if (stacker.calibrate(targetPath, calibratedPath, dark, flat, bias)) { + LOG.info("live stacking calibrated. count={}, output={}", stackCount, calibratedPath) targetPath = calibratedPath } // TODO: Debayer, Resample? if (stackCount > 0) { - // Align. - val aligned = PixInsightAlign(slot, workingDirectory, referencePath, targetPath).use { s -> - val outputPath = s.runSync(runner).outputImage ?: return@use false - LOG.info("live stacking aligned. count={}, output={}", stackCount, outputPath) - outputPath.moveTo(alignedPath, true) - true - } - - if (aligned) { + if (stacker.align(referencePath, targetPath, alignedPath)) { + LOG.info("live stacking aligned. count={}, output={}", stackCount, alignedPath) targetPath = alignedPath - // 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={}, output={}", stackCount++, it) - } + if (stacker.integrate(stackCount, stackedPath, targetPath, stackedPath)) { + LOG.info("live stacking finished. count={}, output={}", stackCount, stackedPath) } + + stackCount++ } } else { targetPath.copyTo(referencePath, true) 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 ad5554d39..5a2f54ff2 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -34,7 +34,7 @@ abstract class AbstractPixInsightScript : PixInsightScript, CommandLineLis if (isDone) return@whenComplete else if (exception != null) completeExceptionally(exception) - else complete(processOnComplete(exitCode).also { LOG.info("script processed. output={}", it) }) + else complete(processOnComplete(exitCode).also { LOG.info("{} script processed. output={}", this::class.simpleName, it) }) } finally { commandLine.unregisterCommandLineListener(this) } 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 53cd83351..c9c7a3f2d 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -12,9 +12,9 @@ data class PixInsightCalibrate( private val slot: Int, private val workingDirectory: Path, private val targetPath: Path, - private val dark: Path? = null, - private val flat: Path? = null, - private val bias: Path? = null, + private val darkPath: Path? = null, + private val flatPath: Path? = null, + private val biasPath: Path? = null, private val compress: Boolean = false, private val use32Bit: Boolean = false, ) : AbstractPixInsightScript() { @@ -50,7 +50,7 @@ data class PixInsightCalibrate( } override val arguments = - listOf("-x=${execute(slot, scriptPath, Input(targetPath, workingDirectory, statusPath, dark, flat, bias, compress, use32Bit))}") + listOf("-x=${execute(slot, scriptPath, Input(targetPath, workingDirectory, statusPath, darkPath, flatPath, biasPath, compress, use32Bit))}") override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLRGBCombination.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLRGBCombination.kt new file mode 100644 index 000000000..e8362387c --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLRGBCombination.kt @@ -0,0 +1,78 @@ +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 PixInsightLRGBCombination( + private val slot: Int, + private val outputPath: Path, + private val luminancePath: Path? = null, + private val redPath: Path? = null, + private val greenPath: Path? = null, + private val bluePath: Path? = null, +) : AbstractPixInsightScript() { + + @Suppress("ArrayInDataClass") + private data class Input( + @JvmField val outputPath: Path, + @JvmField val statusPath: Path, + @JvmField val luminancePath: Path?, + @JvmField val redPath: Path?, + @JvmField val greenPath: Path?, + @JvmField val bluePath: Path?, + @JvmField val channelWeights: DoubleArray, + ) + + data class Output( + @JvmField val success: Boolean = false, + @JvmField val errorMessage: String? = null, + @JvmField val outputImage: Path? = null, + ) { + + companion object { + + @JvmStatic val FAILED = Output() + } + } + + private val scriptPath = Files.createTempFile("pi-", ".js") + private val statusPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/LRGBCombination.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = + listOf("-x=${execute(slot, scriptPath, Input(outputPath, statusPath, luminancePath, redPath, greenPath, bluePath, DEFAULT_CHANNEL_WEIGHTS))}") + + override fun processOnComplete(exitCode: Int): Output { + if (exitCode == 0) { + repeat(30) { + 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() + } + + companion object { + + @JvmStatic private val DEFAULT_CHANNEL_WEIGHTS = doubleArrayOf(1.0, 1.0, 1.0, 1.0) + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt new file mode 100644 index 000000000..7619a92a1 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt @@ -0,0 +1,50 @@ +package nebulosa.pixinsight.stacker + +import nebulosa.pixinsight.script.PixInsightScript +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.stacker.AutoStacker +import java.nio.file.Path +import kotlin.io.path.copyTo +import kotlin.io.path.deleteIfExists + +data class PixInsightAutoStacker( + private val runner: PixInsightScriptRunner, + private val workingDirectory: Path, + private val darkPath: Path? = null, + private val flatPath: Path? = null, + private val biasPath: Path? = null, + private val slot: Int = PixInsightScript.UNSPECIFIED_SLOT, +) : AutoStacker { + + private val stacker = PixInsightStacker(runner, workingDirectory, slot) + + override fun stack(paths: Collection, outputPath: Path, referencePath: Path): Boolean { + if (paths.isEmpty()) return false + + val calibratedPath = Path.of("$workingDirectory", "calibrated.xisf") + val alignedPath = Path.of("$workingDirectory", "aligned.xisf") + + try { + paths.forEachIndexed { stackCount, path -> + var targetPath = path + + if (stacker.calibrate(targetPath, calibratedPath, darkPath, flatPath, biasPath)) { + targetPath = calibratedPath + } + + if (stackCount > 0) { + if (stacker.align(referencePath, targetPath, alignedPath)) { + stacker.integrate(stackCount, outputPath, alignedPath, outputPath) + } + } else { + targetPath.copyTo(outputPath, true) + } + } + } finally { + calibratedPath.deleteIfExists() + alignedPath.deleteIfExists() + } + + return true + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt new file mode 100644 index 000000000..b66a33733 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt @@ -0,0 +1,34 @@ +package nebulosa.pixinsight.stacker + +import nebulosa.pixinsight.script.* +import nebulosa.stacker.Stacker +import java.nio.file.Path +import kotlin.io.path.moveTo + +data class PixInsightStacker( + private val runner: PixInsightScriptRunner, + private val workingDirectory: Path, + private val slot: Int = PixInsightScript.UNSPECIFIED_SLOT, +) : Stacker { + + override fun calibrate( + targetPath: Path, outputPath: Path, + darkPath: Path?, flatPath: Path?, biasPath: Path?, + ) = if (darkPath != null || flatPath != null || biasPath != null) { + PixInsightCalibrate(slot, workingDirectory, targetPath, darkPath, flatPath, if (darkPath == null) biasPath else null) + .use { it.runSync(runner).outputImage?.moveTo(outputPath, true) != null } + } else { + false + } + + override fun align(referencePath: Path, targetPath: Path, outputPath: Path): Boolean { + return PixInsightAlign(slot, workingDirectory, referencePath, targetPath) + .use { it.runSync(runner).outputImage?.moveTo(outputPath, true) != null } + } + + override fun integrate(stackCount: Int, stackedPath: Path, targetPath: Path, outputPath: Path): Boolean { + val expressionRK = "({{0}} * $stackCount + {{1}}) / ${stackCount + 1}" + return PixInsightPixelMath(slot, listOf(stackedPath, targetPath), outputPath, expressionRK) + .use { it.runSync(runner).stackedImage != null } + } +} diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js index f8f93f38c..d03400960 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js @@ -103,7 +103,7 @@ function alignment() { P.pixelInterpolation = StarAlignment.prototype.Auto P.clampingThreshold = 0.30 P.outputDirectory = outputDirectory - P.outputExtension = ".fits" + P.outputExtension = ".xisf" P.outputPrefix = "" P.outputPostfix = "_a" P.maskPostfix = "_m" diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js index 814586b86..7561a2ba0 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -89,7 +89,7 @@ function calibrate() { P.psfGrowth = 1.00 P.maxStars = 24576 P.outputDirectory = outputDirectory - P.outputExtension = ".fits" + P.outputExtension = ".xisf" P.outputPrefix = "" P.outputPostfix = "_c" P.outputSampleFormat = use32Bit ? ImageCalibration.prototype.f32 : ImageCalibration.prototype.i16 diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/LRGBCombination.js b/nebulosa-pixinsight/src/main/resources/pixinsight/LRGBCombination.js new file mode 100644 index 000000000..bd44e4632 --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/LRGBCombination.js @@ -0,0 +1,80 @@ +function decodeParams(hex) { + const buffer = new Uint8Array(hex.length / 4) + + for (let i = 0; i < hex.length; i += 4) { + buffer[i / 4] = parseInt(hex.substr(i, 4), 16) + } + + return JSON.parse(String.fromCharCode.apply(null, buffer)) +} + +function lrgbCombination() { + const data = { + success: true, + errorMessage: null, + outputImage: null, + } + + try { + const input = decodeParams(jsArguments[0]) + + const outputPath = input.outputPath + const statusPath = input.statusPath + const channelWeights = input.channelWeights + const luminancePath = input.luminancePath + const redPath = input.redPath + const greenPath = input.greenPath + const bluePath = input.bluePath + + console.writeln("outputPath=" + outputPath) + console.writeln("statusPath=" + statusPath) + console.writeln("channelWeights=" + channelWeights) + console.writeln("luminancePath=" + luminancePath) + console.writeln("redPath=" + redPath) + console.writeln("greenPath=" + greenPath) + console.writeln("bluePath=" + bluePath) + + const luminanceWindow = luminancePath ? ImageWindow.open(luminancePath)[0] : undefined + const redWindow = redPath ? ImageWindow.open(redPath)[0] : undefined + const greenWindow = greenPath ? ImageWindow.open(greenPath)[0] : undefined + const blueWindow = bluePath ? ImageWindow.open(bluePath)[0] : undefined + + var P = new LRGBCombination + P.channels = [ // enabled, id, k + [!!luminancePath, luminanceWindow? luminanceWindow.mainView.id : "", channelWeights[0]], + [!!redPath, redWindow ? redWindow.mainView.id : "", channelWeights[1]], + [!!greenPath, greenWindow ? greenWindow.mainView.id : "", channelWeights[2]], + [!!bluePath, blueWindow ? blueWindow.mainView.id : "", channelWeights[3]] + ] + P.mL = 0.500 + P.mc = 0.500 + P.clipHighlights = true + P.noiseReduction = false + P.layersRemoved = 4 + P.layersProtected = 2 + P.inheritAstrometricSolution = true + + P.executeGlobal() + + const window = ImageWindow.windows[ImageWindow.windows.length - 1] + window.saveAs(outputPath, false, false, false, false) + window.forceClose() + + if (luminanceWindow) luminanceWindow.forceClose() + if (redWindow) redWindow.forceClose() + if (greenWindow) greenWindow.forceClose() + if (blueWindow) blueWindow.forceClose() + + data.outputImage = outputPath + + console.writeln("LRGB combination finished") + } catch (e) { + data.success = false + data.errorMessage = e.message + console.writeln(data.errorMessage) + } finally { + File.writeTextFile(statusPath, "@" + JSON.stringify(data) + "#") + } +} + +lrgbCombination() diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightAutoStackerTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightAutoStackerTest.kt new file mode 100644 index 000000000..e4c6a0feb --- /dev/null +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightAutoStackerTest.kt @@ -0,0 +1,42 @@ +import PixInsightScriptTest.Companion.openAsImage +import io.kotest.core.annotation.EnabledIf +import io.kotest.engine.spec.tempdir +import io.kotest.engine.spec.tempfile +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe +import nebulosa.image.algorithms.transformation.AutoScreenTransformFunction +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.pixinsight.stacker.PixInsightAutoStacker +import nebulosa.test.AbstractFitsAndXisfTest +import nebulosa.test.NonGitHubOnlyCondition +import java.nio.file.Path + +@EnabledIf(NonGitHubOnlyCondition::class) +class PixInsightAutoStackerTest : AbstractFitsAndXisfTest() { + + init { + val runner = PixInsightScriptRunner(Path.of("PixInsight")) + val workingDirectory = tempdir("pi-").toPath() + + "stack" { + val files = listOf(PI_01_LIGHT, PI_02_LIGHT, PI_03_LIGHT, PI_04_LIGHT, PI_05_LIGHT, PI_06_LIGHT, PI_07_LIGHT, PI_08_LIGHT) + val outputPath = tempfile("pi-", ".fits").toPath() + + val stacker = PixInsightAutoStacker(runner, workingDirectory) + stacker.stack(files, outputPath).shouldBeTrue() + + outputPath.openAsImage().transform(AutoScreenTransformFunction) + .save("pi-auto-stacked").second shouldBe "a107143dff3d43c4b56c872da869f89b" + } + "!calibrated stack" { + val files = listOf(PI_01_LIGHT, PI_02_LIGHT, PI_03_LIGHT, PI_04_LIGHT, PI_05_LIGHT, PI_06_LIGHT, PI_07_LIGHT, PI_08_LIGHT) + val outputPath = tempfile("pi-", ".fits").toPath() + + val stacker = PixInsightAutoStacker(runner, workingDirectory, PI_DARK, PI_FLAT, PI_BIAS) + stacker.stack(files, outputPath).shouldBeTrue() + + outputPath.openAsImage().transform(AutoScreenTransformFunction) + .save("pi-calibrated-auto-stacked").second shouldBe "" + } + } +} diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index 2bc11d4d2..ab35696bc 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -4,12 +4,16 @@ import io.kotest.engine.spec.tempfile import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.paths.shouldExist import io.kotest.matchers.shouldBe +import nebulosa.fits.fits +import nebulosa.fits.isFits +import nebulosa.image.Image +import nebulosa.image.algorithms.transformation.AutoScreenTransformFunction import nebulosa.pixinsight.script.* import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.test.NonGitHubOnlyCondition -import java.nio.file.Files +import nebulosa.xisf.isXisf +import nebulosa.xisf.xisf import java.nio.file.Path @EnabledIf(NonGitHubOnlyCondition::class) @@ -19,21 +23,23 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { val runner = PixInsightScriptRunner(Path.of("PixInsight")) val workingDirectory = tempdir("pi-").toPath() - "startup" { + "!startup" { PixInsightStartup(PixInsightScript.DEFAULT_SLOT) .use { it.runSync(runner).shouldBeTrue() } } - "is running" { + "!is running" { PixInsightIsRunning(PixInsightScript.DEFAULT_SLOT) .use { it.runSync(runner).shouldBeTrue() } } "calibrate" { PixInsightCalibrate(PixInsightScript.UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().shouldExist() } + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-calibrate").second shouldBe "731562ee12f45bf7c1095f4773f70e71" } "align" { PixInsightAlign(PixInsightScript.UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_02_LIGHT) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().shouldExist() } + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-align").second shouldBe "483ebaf15afa5957fe099f3ee2beff78" } "detect stars" { PixInsightDetectStars(PixInsightScript.UNSPECIFIED_SLOT, PI_FOCUS_0) @@ -52,14 +58,32 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { .average() shouldBe (18.35 plusOrMinus 1e-2) } "pixel math" { - val outputPath = Files.createTempFile("pi-stacked-", ".fits") + val outputPath = tempfile("pi-stacked-", ".fits").toPath() PixInsightPixelMath(PixInsightScript.UNSPECIFIED_SLOT, listOf(PI_01_LIGHT, PI_02_LIGHT), outputPath, "{{0}} + {{1}}") - .use { it.runSync(runner).also(::println).stackedImage.shouldNotBeNull().shouldExist() } + .use { it.runSync(runner).also(::println).stackedImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-pixelmath").second shouldBe "cafc8138e2ce17614dcfa10edf410b07" } "abe" { - val outputPath = tempfile("pi-", ".fits").toPath() + val outputPath = tempfile("pi-abe-", ".fits").toPath() PixInsightAutomaticBackgroundExtractor(PixInsightScript.UNSPECIFIED_SLOT, PI_01_LIGHT, outputPath) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull() } + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-abe").second shouldBe "bf62207dc17190009ba215da7c011297" + } + "lrgb combination" { + val outputPath = tempfile("pi-lrgb-", ".fits").toPath() + PixInsightLRGBCombination(PixInsightScript.UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-lrgb").second shouldBe "99db35d78f7b360e7592217f4179b189" + } + } + + companion object { + + @JvmStatic + internal fun Path.openAsImage(): Image { + return if (isFits()) fits().use(Image::open) + else if (isXisf()) xisf().use(Image::open) + else throw IllegalArgumentException("the path at $this is not an image") } } } diff --git a/nebulosa-stacker/build.gradle.kts b/nebulosa-stacker/build.gradle.kts new file mode 100644 index 000000000..4d1b2976b --- /dev/null +++ b/nebulosa-stacker/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("jvm") + id("maven-publish") +} + +dependencies { + api(libs.logback) +} + +publishing { + publications { + create("pluginMaven") { + from(components["java"]) + } + } +} diff --git a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt new file mode 100644 index 000000000..811c4c867 --- /dev/null +++ b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt @@ -0,0 +1,8 @@ +package nebulosa.stacker + +import java.nio.file.Path + +interface AutoStacker { + + fun stack(paths: Collection, outputPath: Path, referencePath: Path = paths.first()): Boolean +} diff --git a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt new file mode 100644 index 000000000..bbf15d88e --- /dev/null +++ b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt @@ -0,0 +1,15 @@ +package nebulosa.stacker + +import java.nio.file.Path + +interface Stacker { + + fun calibrate( + targetPath: Path, outputPath: Path, + darkPath: Path? = null, flatPath: Path? = null, biasPath: Path? = null, + ): Boolean + + fun align(referencePath: Path, targetPath: Path, outputPath: Path): Boolean + + fun integrate(stackCount: Int, stackedPath: Path, targetPath: Path, outputPath: Path): Boolean +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3e9c051c9..95058fe59 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -88,6 +88,7 @@ include(":nebulosa-skycatalog-hyg") include(":nebulosa-skycatalog-sao") include(":nebulosa-skycatalog-stellarium") include(":nebulosa-stardetector") +include(":nebulosa-stacker") include(":nebulosa-stellarium-protocol") include(":nebulosa-test") include(":nebulosa-time") From 15d1db5880ff0e8cf10f1915d8441018a928a7ea Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 12 Jul 2024 17:44:20 -0300 Subject: [PATCH 2/5] [api][desktop]: Implement PixInsight Stacker --- .../alignment/polar/darv/DARVStartRequest.kt | 6 +- .../kotlin/nebulosa/api/atlas/BodyPosition.kt | 26 +++--- .../nebulosa/api/atlas/CloseApproach.kt | 10 +-- .../kotlin/nebulosa/api/atlas/Location.kt | 2 +- .../kotlin/nebulosa/api/atlas/MinorPlanet.kt | 27 +++--- .../nebulosa/api/atlas/SatelliteEntity.kt | 6 +- .../api/atlas/SkyObjectInsideCoordinate.kt | 6 +- .../kotlin/nebulosa/api/atlas/Twilight.kt | 14 ++-- .../api/calibration/CalibrationFrameEntity.kt | 24 +++--- .../api/calibration/CalibrationFrameGroup.kt | 8 +- .../api/calibration/CalibrationGroupKey.kt | 14 ++-- .../nebulosa/api/cameras/CameraCaptureTask.kt | 15 ++-- .../api/connection/ConnectionStatus.kt | 9 +- .../kotlin/nebulosa/api/guiding/GuiderInfo.kt | 8 +- .../api/guiding/GuiderMessageEvent.kt | 2 +- .../nebulosa/api/guiding/HistoryStep.kt | 14 ++-- .../kotlin/nebulosa/api/guiding/SettleInfo.kt | 6 +- .../api/image/CoordinateInterpolation.kt | 12 ++- .../nebulosa/api/image/ImageAnnotation.kt | 8 +- .../nebulosa/api/image/ImageHeaderItem.kt | 2 +- .../kotlin/nebulosa/api/image/ImageInfo.kt | 24 +++--- .../kotlin/nebulosa/api/image/ImageSolved.kt | 16 ++-- .../kotlin/nebulosa/api/image/SaveImage.kt | 10 +-- .../nebulosa/api/indi/INDIMessageEvent.kt | 4 +- .../nebulosa/api/indi/INDISendProperty.kt | 6 +- .../nebulosa/api/indi/INDISendPropertyItem.kt | 4 +- .../api/livestacker/LiveStackingRequest.kt | 25 ++---- .../nebulosa/api/mounts/ComputedLocation.kt | 22 ++--- .../api/preference/PreferenceEntity.kt | 4 +- .../api/preference/PreferenceRequestBody.kt | 3 - .../nebulosa/api/stacker/StackerController.kt | 21 +++++ .../nebulosa/api/stacker/StackerGroupType.kt | 10 +++ .../nebulosa/api/stacker/StackerService.kt | 78 ++++++++++++++++++ .../nebulosa/api/stacker/StackerType.kt | 5 ++ .../nebulosa/api/stacker/StackingRequest.kt | 36 ++++++++ .../nebulosa/api/stacker/StackingTarget.kt | 11 +++ .../api/stardetector/StarDetectionRequest.kt | 13 +-- desktop/.editorconfig | 1 + desktop/src/app/about/about.component.html | 7 +- desktop/src/app/app-routing.module.ts | 5 ++ desktop/src/app/app.module.ts | 2 + desktop/src/app/camera/camera.component.html | 6 +- desktop/src/app/camera/camera.component.ts | 6 +- .../src/app/settings/settings.component.html | 17 ++-- .../src/app/settings/settings.component.ts | 27 +----- .../src/app/stacker/stacker.component.html | 0 desktop/src/app/stacker/stacker.component.ts | 7 ++ desktop/src/assets/icons/photo-filter.png | Bin 0 -> 1657 bytes .../src/shared/pipes/dropdown-options.pipe.ts | 9 +- desktop/src/shared/pipes/enum.pipe.ts | 62 ++++++++------ desktop/src/shared/services/api.service.ts | 23 +----- .../shared/services/browser-window.service.ts | 7 +- desktop/src/shared/types/camera.types.ts | 6 +- desktop/src/shared/types/settings.types.ts | 7 ++ desktop/src/shared/types/stacker.types.ts | 23 ++++++ .../livestacker/PixInsightLiveStacker.kt | 12 ++- .../pixinsight/script/PixInsightAlign.kt | 6 +- .../PixInsightAutomaticBackgroundExtractor.kt | 6 +- .../pixinsight/script/PixInsightCalibrate.kt | 6 +- .../script/PixInsightDetectStars.kt | 6 +- .../pixinsight/script/PixInsightHelper.kt | 15 ++++ .../script/PixInsightLRGBCombination.kt | 11 ++- .../script/PixInsightLuminanceCombination.kt | 68 +++++++++++++++ .../pixinsight/script/PixInsightOutput.kt | 8 ++ .../pixinsight/script/PixInsightPixelMath.kt | 8 +- .../stacker/PixInsightAutoStacker.kt | 28 ++++++- .../pixinsight/stacker/PixInsightStacker.kt | 19 ++++- .../src/main/resources/pixinsight/ABE.js | 1 + .../src/main/resources/pixinsight/Align.js | 1 + .../main/resources/pixinsight/Calibrate.js | 1 + .../resources/pixinsight/LRGBCombination.js | 5 +- .../pixinsight/LuminanceCombination.js | 71 ++++++++++++++++ .../main/resources/pixinsight/PixelMath.js | 7 +- .../src/test/kotlin/PixInsightScriptTest.kt | 60 ++++++++++++-- .../src/test/kotlin/PixInsightStackerTest.kt | 58 +++++++++++++ .../kotlin/nebulosa/siril/command/StartLs.kt | 8 +- .../siril/livestacker/SirilLiveStacker.kt | 6 +- .../kotlin/nebulosa/stacker/AutoStacker.kt | 2 +- .../main/kotlin/nebulosa/stacker/Stacker.kt | 4 + 79 files changed, 827 insertions(+), 316 deletions(-) delete mode 100644 api/src/main/kotlin/nebulosa/api/preference/PreferenceRequestBody.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stacker/StackerController.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stacker/StackerGroupType.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stacker/StackerType.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stacker/StackingRequest.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stacker/StackingTarget.kt create mode 100644 desktop/src/app/stacker/stacker.component.html create mode 100644 desktop/src/app/stacker/stacker.component.ts create mode 100644 desktop/src/assets/icons/photo-filter.png create mode 100644 desktop/src/shared/types/stacker.types.ts create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightHelper.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLuminanceCombination.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightOutput.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/LuminanceCombination.js create mode 100644 nebulosa-pixinsight/src/test/kotlin/PixInsightStackerTest.kt diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt index 53b99114f..209792636 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt @@ -4,7 +4,7 @@ import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.guiding.GuideDirection data class DARVStartRequest( - val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY, - val direction: GuideDirection = GuideDirection.NORTH, - val reversed: Boolean = false, + @JvmField val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY, + @JvmField val direction: GuideDirection = GuideDirection.NORTH, + @JvmField val reversed: Boolean = false, ) diff --git a/api/src/main/kotlin/nebulosa/api/atlas/BodyPosition.kt b/api/src/main/kotlin/nebulosa/api/atlas/BodyPosition.kt index 64802dd6d..e455723ee 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/BodyPosition.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/BodyPosition.kt @@ -15,19 +15,19 @@ import nebulosa.nova.astrometry.Constellation import nebulosa.skycatalog.SkyObject data class BodyPosition( - @field:JsonSerialize(using = RightAscensionSerializer::class) val rightAscensionJ2000: Angle, - @field:JsonSerialize(using = DeclinationSerializer::class) val declinationJ2000: Angle, - @field:JsonSerialize(using = RightAscensionSerializer::class) val rightAscension: Angle, - @field:JsonSerialize(using = DeclinationSerializer::class) val declination: Angle, - @field:JsonSerialize(using = AzimuthSerializer::class) val azimuth: Angle, - @field:JsonSerialize(using = DeclinationSerializer::class) val altitude: Angle, - val magnitude: Double, - val constellation: Constellation, - val distance: Double, - val distanceUnit: String, - val illuminated: Double, - @field:JsonSerialize(using = DegreesSerializer::class) val elongation: Angle, - val leading: Boolean, // true = rises and sets BEFORE Sun. + @field:JsonSerialize(using = RightAscensionSerializer::class) @JvmField val rightAscensionJ2000: Angle, + @field:JsonSerialize(using = DeclinationSerializer::class) @JvmField val declinationJ2000: Angle, + @field:JsonSerialize(using = RightAscensionSerializer::class) @JvmField val rightAscension: Angle, + @field:JsonSerialize(using = DeclinationSerializer::class) @JvmField val declination: Angle, + @field:JsonSerialize(using = AzimuthSerializer::class) @JvmField val azimuth: Angle, + @field:JsonSerialize(using = DeclinationSerializer::class) @JvmField val altitude: Angle, + @JvmField val magnitude: Double, + @JvmField val constellation: Constellation, + @JvmField val distance: Double, + @JvmField val distanceUnit: String, + @JvmField val illuminated: Double, + @field:JsonSerialize(using = DegreesSerializer::class) @JvmField val elongation: Angle, + @JvmField val leading: Boolean, // true = rises and sets BEFORE Sun. ) { companion object { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/CloseApproach.kt b/api/src/main/kotlin/nebulosa/api/atlas/CloseApproach.kt index 06914286a..c011e7c78 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/CloseApproach.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/CloseApproach.kt @@ -8,11 +8,11 @@ import java.time.format.DateTimeFormatter import java.util.* data class CloseApproach( - val name: String = "", - val designation: String = "", - val dateTime: Long = 0, - val distance: Double = 0.0, - val absoluteMagnitude: Double = 0.0, + @JvmField val name: String = "", + @JvmField val designation: String = "", + @JvmField val dateTime: Long = 0, + @JvmField val distance: Double = 0.0, + @JvmField val absoluteMagnitude: Double = 0.0, ) { companion object { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/Location.kt b/api/src/main/kotlin/nebulosa/api/atlas/Location.kt index 7d92156e3..8bd3b0ca3 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/Location.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/Location.kt @@ -15,7 +15,7 @@ data class Location( @field:JsonSerialize(using = DegreesSerializer::class) @field:JsonDeserialize(using = DegreesDeserializer::class) override val latitude: Angle = 0.0, @field:JsonSerialize(using = DegreesSerializer::class) @field:JsonDeserialize(using = DegreesDeserializer::class) override val longitude: Angle = 0.0, @field:JsonSerialize(using = MetersSerializer::class) @field:JsonDeserialize(using = MetersDeserializer::class) override val elevation: Distance = 0.0, - val offsetInMinutes: Int = 0, + @JvmField val offsetInMinutes: Int = 0, ) : GeographicCoordinate, TimeZonedInSeconds { override val offsetInSeconds = offsetInMinutes * 60 diff --git a/api/src/main/kotlin/nebulosa/api/atlas/MinorPlanet.kt b/api/src/main/kotlin/nebulosa/api/atlas/MinorPlanet.kt index 423a774b0..5d13a502a 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/MinorPlanet.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/MinorPlanet.kt @@ -3,20 +3,21 @@ package nebulosa.api.atlas import nebulosa.sbd.SmallBody data class MinorPlanet( - val found: Boolean = false, - val name: String = "", - val spkId: Int = -1, - val kind: SmallBody.BodyKind? = null, - val pha: Boolean = false, val neo: Boolean = false, - val orbitType: String = "", - val parameters: List = emptyList(), - val searchItems: List = emptyList(), + @JvmField val found: Boolean = false, + @JvmField val name: String = "", + @JvmField val spkId: Int = -1, + @JvmField val kind: SmallBody.BodyKind? = null, + @JvmField val pha: Boolean = false, + @JvmField val neo: Boolean = false, + @JvmField val orbitType: String = "", + @JvmField val parameters: List = emptyList(), + @JvmField val searchItems: List = emptyList(), ) { data class OrbitalPhysicalParameter( - val name: String, - val description: String, - val value: String, + @JvmField val name: String, + @JvmField val description: String, + @JvmField val value: String, ) { constructor(param: SmallBody.OrbitElement) : this( @@ -31,8 +32,8 @@ data class MinorPlanet( } data class SearchItem( - val name: String, - val pdes: String, + @JvmField val name: String, + @JvmField val pdes: String, ) companion object { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt index bdaaf93bf..bed2ebab2 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt @@ -7,7 +7,7 @@ import nebulosa.api.database.BoxEntity @Entity data class SatelliteEntity( @Id(assignable = true) override var id: Long = 0L, - var name: String = "", - var tle: String = "", - var groups: MutableList = ArrayList(0), + @JvmField var name: String = "", + @JvmField var tle: String = "", + @JvmField var groups: MutableList = ArrayList(0), ) : BoxEntity diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectInsideCoordinate.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectInsideCoordinate.kt index 10b0857bb..ac2e89655 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectInsideCoordinate.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectInsideCoordinate.kt @@ -9,9 +9,9 @@ import kotlin.math.cos import kotlin.math.sin data class SkyObjectInsideCoordinate( - private val rightAscension: Angle, - private val declination: Angle, - private val radius: Angle, + @JvmField val rightAscension: Angle, + @JvmField val declination: Angle, + @JvmField val radius: Angle, ) : QueryFilter { private val sinDEC = declination.sin diff --git a/api/src/main/kotlin/nebulosa/api/atlas/Twilight.kt b/api/src/main/kotlin/nebulosa/api/atlas/Twilight.kt index 3414234d5..3ec8cab46 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/Twilight.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/Twilight.kt @@ -2,11 +2,11 @@ package nebulosa.api.atlas @Suppress("ArrayInDataClass") data class Twilight( - val civilDusk: DoubleArray, - val nauticalDusk: DoubleArray, - val astronomicalDusk: DoubleArray, - val night: DoubleArray, - val astronomicalDawn: DoubleArray, - val nauticalDawn: DoubleArray, - val civilDawn: DoubleArray, + @JvmField val civilDusk: DoubleArray, + @JvmField val nauticalDusk: DoubleArray, + @JvmField val astronomicalDusk: DoubleArray, + @JvmField val night: DoubleArray, + @JvmField val astronomicalDawn: DoubleArray, + @JvmField val nauticalDawn: DoubleArray, + @JvmField val civilDawn: DoubleArray, ) diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt index 631dc5b9e..1b15e657e 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt @@ -13,16 +13,16 @@ import java.nio.file.Path @Entity data class CalibrationFrameEntity( @Id override var id: Long = 0L, - @Index @Convert(converter = FrameTypePropertyConverter::class, dbType = Int::class) var type: FrameType = FrameType.LIGHT, - @Index var name: String = "", - var filter: String? = null, - var exposureTime: Long = 0L, - var temperature: Double = 0.0, - var width: Int = 0, - var height: Int = 0, - var binX: Int = 0, - var binY: Int = 0, - var gain: Double = 0.0, - @Convert(converter = PathPropertyConverter::class, dbType = String::class) var path: Path? = null, - var enabled: Boolean = true, + @JvmField @Index @Convert(converter = FrameTypePropertyConverter::class, dbType = Int::class) var type: FrameType = FrameType.LIGHT, + @JvmField @Index var name: String = "", + @JvmField var filter: String? = null, + @JvmField var exposureTime: Long = 0L, + @JvmField var temperature: Double = 0.0, + @JvmField var width: Int = 0, + @JvmField var height: Int = 0, + @JvmField var binX: Int = 0, + @JvmField var binY: Int = 0, + @JvmField var gain: Double = 0.0, + @JvmField @Convert(converter = PathPropertyConverter::class, dbType = String::class) var path: Path? = null, + @JvmField var enabled: Boolean = true, ) : BoxEntity diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt index 7e4171cda..df469a2cd 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt @@ -1,8 +1,8 @@ package nebulosa.api.calibration data class CalibrationFrameGroup( - val id: Int, - val name: String, - val key: CalibrationGroupKey, - val frames: List, + @JvmField val id: Int, + @JvmField val name: String, + @JvmField val key: CalibrationGroupKey, + @JvmField val frames: List, ) diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationGroupKey.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationGroupKey.kt index dd659e464..5f63971b4 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationGroupKey.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationGroupKey.kt @@ -4,11 +4,15 @@ import nebulosa.indi.device.camera.FrameType import kotlin.math.roundToInt data class CalibrationGroupKey( - val type: FrameType, val filter: String?, - val width: Int, val height: Int, - val binX: Int, val binY: Int, - val exposureTime: Long, - val temperature: Int, val gain: Double, + @JvmField val type: FrameType, + @JvmField val filter: String?, + @JvmField val width: Int, + @JvmField val height: Int, + @JvmField val binX: Int, + @JvmField val binY: Int, + @JvmField val exposureTime: Long, + @JvmField val temperature: Int, + @JvmField val gain: Double, ) { companion object { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index 3b6abfdc3..c57a709c7 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -101,7 +101,7 @@ data class CameraCaptureTask( private fun LiveStackingRequest.processCalibrationGroup(): LiveStackingRequest { return if (calibrationFrameProvider != null && enabled && - !request.calibrationGroup.isNullOrBlank() && (dark == null || flat == null || bias == null) + !request.calibrationGroup.isNullOrBlank() && (darkPath == null || flatPath == null || biasPath == null) ) { val calibrationGroup = request.calibrationGroup val temperature = camera.temperature @@ -120,24 +120,27 @@ data class CameraCaptureTask( calibrationGroup, temperature, binX, binY, width, height, exposureTime, gain, filter ) - val newDark = dark?.takeIf { it.exists() } ?: calibrationFrameProvider + val newDarkPath = darkPath?.takeIf { it.exists() } ?: calibrationFrameProvider .findBestDarkFrames(calibrationGroup, temperature, width, height, binX, binY, exposureTime, gain) .firstOrNull() ?.path - val newFlat = flat?.takeIf { it.exists() } ?: calibrationFrameProvider + val newFlatPath = flatPath?.takeIf { it.exists() } ?: calibrationFrameProvider .findBestFlatFrames(calibrationGroup, width, height, binX, binY, filter) .firstOrNull() ?.path - val newBias = if (newDark != null) null else bias?.takeIf { it.exists() } ?: calibrationFrameProvider + val newBiasPath = if (newDarkPath != null) null else biasPath?.takeIf { it.exists() } ?: calibrationFrameProvider .findBestBiasFrames(calibrationGroup, width, height, binX, binY) .firstOrNull() ?.path - LOG.info("live stacking will use calibration frames. group={}, dark={}, flat={}, bias={}", calibrationGroup, newDark, newFlat, newBias) + LOG.info( + "live stacking will use calibration frames. group={}, dark={}, flat={}, bias={}", + calibrationGroup, newDarkPath, newFlatPath, newBiasPath + ) - copy(dark = newDark, flat = newFlat, bias = newBias) + copy(darkPath = newDarkPath, flatPath = newFlatPath, biasPath = newBiasPath) } else { this } diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionStatus.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionStatus.kt index 2eba3f9ae..f511fda92 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionStatus.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionStatus.kt @@ -1,8 +1,9 @@ package nebulosa.api.connection data class ConnectionStatus( - val id: String, - val type: ConnectionType, - val host: String, val port: Int, - val ip: String? = null, + @JvmField val id: String, + @JvmField val type: ConnectionType, + @JvmField val host: String, + @JvmField val port: Int, + @JvmField val ip: String? = null, ) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuiderInfo.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuiderInfo.kt index 3ce34f5b7..ec1d38a4f 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuiderInfo.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuiderInfo.kt @@ -3,10 +3,10 @@ package nebulosa.api.guiding import nebulosa.guiding.GuideState data class GuiderInfo( - val connected: Boolean = false, - val state: GuideState = GuideState.STOPPED, - val settling: Boolean = false, - val pixelScale: Double = 1.0, + @JvmField val connected: Boolean = false, + @JvmField val state: GuideState = GuideState.STOPPED, + @JvmField val settling: Boolean = false, + @JvmField val pixelScale: Double = 1.0, ) { companion object { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt index 33ed3a7bc..8237710b5 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt @@ -2,4 +2,4 @@ package nebulosa.api.guiding import nebulosa.api.message.MessageEvent -data class GuiderMessageEvent(override val eventName: String, val data: Any? = null) : MessageEvent +data class GuiderMessageEvent(override val eventName: String, @JvmField val data: Any? = null) : MessageEvent diff --git a/api/src/main/kotlin/nebulosa/api/guiding/HistoryStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/HistoryStep.kt index a4f178d70..61f22abcd 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/HistoryStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/HistoryStep.kt @@ -3,11 +3,11 @@ package nebulosa.api.guiding import nebulosa.guiding.GuideStep data class HistoryStep( - val id: Long = 0L, - val rmsRA: Double = 0.0, - val rmsDEC: Double = 0.0, - val rmsTotal: Double = 0.0, - val guideStep: GuideStep? = null, - val ditherX: Double = 0.0, - val ditherY: Double = 0.0, + @JvmField val id: Long = 0L, + @JvmField val rmsRA: Double = 0.0, + @JvmField val rmsDEC: Double = 0.0, + @JvmField val rmsTotal: Double = 0.0, + @JvmField val guideStep: GuideStep? = null, + @JvmField val ditherX: Double = 0.0, + @JvmField val ditherY: Double = 0.0, ) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/SettleInfo.kt b/api/src/main/kotlin/nebulosa/api/guiding/SettleInfo.kt index f46d6986d..5ec2eda2b 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/SettleInfo.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/SettleInfo.kt @@ -4,9 +4,9 @@ import nebulosa.guiding.Guider import org.hibernate.validator.constraints.Range data class SettleInfo( - @Range(min = 1, max = 25) val amount: Double = 1.5, - @Range(min = 1, max = 60) val time: Long = 10, - @Range(min = 1, max = 60) val timeout: Long = 30, + @Range(min = 1, max = 25) @JvmField val amount: Double = 1.5, + @Range(min = 1, max = 60) @JvmField val time: Long = 10, + @Range(min = 1, max = 60) @JvmField val timeout: Long = 30, ) { companion object { diff --git a/api/src/main/kotlin/nebulosa/api/image/CoordinateInterpolation.kt b/api/src/main/kotlin/nebulosa/api/image/CoordinateInterpolation.kt index 5fa45e25a..563c7723f 100644 --- a/api/src/main/kotlin/nebulosa/api/image/CoordinateInterpolation.kt +++ b/api/src/main/kotlin/nebulosa/api/image/CoordinateInterpolation.kt @@ -4,8 +4,12 @@ import java.time.LocalDateTime @Suppress("ArrayInDataClass") data class CoordinateInterpolation( - val ma: DoubleArray, - val md: DoubleArray, - val x0: Int, val y0: Int, val x1: Int, val y1: Int, - val delta: Int, val date: LocalDateTime?, + @JvmField val ma: DoubleArray, + @JvmField val md: DoubleArray, + @JvmField val x0: Int, + @JvmField val y0: Int, + @JvmField val x1: Int, + @JvmField val y1: Int, + @JvmField val delta: Int, + @JvmField val date: LocalDateTime?, ) diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt index 5253b0038..1db713fd5 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt @@ -15,9 +15,9 @@ import nebulosa.skycatalog.SkyObjectType data class ImageAnnotation( override val x: Double, override val y: Double, - val star: StarDSO? = null, - val dso: StarDSO? = null, - val minorPlanet: MinorPlanet? = null, + @JvmField val star: StarDSO? = null, + @JvmField val dso: StarDSO? = null, + @JvmField val minorPlanet: MinorPlanet? = null, ) : Point2D { data class StarDSO( @@ -48,6 +48,6 @@ data class ImageAnnotation( @field:JsonSerialize(using = RightAscensionSerializer::class) override val rightAscensionJ2000: Angle = 0.0, @field:JsonSerialize(using = DeclinationSerializer::class) override val declinationJ2000: Angle = 0.0, override val magnitude: Double = SkyObject.UNKNOWN_MAGNITUDE, - val constellation: Constellation = Constellation.find(ICRF.equatorial(rightAscensionJ2000, declinationJ2000)), + @JvmField val constellation: Constellation = Constellation.find(ICRF.equatorial(rightAscensionJ2000, declinationJ2000)), ) : SkyObject } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageHeaderItem.kt b/api/src/main/kotlin/nebulosa/api/image/ImageHeaderItem.kt index faf357950..b02f3a15d 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageHeaderItem.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageHeaderItem.kt @@ -1,3 +1,3 @@ package nebulosa.api.image -data class ImageHeaderItem(val name: String, val value: String) +data class ImageHeaderItem(@JvmField val name: String, @JvmField val value: String) diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt b/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt index f9afc085d..9c41c8d78 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt @@ -10,14 +10,18 @@ import nebulosa.indi.device.camera.Camera import java.nio.file.Path data class ImageInfo( - val path: Path, - val width: Int, val height: Int, val mono: Boolean, - val stretchShadow: Float = 0.0f, val stretchHighlight: Float = 1.0f, val stretchMidtone: Float = 0.5f, - @field:JsonSerialize(using = RightAscensionSerializer::class) val rightAscension: Double? = null, - @field:JsonSerialize(using = DeclinationSerializer::class) val declination: Double? = null, - val solved: ImageSolved? = null, - val headers: List = emptyList(), - val bitpix: Bitpix = Bitpix.BYTE, - val camera: Camera? = null, - @JsonIgnoreProperties("histogram") val statistics: Statistics.Data? = null, + @JvmField val path: Path, + @JvmField val width: Int, + @JvmField val height: Int, + @JvmField val mono: Boolean, + @JvmField val stretchShadow: Float = 0.0f, + @JvmField val stretchHighlight: Float = 1.0f, + @JvmField val stretchMidtone: Float = 0.5f, + @field:JsonSerialize(using = RightAscensionSerializer::class) @JvmField val rightAscension: Double? = null, + @field:JsonSerialize(using = DeclinationSerializer::class) @JvmField val declination: Double? = null, + @JvmField val solved: ImageSolved? = null, + @JvmField val headers: List = emptyList(), + @JvmField val bitpix: Bitpix = Bitpix.BYTE, + @JvmField val camera: Camera? = null, + @JsonIgnoreProperties("histogram") @JvmField val statistics: Statistics.Data? = null, ) diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageSolved.kt b/api/src/main/kotlin/nebulosa/api/image/ImageSolved.kt index 5823fffc0..04202b45f 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageSolved.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageSolved.kt @@ -4,14 +4,14 @@ import nebulosa.math.* import nebulosa.platesolver.PlateSolution data class ImageSolved( - val solved: Boolean = false, - val orientation: Double = 0.0, - val scale: Double = 0.0, - val rightAscensionJ2000: String = "", - val declinationJ2000: String = "", - val width: Double = 0.0, - val height: Double = 0.0, - val radius: Double = 0.0, + @JvmField val solved: Boolean = false, + @JvmField val orientation: Double = 0.0, + @JvmField val scale: Double = 0.0, + @JvmField val rightAscensionJ2000: String = "", + @JvmField val declinationJ2000: String = "", + @JvmField val width: Double = 0.0, + @JvmField val height: Double = 0.0, + @JvmField val radius: Double = 0.0, ) { constructor(solution: PlateSolution) : this( diff --git a/api/src/main/kotlin/nebulosa/api/image/SaveImage.kt b/api/src/main/kotlin/nebulosa/api/image/SaveImage.kt index 1bfa717f4..2c551086f 100644 --- a/api/src/main/kotlin/nebulosa/api/image/SaveImage.kt +++ b/api/src/main/kotlin/nebulosa/api/image/SaveImage.kt @@ -4,9 +4,9 @@ import nebulosa.fits.Bitpix import java.nio.file.Path data class SaveImage( - val format: ImageExtension = ImageExtension.FITS, - val bitpix: Bitpix = Bitpix.BYTE, - val shouldBeTransformed: Boolean = true, - val transformation: ImageTransformation = ImageTransformation.EMPTY, - val path: Path? = null, + @JvmField val format: ImageExtension = ImageExtension.FITS, + @JvmField val bitpix: Bitpix = Bitpix.BYTE, + @JvmField val shouldBeTransformed: Boolean = true, + @JvmField val transformation: ImageTransformation = ImageTransformation.EMPTY, + @JvmField val path: Path? = null, ) diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt index 06c49459b..19b320255 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt @@ -9,8 +9,8 @@ import nebulosa.indi.device.PropertyVector data class INDIMessageEvent( override val eventName: String, override val device: Device? = null, - val property: PropertyVector<*, *>? = null, - val message: String? = null, + @JvmField val property: PropertyVector<*, *>? = null, + @JvmField val message: String? = null, ) : DeviceMessageEvent { constructor(eventName: String, event: DevicePropertyEvent) : this(eventName, event.device, property = event.property) diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDISendProperty.kt b/api/src/main/kotlin/nebulosa/api/indi/INDISendProperty.kt index 9e71146b8..3bd06d872 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDISendProperty.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDISendProperty.kt @@ -7,7 +7,7 @@ import jakarta.validation.constraints.NotNull import nebulosa.indi.protocol.PropertyType data class INDISendProperty( - @field:NotBlank val name: String = "", - @field:NotNull val type: PropertyType = PropertyType.SWITCH, - @field:NotEmpty @field:Valid val items: List = emptyList(), + @field:NotBlank @JvmField val name: String = "", + @field:NotNull @JvmField val type: PropertyType = PropertyType.SWITCH, + @field:NotEmpty @field:Valid @JvmField val items: List = emptyList(), ) diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDISendPropertyItem.kt b/api/src/main/kotlin/nebulosa/api/indi/INDISendPropertyItem.kt index 8b506a378..c6d382497 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDISendPropertyItem.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDISendPropertyItem.kt @@ -4,6 +4,6 @@ import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotNull data class INDISendPropertyItem( - @field:NotBlank val name: String = "", - @field:NotNull val value: Any = "", + @field:NotBlank @JvmField val name: String = "", + @field:NotNull @JvmField val value: Any = "", ) diff --git a/api/src/main/kotlin/nebulosa/api/livestacker/LiveStackingRequest.kt b/api/src/main/kotlin/nebulosa/api/livestacker/LiveStackingRequest.kt index 6f3cd31db..1d9a3bb47 100644 --- a/api/src/main/kotlin/nebulosa/api/livestacker/LiveStackingRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/livestacker/LiveStackingRequest.kt @@ -1,12 +1,10 @@ package nebulosa.api.livestacker +import jakarta.validation.constraints.NotNull import nebulosa.livestacker.LiveStacker import nebulosa.pixinsight.livestacker.PixInsightLiveStacker -import nebulosa.pixinsight.script.PixInsightIsRunning -import nebulosa.pixinsight.script.PixInsightScriptRunner -import nebulosa.pixinsight.script.PixInsightStartup +import nebulosa.pixinsight.script.startPixInsight import nebulosa.siril.livestacker.SirilLiveStacker -import org.jetbrains.annotations.NotNull import java.nio.file.Files import java.nio.file.Path import java.util.function.Supplier @@ -15,9 +13,9 @@ data class LiveStackingRequest( @JvmField val enabled: Boolean = false, @JvmField val type: LiveStackerType = LiveStackerType.SIRIL, @JvmField @field:NotNull val executablePath: Path? = null, - @JvmField val dark: Path? = null, - @JvmField val flat: Path? = null, - @JvmField val bias: Path? = null, + @JvmField val darkPath: Path? = null, + @JvmField val flatPath: Path? = null, + @JvmField val biasPath: Path? = null, @JvmField val use32Bits: Boolean = false, @JvmField val slot: Int = 1, ) : Supplier { @@ -26,17 +24,10 @@ data class LiveStackingRequest( val workingDirectory = Files.createTempDirectory("ls-") return when (type) { - LiveStackerType.SIRIL -> SirilLiveStacker(executablePath!!, workingDirectory, dark, flat, use32Bits) + LiveStackerType.SIRIL -> SirilLiveStacker(executablePath!!, workingDirectory, darkPath, flatPath, 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) + val runner = startPixInsight(executablePath!!, slot) + PixInsightLiveStacker(runner, workingDirectory, darkPath, flatPath, biasPath, use32Bits, slot) } } } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/ComputedLocation.kt b/api/src/main/kotlin/nebulosa/api/mounts/ComputedLocation.kt index f825170f9..497350194 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/ComputedLocation.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/ComputedLocation.kt @@ -12,15 +12,15 @@ import nebulosa.nova.astrometry.Constellation import java.time.LocalDateTime data class ComputedLocation( - @field:JsonSerialize(using = RightAscensionSerializer::class) var rightAscension: Angle = 0.0, - @field:JsonSerialize(using = DeclinationSerializer::class) var declination: Angle = 0.0, - @field:JsonSerialize(using = RightAscensionSerializer::class) var rightAscensionJ2000: Angle = 0.0, - @field:JsonSerialize(using = DeclinationSerializer::class) var declinationJ2000: Angle = 0.0, - @field:JsonSerialize(using = AzimuthSerializer::class) var azimuth: Angle = 0.0, - @field:JsonSerialize(using = DeclinationSerializer::class) var altitude: Angle = 0.0, - var constellation: Constellation = Constellation.AND, - @field:JsonSerialize(using = LSTSerializer::class) var lst: Angle = 0.0, - @field:JsonFormat(pattern = "HH:mm") var meridianAt: LocalDateTime = LocalDateTime.MIN, - @field:JsonSerialize(using = LSTSerializer::class) var timeLeftToMeridianFlip: Angle = 0.0, - var pierSide: PierSide = PierSide.NEITHER, + @field:JsonSerialize(using = RightAscensionSerializer::class) @JvmField var rightAscension: Angle = 0.0, + @field:JsonSerialize(using = DeclinationSerializer::class) @JvmField var declination: Angle = 0.0, + @field:JsonSerialize(using = RightAscensionSerializer::class) @JvmField var rightAscensionJ2000: Angle = 0.0, + @field:JsonSerialize(using = DeclinationSerializer::class) @JvmField var declinationJ2000: Angle = 0.0, + @field:JsonSerialize(using = AzimuthSerializer::class) @JvmField var azimuth: Angle = 0.0, + @field:JsonSerialize(using = DeclinationSerializer::class) @JvmField var altitude: Angle = 0.0, + @JvmField var constellation: Constellation = Constellation.AND, + @field:JsonSerialize(using = LSTSerializer::class) @JvmField var lst: Angle = 0.0, + @field:JsonFormat(pattern = "HH:mm") @JvmField var meridianAt: LocalDateTime = LocalDateTime.MIN, + @field:JsonSerialize(using = LSTSerializer::class) @JvmField var timeLeftToMeridianFlip: Angle = 0.0, + @JvmField var pierSide: PierSide = PierSide.NEITHER, ) diff --git a/api/src/main/kotlin/nebulosa/api/preference/PreferenceEntity.kt b/api/src/main/kotlin/nebulosa/api/preference/PreferenceEntity.kt index b00b5d7f2..4364d1baa 100644 --- a/api/src/main/kotlin/nebulosa/api/preference/PreferenceEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/preference/PreferenceEntity.kt @@ -9,6 +9,6 @@ import nebulosa.api.database.BoxEntity @Entity data class PreferenceEntity( @Id override var id: Long = 0L, - @Unique(onConflict = ConflictStrategy.REPLACE) var key: String = "", - var value: String? = null, + @Unique(onConflict = ConflictStrategy.REPLACE) @JvmField var key: String = "", + @JvmField var value: String? = null, ) : BoxEntity diff --git a/api/src/main/kotlin/nebulosa/api/preference/PreferenceRequestBody.kt b/api/src/main/kotlin/nebulosa/api/preference/PreferenceRequestBody.kt deleted file mode 100644 index 00e0448f2..000000000 --- a/api/src/main/kotlin/nebulosa/api/preference/PreferenceRequestBody.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.api.preference - -data class PreferenceRequestBody(val data: Any?) diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackerController.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackerController.kt new file mode 100644 index 000000000..382b5aa3a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackerController.kt @@ -0,0 +1,21 @@ +package nebulosa.api.stacker + +import jakarta.validation.Valid +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.* +import java.nio.file.Path + +@Validated +@RestController +@RequestMapping("stacker") +class StackerController(private val stackerService: StackerService) { + + @PutMapping + fun stack(@RequestBody @Valid body: StackingRequest): Path? { + return stackerService.stack(body) + } + + @PutMapping("analyze") + fun analyze(@RequestParam path: Path) { + } +} diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackerGroupType.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackerGroupType.kt new file mode 100644 index 000000000..067f1430c --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackerGroupType.kt @@ -0,0 +1,10 @@ +package nebulosa.api.stacker + +enum class StackerGroupType { + LUMINANCE, + RED, + GREEN, + BLUE, + MONO, + RGB, +} diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt new file mode 100644 index 000000000..32b277406 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt @@ -0,0 +1,78 @@ +package nebulosa.api.stacker + +import nebulosa.stacker.AutoStacker +import org.springframework.stereotype.Service +import java.nio.file.Path + +@Service +class StackerService { + + fun stack(request: StackingRequest): Path? { + val luminance = request.targets.filter { it.enabled && it.group == StackerGroupType.LUMINANCE } + val red = request.targets.filter { it.enabled && it.group == StackerGroupType.RED } + val green = request.targets.filter { it.enabled && it.group == StackerGroupType.GREEN } + val blue = request.targets.filter { it.enabled && it.group == StackerGroupType.BLUE } + val mono = request.targets.filter { it.enabled && it.group == StackerGroupType.MONO } + val rgb = request.targets.filter { it.enabled && it.group == StackerGroupType.RGB } + + val name = "${System.currentTimeMillis()}" + + // Combined LRGB + return if (luminance.size + red.size + green.size + blue.size > 1) { + val stacker = request.get() + + val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE) + val stackedRedPath = red.stack(request, stacker, name, StackerGroupType.RED) + val stackedGreenPath = green.stack(request, stacker, name, StackerGroupType.GREEN) + val stackedBluePath = blue.stack(request, stacker, name, StackerGroupType.BLUE) + + val combinedPath = Path.of("${request.outputDirectory}", "$name.LRGB.fits") + stacker.combineLRGB(combinedPath, stackedLuminancePath, stackedRedPath, stackedGreenPath, stackedBluePath) + combinedPath + } + // LRGB + else if (rgb.size > 1 || luminance.size + rgb.size > 1) { + val stacker = request.get() + + val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE) + val stackedRGBPath = rgb.stack(request, stacker, name, StackerGroupType.RGB) + + if (stackedLuminancePath != null && stackedRGBPath != null) { + val combinedPath = Path.of("${request.outputDirectory}", "$name.LRGB.fits") + stacker.combineLuminance(combinedPath, stackedLuminancePath, stackedRGBPath, false) + combinedPath + } else { + stackedLuminancePath ?: stackedRGBPath + } + } + // MONO + else if (mono.size > 1 || luminance.size + mono.size > 1) { + val stacker = request.get() + + val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE) + val stackedMonoPath = mono.stack(request, stacker, name, StackerGroupType.MONO) + + if (stackedLuminancePath != null && stackedMonoPath != null) { + val combinedPath = Path.of("${request.outputDirectory}", "$name.LRGB.fits") + stacker.combineLuminance(combinedPath, stackedLuminancePath, stackedMonoPath, true) + combinedPath + } else { + stackedLuminancePath ?: stackedMonoPath + } + } else { + null + } + } + + private fun List.stack(request: StackingRequest, stacker: AutoStacker, name: String, group: StackerGroupType): Path? { + return if (size > 1) { + val outputPath = Path.of("${request.outputDirectory}", "$name.$group.fits") + if (stacker.stack(map { it.path!! }, outputPath, request.referencePath!!)) outputPath else null + } else if (isNotEmpty()) { + val outputPath = Path.of("${request.outputDirectory}", "$name.$group.fits") + if (stacker.align(request.referencePath!!, this[0].path!!, outputPath)) outputPath else null + } else { + null + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackerType.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackerType.kt new file mode 100644 index 000000000..4aee6a289 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackerType.kt @@ -0,0 +1,5 @@ +package nebulosa.api.stacker + +enum class StackerType { + PIXINSIGHT, +} diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackingRequest.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackingRequest.kt new file mode 100644 index 000000000..9afe5a710 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackingRequest.kt @@ -0,0 +1,36 @@ +package nebulosa.api.stacker + +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import nebulosa.pixinsight.script.startPixInsight +import nebulosa.pixinsight.stacker.PixInsightAutoStacker +import nebulosa.stacker.AutoStacker +import java.nio.file.Files +import java.nio.file.Path +import java.util.function.Supplier + +data class StackingRequest( + @JvmField @field:NotNull val outputDirectory: Path? = null, + @JvmField val type: StackerType = StackerType.PIXINSIGHT, + @JvmField @field:NotNull val executablePath: Path? = null, + @JvmField val darkPath: Path? = null, + @JvmField val flatPath: Path? = null, + @JvmField val biasPath: Path? = null, + @JvmField val use32Bits: Boolean = false, + @JvmField val slot: Int = 1, + @JvmField @field:NotNull val referencePath: Path? = null, + @JvmField @field:Size(min = 2) @field:Valid val targets: List = emptyList(), +) : Supplier { + + override fun get(): AutoStacker { + val workingDirectory = Files.createTempDirectory("as-") + + return when (type) { + StackerType.PIXINSIGHT -> { + val runner = startPixInsight(executablePath!!, slot) + PixInsightAutoStacker(runner, workingDirectory, darkPath, flatPath, biasPath, slot) + } + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackingTarget.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackingTarget.kt new file mode 100644 index 000000000..a544f999f --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackingTarget.kt @@ -0,0 +1,11 @@ +package nebulosa.api.stacker + +import jakarta.validation.constraints.NotNull +import java.nio.file.Path + +data class StackingTarget( + @JvmField val enabled: Boolean = true, + @JvmField @field:NotNull val path: Path? = null, + @JvmField val group: StackerGroupType = StackerGroupType.MONO, + @JvmField val debayer: Boolean = true, +) diff --git a/api/src/main/kotlin/nebulosa/api/stardetector/StarDetectionRequest.kt b/api/src/main/kotlin/nebulosa/api/stardetector/StarDetectionRequest.kt index c43e42c1f..982f000a9 100644 --- a/api/src/main/kotlin/nebulosa/api/stardetector/StarDetectionRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/stardetector/StarDetectionRequest.kt @@ -1,9 +1,7 @@ package nebulosa.api.stardetector import nebulosa.astap.stardetector.AstapStarDetector -import nebulosa.pixinsight.script.PixInsightIsRunning -import nebulosa.pixinsight.script.PixInsightScriptRunner -import nebulosa.pixinsight.script.PixInsightStartup +import nebulosa.pixinsight.script.startPixInsight import nebulosa.pixinsight.stardetector.PixInsightStarDetector import nebulosa.siril.stardetector.SirilStarDetector import nebulosa.stardetector.StarDetector @@ -24,14 +22,7 @@ data class StarDetectionRequest( StarDetectorType.ASTAP -> AstapStarDetector(executablePath!!, minSNR) StarDetectorType.SIRIL -> SirilStarDetector(executablePath!!, maxStars) StarDetectorType.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") - } - } - + val runner = startPixInsight(executablePath!!, slot) PixInsightStarDetector(runner, slot, minSNR, timeout) } } diff --git a/desktop/.editorconfig b/desktop/.editorconfig index 350957ed8..ccc726593 100644 --- a/desktop/.editorconfig +++ b/desktop/.editorconfig @@ -11,6 +11,7 @@ trim_trailing_whitespace = true [*.ts, *.js] quote_type = single +insert_final_newline = true [*.md] trim_trailing_whitespace = false diff --git a/desktop/src/app/about/about.component.html b/desktop/src/app/about/about.component.html index cfddc9af2..c2fc494dd 100644 --- a/desktop/src/app/about/about.component.html +++ b/desktop/src/app/about/about.component.html @@ -39,7 +39,7 @@
Stack icon by Pixel perfect - Flaticon + + Photo filter icon by Freepik - Flaticon + diff --git a/desktop/src/app/app-routing.module.ts b/desktop/src/app/app-routing.module.ts index 04dfdb38a..7f637d3fe 100644 --- a/desktop/src/app/app-routing.module.ts +++ b/desktop/src/app/app-routing.module.ts @@ -20,6 +20,7 @@ import { MountComponent } from './mount/mount.component' import { RotatorComponent } from './rotator/rotator.component' import { SequencerComponent } from './sequencer/sequencer.component' import { SettingsComponent } from './settings/settings.component' +import { StackerComponent } from './stacker/stacker.component' const routes: Routes = [ { @@ -91,6 +92,10 @@ const routes: Routes = [ path: 'auto-focus', component: AutoFocusComponent, }, + { + path: 'stacker', + component: StackerComponent, + }, { path: 'calculator', component: CalculatorComponent, diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index b16bd44e5..2bc430f40 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -93,6 +93,7 @@ import { MountComponent } from './mount/mount.component' import { RotatorComponent } from './rotator/rotator.component' import { SequencerComponent } from './sequencer/sequencer.component' import { SettingsComponent } from './settings/settings.component' +import { StackerComponent } from './stacker/stacker.component' @NgModule({ declarations: [ @@ -141,6 +142,7 @@ import { SettingsComponent } from './settings/settings.component' SettingsComponent, SkyObjectPipe, SlideMenuComponent, + StackerComponent, StopPropagationDirective, WinPipe, ], diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 3fdeabac0..4eed5b90b 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -609,7 +609,7 @@ [directory]="false" label="Dark File" key="LS_DARK_PATH" - [(path)]="liveStacking.request.dark" + [(path)]="liveStacking.request.darkPath" class="w-full" (pathChange)="savePreference()" />
@@ -619,7 +619,7 @@ [directory]="false" label="Flat File" key="LS_FLAT_PATH" - [(path)]="liveStacking.request.flat" + [(path)]="liveStacking.request.flatPath" class="w-full" (pathChange)="savePreference()" /> @@ -629,7 +629,7 @@ [directory]="false" label="Bias File" key="LS_BIAS_PATH" - [(path)]="liveStacking.request.bias" + [(path)]="liveStacking.request.biasPath" class="w-full" (pathChange)="savePreference()" /> diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 668fac8f5..43b72eaba 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -750,9 +750,9 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { this.request.liveStacking.enabled = cameraPreference.liveStacking?.enabled ?? false this.request.liveStacking.type = cameraPreference.liveStacking?.type ?? 'SIRIL' this.request.liveStacking.executablePath = cameraPreference.liveStacking?.executablePath ?? '' - this.request.liveStacking.dark = cameraPreference.liveStacking?.dark - this.request.liveStacking.flat = cameraPreference.liveStacking?.flat - this.request.liveStacking.bias = cameraPreference.liveStacking?.bias + this.request.liveStacking.darkPath = cameraPreference.liveStacking?.darkPath + this.request.liveStacking.flatPath = cameraPreference.liveStacking?.flatPath + this.request.liveStacking.biasPath = cameraPreference.liveStacking?.biasPath this.request.liveStacking.use32Bits = cameraPreference.liveStacking?.use32Bits ?? false this.request.liveStacking.slot = cameraPreference.liveStacking?.slot ?? 1 diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 95f91e293..d4e0ed15f 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -2,11 +2,10 @@
@@ -14,7 +13,7 @@
+ *ngIf="tab === 'LOCATION'">
@@ -61,7 +60,7 @@
+ *ngIf="tab === 'PLATE_SOLVER'">
@@ -139,7 +138,7 @@
+ *ngIf="tab === 'STAR_DETECTOR'">
@@ -236,7 +235,7 @@
+ *ngIf="tab === 'LIVE_STACKER'">
@@ -282,7 +281,7 @@
+ *ngIf="tab === 'CAPTURE_NAMING_FORMAT'">
diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index d164128f5..3c5677e8d 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -6,7 +6,7 @@ import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' import { EMPTY_LOCATION, Location } from '../../shared/types/atlas.types' import { FrameType, LiveStackerType, LiveStackingRequest } from '../../shared/types/camera.types' -import { DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, PlateSolverRequest, PlateSolverType, resetCameraCaptureNamingFormat, StarDetectionRequest, StarDetectorType } from '../../shared/types/settings.types' +import { DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, PlateSolverRequest, PlateSolverType, resetCameraCaptureNamingFormat, SettingsTabKey, StarDetectionRequest, StarDetectorType } from '../../shared/types/settings.types' import { AppComponent } from '../app.component' @Component({ @@ -14,29 +14,8 @@ import { AppComponent } from '../app.component' templateUrl: './settings.component.html', }) export class SettingsComponent { - tab = 0 - readonly tabs: { id: number; name: string }[] = [ - { - id: 0, - name: 'Location', - }, - { - id: 1, - name: 'Plate Solver', - }, - { - id: 2, - name: 'Star Detection', - }, - { - id: 3, - name: 'Live Stacking', - }, - { - id: 4, - name: 'Capture Naming Format', - }, - ] + tab: SettingsTabKey = 'LOCATION' + readonly tabs: SettingsTabKey[] = ['LOCATION', 'PLATE_SOLVER', 'STAR_DETECTOR', 'LIVE_STACKER', 'STACKER', 'CAPTURE_NAMING_FORMAT'] readonly locations: Location[] location: Location diff --git a/desktop/src/app/stacker/stacker.component.html b/desktop/src/app/stacker/stacker.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/desktop/src/app/stacker/stacker.component.ts b/desktop/src/app/stacker/stacker.component.ts new file mode 100644 index 000000000..3d2357a8c --- /dev/null +++ b/desktop/src/app/stacker/stacker.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'neb-stacker', + templateUrl: './stacker.component.html', +}) +export class StackerComponent {} diff --git a/desktop/src/assets/icons/photo-filter.png b/desktop/src/assets/icons/photo-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..d3f7a892d75eaa33910f5b86a1205413c173088f GIT binary patch literal 1657 zcmV-<28Q{GP)fUJ9K`vTe0zr>rflbV^-C7Wprhc@fh_?%gn33gh(J$JvBpa6017eN!KDLR0u<`C_~|#a3&<`O z%L^uJmBjdo2;ZUd8>DpNJdUj~AXhe9u)RfxSZ81*8RjT4eg#0I9+g@$*oh$DPKc(l z)dB>Xc?VK8u#iOgVJ!JkG=hZr1dTcy5t_%A03q?1Y1=_jlHjQ%$`MpfP4jes2>TeO z8d5Zt*b<;qtV6?ZXr~dNLwRAEr}{|pXAEkQ#5hTq&(UeM9IFww1c-`!Zg{pB+BJE$ z5hHhsCuWnMSfZ5(yNGepLZrsno(J4k-k?d@hCanOs^g>4vz=Du^(jh@CjXaZpORsQ zsU}Jll8}w4AH>m2u*LHh9m;x2^gqa!tw^B|5=&?kuMidmQ!WJUR3^0p@$~Ie2PZB<+3&ekU$NH6TnqSI4M#=?2X%}oFSqdCd+@< zQcf1ffEbAVz@PC;0muM|*U z5ugr49Z(GvgM1c5J8;o_^)rMy0YnGL-5~CoFm40U<}9i-B97nC4c494K~w^LjvUFp zEY=5x$x)myb7Sq!VgW!Aswd$8EO84E{{VY{uw#G8NfUr#T9_yssb;!)l_ zS#C;x1gYuXDFl!pjsZWWJ)oK3J^}9g%sm855MY|m+#Dw^fam~r17<#Ra|#QdD;4`b}$>eJzt zb`Qa39{^qb3g@r-lJ3Ltj!?3>H{>B2i@%qS-pRW>-2+~_`gJ-x<|V8nD?_IwKVr(C zs|2V%RlxacIb+vqECvjWUH^VLkD8`T`un;K>nffP-)w3zv~x`t=m~izCGV8BUbt@1 zQouz5WCwJzGm|&eD?(6|A2qadO&9Qcbv}BtjdKk-#8d%5SHGlo!e0!^3Zj&k#;9+U z$OLOEpAR;!>Lw?{SWwpp08p|p%H|E@Z|&M0FSWt(zx}ORI>_4_B5bZm!hXOAuIU0( zQM^zm!#n<}mlhkHG+W12E#Q9gvUEP)GRV20pYl?bHOn3{0RSq>R4U4*PW;R@9}RaE zUanAidWpRO)b#>P$EDj|zW+G@!09VK4(%8uKg;^AldH#oMBHj04}Dk_-08M=Ht4Dr z(AMLprXgo+Dm)$ajd^tRgqgTmd-QApSr_ALg$$o$rNEVL-nCSp!nnwc6 zbO19=It6gu8JzG4HGKj|`95$vZT<`p9-%53{$2kAgN^?Ks#Ak>00000NkvXXu0mjf DMA+lo literal 0 HcmV?d00001 diff --git a/desktop/src/shared/pipes/dropdown-options.pipe.ts b/desktop/src/shared/pipes/dropdown-options.pipe.ts index a0fed50c1..213b5c4e7 100644 --- a/desktop/src/shared/pipes/dropdown-options.pipe.ts +++ b/desktop/src/shared/pipes/dropdown-options.pipe.ts @@ -6,7 +6,8 @@ import { GuideDirection, GuiderPlotMode, GuiderYAxisUnit } from '../types/guider import { Bitpix, ImageChannel, ImageFormat, SCNRProtectionMethod } from '../types/image.types' import { MountRemoteControlType } from '../types/mount.types' import { SequenceCaptureMode } from '../types/sequencer.types' -import { PlateSolverType, StarDetectorType } from '../types/settings.types' +import { PlateSolverType, SettingsTabKey, StarDetectorType } from '../types/settings.types' +import { StackerType } from '../types/stacker.types' export interface DropdownOptions { STAR_DETECTOR: StarDetectorType[] @@ -28,6 +29,8 @@ export interface DropdownOptions { GUIDER_PLOT_MODE: GuiderPlotMode[] GUIDER_Y_AXIS_UNIT: GuiderYAxisUnit[] SEQUENCE_CAPTURE_MODE: SequenceCaptureMode[] + STACKER: StackerType[] + SETTINGS_TAB: SettingsTabKey[] } @Pipe({ name: 'dropdownOptions' }) @@ -72,6 +75,10 @@ export class DropdownOptionsPipe implements PipeTransform { return ['ARCSEC', 'PIXEL'] as DropdownOptions[K] case 'SEQUENCE_CAPTURE_MODE': return ['FULLY', 'INTERLEAVED'] as DropdownOptions[K] + case 'STACKER': + return ['PIXINSIGHT'] as DropdownOptions[K] + case 'SETTINGS_TAB': + return ['LOCATION', 'PLATE_SOLVER', 'STAR_DETECTOR', 'LIVE_STACKER', 'STACKER', 'CAPTURE_NAMING_FORMAT'] as DropdownOptions[K] } return [] diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index 4a09f52ec..69fa7a87c 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -8,7 +8,8 @@ import { GuideDirection, GuideState, GuiderPlotMode, GuiderYAxisUnit } from '../ import { Bitpix, SCNRProtectionMethod } from '../types/image.types' import { MountRemoteControlType } from '../types/mount.types' import { SequenceCaptureMode } from '../types/sequencer.types' -import { PlateSolverType, StarDetectorType } from '../types/settings.types' +import { PlateSolverType, SettingsTabKey, StarDetectorType } from '../types/settings.types' +import { StackerGroupType, StackerType } from '../types/stacker.types' import { Undefinable } from '../utils/types' export type EnumPipeKey = @@ -36,11 +37,16 @@ export type EnumPipeKey = | MountRemoteControlType | SequenceCaptureMode | Bitpix + | StackerType + | StackerGroupType + | SettingsTabKey | 'ALL' @Pipe({ name: 'enum' }) export class EnumPipe implements PipeTransform { readonly enums: Record> = { + 'DX/DY': 'dx/dy', + 'RA/DEC': 'RA/DEC', ABSOLUTE: 'Absolute', ACTIVE_GALAXY_NUCLEUS: 'Active Galaxy Nucleus', ACTIVE: 'Active', @@ -57,10 +63,13 @@ export class EnumPipe implements PipeTransform { AQL: 'Aquila', AQR: 'Aquarius', ARA: 'Ara', + ARCSEC: 'Arcsec', ARGOS: 'ARGOS Data Collection System', ARI: 'Aries', ASSOCIATION_OF_STARS: 'Association of Stars', ASTAP: 'Astap', + ASTROMETRY_NET_ONLINE: 'Astrometry.net (Online)', + ASTROMETRY_NET: 'Astrometry.net', ASYMPTOTIC_GIANT_BRANCH_STAR: 'Asymptotic Giant Branch Star', AUR: 'Auriga', AVERAGE_NEUTRAL: 'Average Neutral', @@ -76,16 +85,19 @@ export class EnumPipe implements PipeTransform { BLUE_OBJECT: 'Blue Object', BLUE_STRAGGLER: 'Blue Straggler', BLUE_SUPERGIANT: 'Blue Supergiant', + BLUE: 'Blue', BOO: 'Boötes', BRIGHTEST_GALAXY_IN_A_CLUSTER_BCG: 'Brightest Galaxy in a Cluster (BCG)', BROWN_DWARF: 'Brown Dwarf', BUBBLE: 'Bubble', BY_DRA_VARIABLE: 'BY Dra Variable', + BYTE: 'Byte', CAE: 'Caelum', CALIBRATING: 'Calibrating', CAM: 'Camelopardalis', CAP: 'Capricornus', CAPTURE_FINISHED: undefined, + CAPTURE_NAMING_FORMAT: 'Capture Naming Format', CAPTURE_STARTED: undefined, CAPTURED: 'Captured', CAR: 'Carina', @@ -134,7 +146,9 @@ export class EnumPipe implements PipeTransform { DMC: 'Disaster Monitoring', DOR: 'Dorado', DOUBLE_OR_MULTIPLE_STAR: 'Double or Multiple Star', + DOUBLE: 'Double', DRA: 'Draco', + EAST: 'East', ECLIPSING_BINARY: 'Eclipsing Binary', EDUCATION: 'Education', ELLIPSOIDAL_VARIABLE: 'Ellipsoidal Variable', @@ -158,8 +172,10 @@ export class EnumPipe implements PipeTransform { FINISHED: 'Finished', FIXED: 'Fixed', FLAT: 'Flat', + FLOAT: 'Float', FOR: 'Fornax', FORWARD: 'Forward', + FULLY: 'Fully', GALAXY_IN_PAIR_OF_GALAXIES: 'Galaxy in Pair of Galaxies', GALAXY_TOWARDS_A_CLUSTER_OF_GALAXIES: 'Galaxy towards a Cluster of Galaxies', GALAXY_TOWARDS_A_GROUP_OF_GALAXIES: 'Galaxy towards a Group of Galaxies', @@ -186,6 +202,7 @@ export class EnumPipe implements PipeTransform { GRAVITATIONALLY_LENSED_IMAGE_OF_A_GALAXY: 'Gravitationally Lensed Image of a Galaxy', GRAVITATIONALLY_LENSED_IMAGE_OF_A_QUASAR: 'Gravitationally Lensed Image of a Quasar', GRAVITATIONALLY_LENSED_IMAGE: 'Gravitationally Lensed Image', + GREEN: 'Green', GROUP_OF_GALAXIES: 'Group of Galaxies', GRU: 'Grus', GUIDING: 'Guiding', @@ -209,8 +226,10 @@ export class EnumPipe implements PipeTransform { IND: 'Indus', INFRA_RED_SOURCE: 'Infra-Red Source', INITIAL_PAUSE: 'Initial Pause', + INTEGER: 'Integer', INTELSAT: 'Intelsat', INTERACTING_GALAXIES: 'Interacting Galaxies', + INTERLEAVED: 'Interleaved', INTERSTELLAR_FILAMENT: 'Interstellar Filament', INTERSTELLAR_MEDIUM_OBJECT: 'Interstellar Medium Object', INTERSTELLAR_SHELL: 'Interstellar Shell', @@ -225,15 +244,20 @@ export class EnumPipe implements PipeTransform { LIB: 'Libra', LIGHT: 'Light', LINER_TYPE_ACTIVE_GALAXY_NUCLEUS: 'LINER-type Active Galaxy Nucleus', + LIVE_STACKER: 'Live Stacker', LMI: 'Leo Minor', + LOCATION: 'Location', LONG_PERIOD_VARIABLE: 'Long-Period Variable', + LONG: 'Long', LOOP: 'Loop', LOOPING: 'Looping', LOST_LOCK: 'Lost Lock', LOW_MASS_STAR: 'Low-mass Star', LOW_MASS_X_RAY_BINARY: 'Low Mass X-ray Binary', LOW_SURFACE_BRIGHTNESS_GALAXY: 'Low Surface Brightness Galaxy', + LUMINANCE: 'Luminance', LUP: 'Lupus', + LX200: 'LX200', LYN: 'Lynx', LYR: 'Lyra', MAIN_SEQUENCE_STAR: 'Main Sequence Star', @@ -253,6 +277,7 @@ export class EnumPipe implements PipeTransform { MOLECULAR_CLOUD: 'Molecular Cloud', MOLNIYA: 'Molniya', MON: 'Monoceros', + MONO: 'Mono', MOVING_GROUP: 'Moving Group', MOVING: 'Moving', MUS: 'Musca', @@ -264,6 +289,8 @@ export class EnumPipe implements PipeTransform { NOAA: 'NOAA', NONE: 'None', NOR: 'Norma', + NORTH: 'North', + NORTHERN: 'Northern', NOT_AN_OBJECT_ERROR_ARTEFACT: 'Not an Object (Error, Artefact, ...)', OBJECT_OF_UNKNOWN_NATURE: 'Object of Unknown Nature', OCT: 'Octans', @@ -290,9 +317,11 @@ export class EnumPipe implements PipeTransform { PER: 'Perseus', PHE: 'Phoenix', PIC: 'Pictor', + PIXEL: 'Pixel', PIXINSIGHT: 'PixInsight', PLANET: 'Planet', PLANETARY_NEBULA: 'Planetary Nebula', + PLATE_SOLVER: 'Plate Solver', POST_AGB_STAR: 'Post-AGB Star', PROTO_CLUSTER_OF_GALAXIES: 'Proto Cluster of Galaxies', PSA: 'Piscis Austrinus', @@ -310,10 +339,12 @@ export class EnumPipe implements PipeTransform { RADUGA: 'Raduga', RED_GIANT_BRANCH_STAR: 'Red Giant Branch star', RED_SUPERGIANT: 'Red Supergiant', + RED: 'Red', REFLECTION_NEBULA: 'Reflection Nebula', REGION_DEFINED_IN_THE_SKY: 'Region defined in the Sky', RESOURCE: 'Earth Resources', RET: 'Reticulum', + RGB: 'RGB', ROTATING_VARIABLE: 'Rotating Variable', RR_LYRAE_VARIABLE: 'RR Lyrae Variable', RS_CVN_VARIABLE: 'RS CVn Variable', @@ -336,21 +367,27 @@ export class EnumPipe implements PipeTransform { SEYFERT_GALAXY: 'Seyfert Galaxy', SGE: 'Sagitta', SGR: 'Sagittarius', + SHORT: 'Short', SINGLE: 'Single', SIRIL: 'Siril', SLEWED: 'Slewed', SLEWING: 'Slewing', SOLVED: 'Solved', SOLVING: 'Solving', + SOUTH: 'South', + SOUTHERN: 'Southern', SPECTROSCOPIC_BINARY: 'Spectroscopic Binary', SPIRE: 'Spire', + STACKER: 'Stacker', STACKING: 'Stacking', + STAR_DETECTOR: 'Star Detector', STAR_FORMING_REGION: 'Star Forming Region', STAR: 'Star', STARBURST_GALAXY: 'Starburst Galaxy', STARLINK: 'Starlink', STATIONS: 'Space Stations', STELLAR_STREAM: 'Stellar Stream', + STELLARIUM: 'Stellarium', STOPPED: 'Stopped', SUB_MILLIMETRIC_SOURCE: 'Sub-Millimetric Source', SUPERCLUSTER_OF_GALAXIES: 'Supercluster of Galaxies', @@ -384,6 +421,7 @@ export class EnumPipe implements PipeTransform { VUL: 'Vulpecula', WAITING: 'Waiting', WEATHER: 'Weather', + WEST: 'West', WHITE_DWARF: 'White Dwarf', WOLF_RAYET: 'Wolf-Rayet', X_COMM: 'Experimental Comm', @@ -391,28 +429,6 @@ export class EnumPipe implements PipeTransform { X_RAY_SOURCE: 'X-ray Source', YELLOW_SUPERGIANT: 'Yellow Supergiant', YOUNG_STELLAR_OBJECT: 'Young Stellar Object', - ASTROMETRY_NET: 'Astrometry.net', - ASTROMETRY_NET_ONLINE: 'Astrometry.net (Online)', - NORTH: 'North', - NORTHERN: 'Northern', - SOUTH: 'South', - SOUTHERN: 'Southern', - WEST: 'West', - EAST: 'East', - 'RA/DEC': 'RA/DEC', - 'DX/DY': 'dx/dy', - ARCSEC: 'Arcsec', - PIXEL: 'Pixel', - LX200: 'LX200', - STELLARIUM: 'Stellarium', - FULLY: 'Fully', - INTERLEAVED: 'Interleaved', - BYTE: 'Byte', - SHORT: 'Short', - INTEGER: 'Integer', - LONG: 'Long', - FLOAT: 'Float', - DOUBLE: 'Double', } // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 5e2366079..3763e9ddb 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -16,6 +16,7 @@ import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlTyp import { Rotator } from '../types/rotator.types' import { SequencePlan } from '../types/sequencer.types' import { PlateSolverRequest, StarDetectionRequest } from '../types/settings.types' +import { StackingRequest } from '../types/stacker.types' import { FilterWheel } from '../types/wheel.types' import { Undefinable } from '../utils/types' import { HttpService } from './http.service' @@ -680,26 +681,10 @@ export class ApiService { return this.http.put(`auto-focus/${camera.id}/stop`) } - // PREFERENCE + // STACKER - clearPreferences() { - return this.http.put('preferences/clear') - } - - deletePreference(key: string) { - return this.http.delete(`preferences/${key}`) - } - - getPreference(key: string) { - return this.http.get(`preferences/${key}`) - } - - setPreference(key: string, data: unknown) { - return this.http.put(`preferences/${key}`, { data }) - } - - hasPreference(key: string) { - return this.http.get(`preferences/${key}/exists`) + stacker(request: StackingRequest) { + return this.http.put('stacker', request) } // CONFIRMATION diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index c96e3e34b..201f8877a 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -139,10 +139,15 @@ export class BrowserWindowService { } openCalibration(preference: WindowPreference = {}) { - Object.assign(preference, { icon: 'stack', width: 420, height: 400, minHeight: 400 }) + Object.assign(preference, { icon: 'photo-filter', width: 420, height: 400, minHeight: 400 }) return this.openWindow({ preference, id: 'calibration', path: 'calibration' }) } + openStacker(preference: WindowPreference = {}) { + Object.assign(preference, { icon: 'stack', width: 420, height: 400 }) + return this.openWindow({ preference, id: 'stacker', path: 'stacker' }) + } + openAbout() { const preference: WindowPreference = { icon: 'about', width: 430, height: 307, bringToFront: true } return this.openWindow({ preference, id: 'about', path: 'about' }) diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index e0bd4123d..4948c7139 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -293,9 +293,9 @@ export interface LiveStackingRequest { enabled: boolean type: LiveStackerType executablePath: string - dark?: string - flat?: string - bias?: string + darkPath?: string + flatPath?: string + biasPath?: string use32Bits: boolean slot: number } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index 2a0d19960..c906522dd 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -47,6 +47,13 @@ export interface CameraCaptureNamingFormat { bias?: string } +export type SettingsTabKey = 'LOCATION' | 'PLATE_SOLVER' | 'STAR_DETECTOR' | 'LIVE_STACKER' | 'STACKER' | 'CAPTURE_NAMING_FORMAT' + +export interface SettingsTab { + id: SettingsTabKey + name: string +} + export const DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT: CameraCaptureNamingFormat = { light: '[camera]_[type]_[year:2][month][day][hour][min][sec][ms]_[filter]_[width]_[height]_[exp]_[bin]_[gain]', dark: '[camera]_[type]_[width]_[height]_[exp]_[bin]_[gain]', diff --git a/desktop/src/shared/types/stacker.types.ts b/desktop/src/shared/types/stacker.types.ts new file mode 100644 index 000000000..135f3f371 --- /dev/null +++ b/desktop/src/shared/types/stacker.types.ts @@ -0,0 +1,23 @@ +export type StackerType = 'PIXINSIGHT' + +export type StackerGroupType = 'LUMINANCE' | 'RED' | 'GREEN' | 'BLUE' | 'MONO' | 'RGB' + +export interface StackingRequest { + outputDirectory: string + type: StackerType + executablePath: string + darkPath?: string + flatPath?: string + biasPath?: string + use32Bits: boolean + slot: number + referencePath: string + targets: StackingTarget[] +} + +export interface StackingTarget { + enabled: boolean + path: string + group: StackerGroupType + debayer: boolean +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacker/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacker/PixInsightLiveStacker.kt index 0e44d456d..9a4a12f56 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacker/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacker/PixInsightLiveStacker.kt @@ -15,9 +15,9 @@ import kotlin.io.path.deleteIfExists data class PixInsightLiveStacker( private val runner: PixInsightScriptRunner, private val workingDirectory: Path, - private val dark: Path? = null, - private val flat: Path? = null, - private val bias: Path? = null, + private val darkPath: Path? = null, + private val flatPath: Path? = null, + private val biasPath: Path? = null, private val use32Bits: Boolean = false, private val slot: Int = PixInsightScript.UNSPECIFIED_SLOT, ) : LiveStacker { @@ -42,9 +42,7 @@ data class PixInsightLiveStacker( @Synchronized override fun start() { if (!running.get()) { - val isPixInsightRunning = PixInsightIsRunning(slot).use { it.runSync(runner) } - - if (!isPixInsightRunning) { + if (!PixInsightIsRunning(slot).use { it.runSync(runner) }) { try { check(PixInsightStartup(slot).use { it.runSync(runner) }) } catch (e: Throwable) { @@ -64,7 +62,7 @@ data class PixInsightLiveStacker( return if (running.get()) { stacking.set(true) - if (stacker.calibrate(targetPath, calibratedPath, dark, flat, bias)) { + if (stacker.calibrate(targetPath, calibratedPath, darkPath, flatPath, biasPath)) { LOG.info("live stacking calibrated. count={}, output={}", stackCount, calibratedPath) targetPath = calibratedPath } 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 2958d15b0..67e17fc19 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt @@ -23,8 +23,8 @@ data class PixInsightAlign( ) data class Output( - @JvmField val success: Boolean = false, - @JvmField val errorMessage: String? = null, + override val success: Boolean = false, + override val errorMessage: String? = null, @JvmField val outputImage: Path? = null, @JvmField val outputMaskImage: Path? = null, @JvmField val totalPairMatches: Int = 0, @@ -45,7 +45,7 @@ data class PixInsightAlign( @JvmField val h31: Double = 0.0, @JvmField val h32: Double = 0.0, @JvmField val h33: Double = 0.0, - ) { + ) : PixInsightOutput { companion object { diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt index a68dd5049..ddd3fd64d 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt @@ -21,10 +21,10 @@ data class PixInsightAutomaticBackgroundExtractor( ) data class Output( - @JvmField val success: Boolean = false, - @JvmField val errorMessage: String? = null, + override val success: Boolean = false, + override val errorMessage: String? = null, @JvmField val outputImage: Path? = null, - ) { + ) : PixInsightOutput { companion object { 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 c9c7a3f2d..9cb328be8 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -31,10 +31,10 @@ data class PixInsightCalibrate( ) data class Output( - @JvmField val success: Boolean = false, - @JvmField val errorMessage: String? = null, + override val success: Boolean = false, + override val errorMessage: String? = null, @JvmField val outputImage: Path? = null, - ) { + ) : PixInsightOutput { companion object { 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 bfb4952f1..199de03a6 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt @@ -26,10 +26,10 @@ data class PixInsightDetectStars( ) data class Output( - @JvmField val success: Boolean = false, - @JvmField val errorMessage: String? = null, + override val success: Boolean = false, + override val errorMessage: String? = null, @JvmField val stars: List = emptyList(), - ) { + ) : PixInsightOutput { override fun toString() = "Output(success=$success, errorMessage=$errorMessage, stars=${stars.size})" diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightHelper.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightHelper.kt new file mode 100644 index 000000000..638bf19fe --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightHelper.kt @@ -0,0 +1,15 @@ +package nebulosa.pixinsight.script + +import java.nio.file.Path + +fun startPixInsight(executablePath: Path, slot: Int): PixInsightScriptRunner { + val runner = PixInsightScriptRunner(executablePath) + + if (!PixInsightIsRunning(slot).use { it.runSync(runner) }) { + if (!PixInsightStartup(slot).use { it.runSync(runner) }) { + throw IllegalStateException("unable to start PixInsight") + } + } + + return runner +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLRGBCombination.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLRGBCombination.kt index e8362387c..9b92c1304 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLRGBCombination.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLRGBCombination.kt @@ -8,6 +8,7 @@ import kotlin.io.path.deleteIfExists import kotlin.io.path.outputStream import kotlin.io.path.readText +@Suppress("ArrayInDataClass") data class PixInsightLRGBCombination( private val slot: Int, private val outputPath: Path, @@ -15,6 +16,7 @@ data class PixInsightLRGBCombination( private val redPath: Path? = null, private val greenPath: Path? = null, private val bluePath: Path? = null, + private val weights: DoubleArray = DEFAULT_CHANNEL_WEIGHTS, ) : AbstractPixInsightScript() { @Suppress("ArrayInDataClass") @@ -29,10 +31,10 @@ data class PixInsightLRGBCombination( ) data class Output( - @JvmField val success: Boolean = false, - @JvmField val errorMessage: String? = null, + override val success: Boolean = false, + override val errorMessage: String? = null, @JvmField val outputImage: Path? = null, - ) { + ) : PixInsightOutput { companion object { @@ -44,11 +46,12 @@ data class PixInsightLRGBCombination( private val statusPath = Files.createTempFile("pi-", ".txt") init { + require(weights.size >= 4) { "invalid weights size: ${weights.size}" } resource("pixinsight/LRGBCombination.js")!!.transferAndClose(scriptPath.outputStream()) } override val arguments = - listOf("-x=${execute(slot, scriptPath, Input(outputPath, statusPath, luminancePath, redPath, greenPath, bluePath, DEFAULT_CHANNEL_WEIGHTS))}") + listOf("-x=${execute(slot, scriptPath, Input(outputPath, statusPath, luminancePath, redPath, greenPath, bluePath, weights))}") override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLuminanceCombination.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLuminanceCombination.kt new file mode 100644 index 000000000..74e2a3015 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightLuminanceCombination.kt @@ -0,0 +1,68 @@ +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 PixInsightLuminanceCombination( + private val slot: Int, + private val outputPath: Path, + private val luminancePath: Path, + private val targetPath: Path, +) : AbstractPixInsightScript() { + + private data class Input( + @JvmField val outputPath: Path, + @JvmField val statusPath: Path, + @JvmField val luminancePath: Path, + @JvmField val targetPath: Path, + @JvmField val wWeight: Double, + ) + + data class Output( + override val success: Boolean = false, + override val errorMessage: String? = null, + @JvmField val outputImage: Path? = null, + ) : PixInsightOutput { + + companion object { + + @JvmStatic val FAILED = Output() + } + } + + private val scriptPath = Files.createTempFile("pi-", ".js") + private val statusPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/LuminanceCombination.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = + listOf("-x=${execute(slot, scriptPath, Input(outputPath, statusPath, luminancePath, targetPath, 1.0))}") + + override fun processOnComplete(exitCode: Int): Output { + if (exitCode == 0) { + repeat(30) { + 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/PixInsightOutput.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightOutput.kt new file mode 100644 index 000000000..c4d73143a --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightOutput.kt @@ -0,0 +1,8 @@ +package nebulosa.pixinsight.script + +sealed interface PixInsightOutput { + + val success: Boolean + + val errorMessage: String? +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt index a4cd13ad8..7c69ffb33 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt @@ -27,10 +27,10 @@ data class PixInsightPixelMath( ) data class Output( - @JvmField val success: Boolean = false, - @JvmField val errorMessage: String? = null, - @JvmField val stackedImage: Path? = null, - ) { + override val success: Boolean = false, + override val errorMessage: String? = null, + @JvmField val outputImage: Path? = null, + ) : PixInsightOutput { companion object { diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt index 7619a92a1..ac5028c90 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt @@ -25,8 +25,10 @@ data class PixInsightAutoStacker( val alignedPath = Path.of("$workingDirectory", "aligned.xisf") try { - paths.forEachIndexed { stackCount, path -> - var targetPath = path + var stackCount = 0 + + paths.forEach { + var targetPath = it if (stacker.calibrate(targetPath, calibratedPath, darkPath, flatPath, biasPath)) { targetPath = calibratedPath @@ -35,9 +37,11 @@ data class PixInsightAutoStacker( if (stackCount > 0) { if (stacker.align(referencePath, targetPath, alignedPath)) { stacker.integrate(stackCount, outputPath, alignedPath, outputPath) + stackCount++ } } else { targetPath.copyTo(outputPath, true) + stackCount = 1 } } } finally { @@ -47,4 +51,24 @@ data class PixInsightAutoStacker( return true } + + override fun calibrate(targetPath: Path, outputPath: Path, darkPath: Path?, flatPath: Path?, biasPath: Path?): Boolean { + return stacker.calibrate(targetPath, outputPath, darkPath, flatPath, biasPath) + } + + override fun align(referencePath: Path, targetPath: Path, outputPath: Path): Boolean { + return stacker.align(referencePath, targetPath, outputPath) + } + + override fun integrate(stackCount: Int, stackedPath: Path, targetPath: Path, outputPath: Path): Boolean { + return stacker.integrate(stackCount, stackedPath, targetPath, outputPath) + } + + override fun combineLRGB(outputPath: Path, luminancePath: Path?, redPath: Path?, greenPath: Path?, bluePath: Path?): Boolean { + return stacker.combineLRGB(outputPath, luminancePath, redPath, greenPath, bluePath) + } + + override fun combineLuminance(outputPath: Path, luminancePath: Path, targetPath: Path, mono: Boolean): Boolean { + return stacker.combineLuminance(outputPath, luminancePath, targetPath, mono) + } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt index b66a33733..56909d1f6 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt @@ -29,6 +29,23 @@ data class PixInsightStacker( override fun integrate(stackCount: Int, stackedPath: Path, targetPath: Path, outputPath: Path): Boolean { val expressionRK = "({{0}} * $stackCount + {{1}}) / ${stackCount + 1}" return PixInsightPixelMath(slot, listOf(stackedPath, targetPath), outputPath, expressionRK) - .use { it.runSync(runner).stackedImage != null } + .use { it.runSync(runner).outputImage != null } + } + + override fun combineLRGB(outputPath: Path, luminancePath: Path?, redPath: Path?, greenPath: Path?, bluePath: Path?): Boolean { + if (luminancePath == null && redPath == null && greenPath == null && bluePath == null) return false + + return PixInsightLRGBCombination(slot, outputPath, luminancePath, redPath, greenPath, bluePath) + .use { it.runSync(runner).outputImage != null } + } + + override fun combineLuminance(outputPath: Path, luminancePath: Path, targetPath: Path, mono: Boolean): Boolean { + return if (mono) { + PixInsightPixelMath(slot, listOf(luminancePath, targetPath), outputPath, "{{0}} + (1 - {{0}}) * {{1}}") + .use { it.runSync(runner).outputImage != null } + } else { + PixInsightLuminanceCombination(slot, outputPath, luminancePath, targetPath) + .use { it.runSync(runner).outputImage != null } + } } } diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js b/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js index 9ffb6e97c..7fb6fa311 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js @@ -22,6 +22,7 @@ function abe() { const outputPath = input.outputPath const statusPath = input.statusPath + console.writeln("abe started") console.writeln("targetPath=" + targetPath) console.writeln("outputPath=" + outputPath) console.writeln("statusPath=" + statusPath) diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js index d03400960..1df279c34 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js @@ -42,6 +42,7 @@ function alignment() { const outputDirectory = input.outputDirectory const statusPath = input.statusPath + console.writeln("alignment started") console.writeln("referencePath=" + referencePath) console.writeln("targetPath=" + targetPath) console.writeln("outputDirectory=" + outputDirectory) diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js index 7561a2ba0..a4290b64d 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -27,6 +27,7 @@ function calibrate() { const compress = input.compress const use32Bit = input.use32Bit + console.writeln("calibration started") console.writeln("targetPath=" + targetPath) console.writeln("outputDirectory=" + outputDirectory) console.writeln("statusPath=" + statusPath) diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/LRGBCombination.js b/nebulosa-pixinsight/src/main/resources/pixinsight/LRGBCombination.js index bd44e4632..455d9b6ab 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/LRGBCombination.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/LRGBCombination.js @@ -26,6 +26,7 @@ function lrgbCombination() { const greenPath = input.greenPath const bluePath = input.bluePath + console.writeln("LRGB combination started") console.writeln("outputPath=" + outputPath) console.writeln("statusPath=" + statusPath) console.writeln("channelWeights=" + channelWeights) @@ -41,10 +42,10 @@ function lrgbCombination() { var P = new LRGBCombination P.channels = [ // enabled, id, k - [!!luminancePath, luminanceWindow? luminanceWindow.mainView.id : "", channelWeights[0]], [!!redPath, redWindow ? redWindow.mainView.id : "", channelWeights[1]], [!!greenPath, greenWindow ? greenWindow.mainView.id : "", channelWeights[2]], - [!!bluePath, blueWindow ? blueWindow.mainView.id : "", channelWeights[3]] + [!!bluePath, blueWindow ? blueWindow.mainView.id : "", channelWeights[3]], + [!!luminancePath, luminanceWindow ? luminanceWindow.mainView.id : "", channelWeights[0]] ] P.mL = 0.500 P.mc = 0.500 diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/LuminanceCombination.js b/nebulosa-pixinsight/src/main/resources/pixinsight/LuminanceCombination.js new file mode 100644 index 000000000..e70684726 --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/LuminanceCombination.js @@ -0,0 +1,71 @@ +function decodeParams(hex) { + const buffer = new Uint8Array(hex.length / 4) + + for (let i = 0; i < hex.length; i += 4) { + buffer[i / 4] = parseInt(hex.substr(i, 4), 16) + } + + return JSON.parse(String.fromCharCode.apply(null, buffer)) +} + +function luminanceCombination() { + const data = { + success: true, + errorMessage: null, + outputImage: null, + } + + try { + const input = decodeParams(jsArguments[0]) + + const outputPath = input.outputPath + const statusPath = input.statusPath + const weight = input.weight + const luminancePath = input.luminancePath + const targetPath = input.targetPath + + console.writeln("Luminance combination started") + console.writeln("outputPath=" + outputPath) + console.writeln("statusPath=" + statusPath) + console.writeln("weight=" + weight) + console.writeln("luminancePath=" + luminancePath) + console.writeln("targetPath=" + targetPath) + + const luminanceWindow = luminancePath ? ImageWindow.open(luminancePath)[0] : undefined + const targetWindow = targetPath ? ImageWindow.open(targetPath)[0] : undefined + + var P = new LRGBCombination + P.channels = [ // enabled, id, k + [false, "", 1.0], + [false, "", 1.0], + [false, "", 1.0], + [true, luminanceWindow.mainView.id, weight] + ] + P.mL = 0.500 + P.mc = 0.500 + P.clipHighlights = true + P.noiseReduction = false + P.layersRemoved = 4 + P.layersProtected = 2 + P.inheritAstrometricSolution = true + + P.executeOn(targetWindow.mainView) + + targetWindow.saveAs(outputPath, false, false, false, false) + window.forceClose() + + luminanceWindow.forceClose() + + data.outputImage = outputPath + + console.writeln("Luminance combination finished") + } catch (e) { + data.success = false + data.errorMessage = e.message + console.writeln(data.errorMessage) + } finally { + File.writeTextFile(statusPath, "@" + JSON.stringify(data) + "#") + } +} + +luminanceCombination() diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js index 7c335ff77..b3ad983a2 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js @@ -12,7 +12,7 @@ function pixelMath() { const data = { success: true, errorMessage: null, - stackedImage: null, + outputImage: null, } try { @@ -47,6 +47,7 @@ function pixelMath() { } } + console.writeln("pixel math started") console.writeln("expressionRK=" + expressionRK) console.writeln("expressionG=" + expressionG) console.writeln("expressionB=" + expressionB) @@ -87,9 +88,9 @@ function pixelMath() { windows[i].forceClose() } - data.stackedImage = outputPath + data.outputImage = outputPath - console.writeln("stacking finished") + console.writeln("pixel math finished") } catch (e) { data.success = false data.errorMessage = e.message diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index ab35696bc..2e5f8f381 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -10,6 +10,7 @@ import nebulosa.fits.isFits import nebulosa.image.Image import nebulosa.image.algorithms.transformation.AutoScreenTransformFunction import nebulosa.pixinsight.script.* +import nebulosa.pixinsight.script.PixInsightScript.Companion.UNSPECIFIED_SLOT import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.test.NonGitHubOnlyCondition import nebulosa.xisf.isXisf @@ -32,48 +33,89 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { .use { it.runSync(runner).shouldBeTrue() } } "calibrate" { - PixInsightCalibrate(PixInsightScript.UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) + PixInsightCalibrate(UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-calibrate").second shouldBe "731562ee12f45bf7c1095f4773f70e71" } "align" { - PixInsightAlign(PixInsightScript.UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_02_LIGHT) + PixInsightAlign(UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_02_LIGHT) .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-align").second shouldBe "483ebaf15afa5957fe099f3ee2beff78" } "detect stars" { - PixInsightDetectStars(PixInsightScript.UNSPECIFIED_SLOT, PI_FOCUS_0) + PixInsightDetectStars(UNSPECIFIED_SLOT, PI_FOCUS_0) .use { it.runSync(runner).also(::println).stars } .map { it.hfd } .average() shouldBe (8.43 plusOrMinus 1e-2) - PixInsightDetectStars(PixInsightScript.UNSPECIFIED_SLOT, PI_FOCUS_30000) + PixInsightDetectStars(UNSPECIFIED_SLOT, PI_FOCUS_30000) .use { it.runSync(runner).also(::println).stars } .map { it.hfd } .average() shouldBe (1.85 plusOrMinus 1e-2) - PixInsightDetectStars(PixInsightScript.UNSPECIFIED_SLOT, PI_FOCUS_100000) + PixInsightDetectStars(UNSPECIFIED_SLOT, PI_FOCUS_100000) .use { it.runSync(runner).also(::println).stars } .map { it.hfd } .average() shouldBe (18.35 plusOrMinus 1e-2) } "pixel math" { val outputPath = tempfile("pi-stacked-", ".fits").toPath() - PixInsightPixelMath(PixInsightScript.UNSPECIFIED_SLOT, listOf(PI_01_LIGHT, PI_02_LIGHT), outputPath, "{{0}} + {{1}}") - .use { it.runSync(runner).also(::println).stackedImage.shouldNotBeNull().openAsImage() } + PixInsightPixelMath(UNSPECIFIED_SLOT, listOf(PI_01_LIGHT, PI_02_LIGHT), outputPath, "{{0}} + {{1}}") + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-pixelmath").second shouldBe "cafc8138e2ce17614dcfa10edf410b07" } "abe" { val outputPath = tempfile("pi-abe-", ".fits").toPath() - PixInsightAutomaticBackgroundExtractor(PixInsightScript.UNSPECIFIED_SLOT, PI_01_LIGHT, outputPath) + PixInsightAutomaticBackgroundExtractor(UNSPECIFIED_SLOT, PI_01_LIGHT, outputPath) .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-abe").second shouldBe "bf62207dc17190009ba215da7c011297" } "lrgb combination" { val outputPath = tempfile("pi-lrgb-", ".fits").toPath() - PixInsightLRGBCombination(PixInsightScript.UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT) + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT) .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-lrgb").second shouldBe "99db35d78f7b360e7592217f4179b189" + + val weights = doubleArrayOf(1.0, 0.2470588, 0.31764705, 0.709803921) // LRGB #3F51B5 + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, weights) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-weighted-lrgb").second shouldBe "1148ee222fbfb382ad2d708df5b0f79f" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, null, null) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-lr").second shouldBe "9100d3ce892f05f4b832b2fb5f35b5a1" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, null, PI_01_LIGHT, null) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-lg").second shouldBe "b4e8d8f7e289db60b41ba2bbe0035344" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, null, null, PI_01_LIGHT) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-lb").second shouldBe "1760e7cb1d139b63022dd975fe84897d" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, null, PI_01_LIGHT, PI_01_LIGHT, null) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-rg").second shouldBe "8c59307b5943932aefdf2dedfe1c8178" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, null, PI_01_LIGHT, null, PI_01_LIGHT) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-rb").second shouldBe "1bdf9cada6a33f76dceaccdaacf30fef" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, null, null, PI_01_LIGHT, PI_01_LIGHT) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-bg").second shouldBe "4a9c81c71fd37546fd300d1037742fa2" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, null) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-lrg").second shouldBe "06c32c8679d409302423baa3a07fb241" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, null, PI_01_LIGHT) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-lrb").second shouldBe "f6d026cb63f7a58fc325e422c277ff89" + + PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, null, PI_01_LIGHT, PI_01_LIGHT) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .transform(AutoScreenTransformFunction).save("pi-lbg").second shouldBe "67f961110fb4b9f0033b3b8dbc8b1638" } } diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightStackerTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightStackerTest.kt new file mode 100644 index 000000000..1a8574bb2 --- /dev/null +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightStackerTest.kt @@ -0,0 +1,58 @@ +import PixInsightScriptTest.Companion.openAsImage +import io.kotest.core.annotation.EnabledIf +import io.kotest.engine.spec.tempdir +import io.kotest.engine.spec.tempfile +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe +import nebulosa.image.algorithms.transformation.AutoScreenTransformFunction +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.pixinsight.stacker.PixInsightStacker +import nebulosa.test.AbstractFitsAndXisfTest +import nebulosa.test.NonGitHubOnlyCondition +import java.nio.file.Path + +@EnabledIf(NonGitHubOnlyCondition::class) +class PixInsightStackerTest : AbstractFitsAndXisfTest() { + + init { + val runner = PixInsightScriptRunner(Path.of("PixInsight")) + val workingDirectory = tempdir("pi-").toPath() + val stacker = PixInsightStacker(runner, workingDirectory) + + "align" { + val outputPath = tempfile("pi-", ".fits").toPath() + stacker.align(PI_01_LIGHT, PI_03_LIGHT, outputPath).shouldBeTrue() + + outputPath.openAsImage().transform(AutoScreenTransformFunction) + .save("pi-aligned").second shouldBe "106651a7c1e640852384284ec12e0977" + } + "calibrate" { + val outputPath = tempfile("pi-", ".fits").toPath() + stacker.calibrate(PI_01_LIGHT, outputPath, PI_DARK).shouldBeTrue() + + outputPath.openAsImage().transform(AutoScreenTransformFunction) + .save("pi-calibrated").second shouldBe "8f5a2632c701680b41fcfe170c9cf468" + } + "integrate" { + val outputPath = tempfile("pi-", ".fits").toPath() + stacker.integrate(1, PI_01_LIGHT, PI_01_LIGHT, outputPath).shouldBeTrue() + + outputPath.openAsImage().transform(AutoScreenTransformFunction) + .save("pi-integrated").second shouldBe "bf62207dc17190009ba215da7c011297" + } + "combine LRGB" { + val outputPath = tempfile("pi-", ".fits").toPath() + stacker.combineLRGB(outputPath, PI_01_LIGHT, PI_01_LIGHT).shouldBeTrue() + + outputPath.openAsImage().transform(AutoScreenTransformFunction) + .save("pi-lrgb-combined").second shouldBe "9100d3ce892f05f4b832b2fb5f35b5a1" + } + "combine mono luminance" { + val outputPath = tempfile("pi-", ".fits").toPath() + stacker.combineLuminance(outputPath, PI_01_LIGHT, PI_01_LIGHT, true).shouldBeTrue() + + outputPath.openAsImage().transform(AutoScreenTransformFunction) + .save("pi-mono-luminance-combined").second shouldBe "85de365a9895234222acdc6e9feb7009" + } + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StartLs.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StartLs.kt index 716b47754..85afd66af 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StartLs.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StartLs.kt @@ -12,16 +12,16 @@ import kotlin.io.path.isRegularFile * Initializes a livestacking session. */ data class StartLs( - @JvmField val dark: Path? = null, - @JvmField val flat: Path? = null, + @JvmField val darkPath: Path? = null, + @JvmField val flatPath: Path? = null, @JvmField val use32Bits: Boolean = false, ) : SirilCommand, CommandLineListener { private val command by lazy { buildString(256) { append("start_ls") - if (dark != null && dark.exists() && dark.isRegularFile()) append(" \"-dark=$dark\"") - if (flat != null && flat.exists() && flat.isRegularFile()) append(" \"-flat=$flat\"") + if (darkPath != null && darkPath.exists() && darkPath.isRegularFile()) append(" \"-dark=$darkPath\"") + if (flatPath != null && flatPath.exists() && flatPath.isRegularFile()) append(" \"-flat=$flatPath\"") if (use32Bits) append(" -32bits") } } diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt index f8617f95b..a1e421fef 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt @@ -14,8 +14,8 @@ import kotlin.io.path.name data class SirilLiveStacker( private val executablePath: Path, private val workingDirectory: Path, - private val dark: Path? = null, - private val flat: Path? = null, + private val darkPath: Path? = null, + private val flatPath: Path? = null, private val use32Bits: Boolean = false, ) : LiveStacker, CommandLineListener { @@ -38,7 +38,7 @@ data class SirilLiveStacker( try { check(commandLine.execute(Cd(workingDirectory))) { "failed to run cd command" } - check(commandLine.execute(StartLs(dark, flat, use32Bits))) { "failed to start livestacking" } + check(commandLine.execute(StartLs(darkPath, flatPath, use32Bits))) { "failed to start livestacking" } } catch (e: Throwable) { commandLine.close() throw e diff --git a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt index 811c4c867..845c0d96e 100644 --- a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt +++ b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt @@ -2,7 +2,7 @@ package nebulosa.stacker import java.nio.file.Path -interface AutoStacker { +interface AutoStacker : Stacker { fun stack(paths: Collection, outputPath: Path, referencePath: Path = paths.first()): Boolean } diff --git a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt index bbf15d88e..76a74fcef 100644 --- a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt +++ b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt @@ -12,4 +12,8 @@ interface Stacker { fun align(referencePath: Path, targetPath: Path, outputPath: Path): Boolean fun integrate(stackCount: Int, stackedPath: Path, targetPath: Path, outputPath: Path): Boolean + + fun combineLRGB(outputPath: Path, luminancePath: Path? = null, redPath: Path? = null, greenPath: Path? = null, bluePath: Path? = null): Boolean + + fun combineLuminance(outputPath: Path, luminancePath: Path, targetPath: Path, mono: Boolean): Boolean } From 320d7a8fb265386df7b09999c7ad72ad0de1bc77 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 13 Jul 2024 16:14:58 -0300 Subject: [PATCH 3/5] [api][desktop]: Implement PixInsight Stacker --- .../nebulosa/api/stacker/AnalyzedTarget.kt | 23 +++ .../nebulosa/api/stacker/StackerController.kt | 35 +++- .../nebulosa/api/stacker/StackerGroupType.kt | 17 +- .../nebulosa/api/stacker/StackerService.kt | 64 ++++++-- api/src/test/kotlin/StackerServiceTest.kt | 69 ++++++++ desktop/app/window.manager.ts | 10 +- desktop/src/app/home/home.component.html | 9 ++ desktop/src/app/home/home.component.ts | 3 + .../src/app/settings/settings.component.html | 46 ++++++ .../src/app/settings/settings.component.ts | 10 ++ .../src/app/stacker/stacker.component.html | 119 ++++++++++++++ desktop/src/app/stacker/stacker.component.ts | 149 +++++++++++++++++- .../src/shared/pipes/dropdown-options.pipe.ts | 5 +- desktop/src/shared/services/api.service.ts | 18 ++- .../shared/services/browser-window.service.ts | 2 +- .../src/shared/services/electron.service.ts | 35 ++-- .../src/shared/services/preference.service.ts | 6 + desktop/src/shared/types/app.types.ts | 1 + desktop/src/shared/types/home.types.ts | 2 +- desktop/src/shared/types/image.types.ts | 2 +- desktop/src/shared/types/stacker.types.ts | 33 ++++ .../concurrency/cancel/CancellationToken.kt | 10 ++ .../script/AbstractPixInsightScript.kt | 4 +- .../script/PixInsightFileFormatConversion.kt | 65 ++++++++ .../stacker/PixInsightAutoStacker.kt | 50 +++++- .../pixinsight/stacker/PixInsightStacker.kt | 5 + .../pixinsight/FileFormatConversion.js | 46 ++++++ .../src/test/kotlin/PixInsightScriptTest.kt | 45 +++--- .../kotlin/nebulosa/stacker/AutoStacker.kt | 2 + .../main/kotlin/nebulosa/stacker/Stacker.kt | 2 + 30 files changed, 818 insertions(+), 69 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/stacker/AnalyzedTarget.kt create mode 100644 api/src/test/kotlin/StackerServiceTest.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightFileFormatConversion.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/FileFormatConversion.js diff --git a/api/src/main/kotlin/nebulosa/api/stacker/AnalyzedTarget.kt b/api/src/main/kotlin/nebulosa/api/stacker/AnalyzedTarget.kt new file mode 100644 index 000000000..892ff518b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stacker/AnalyzedTarget.kt @@ -0,0 +1,23 @@ +package nebulosa.api.stacker + +import nebulosa.fits.* +import nebulosa.image.format.ReadableHeader +import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.camera.FrameType.Companion.frameType + +data class AnalyzedTarget( + @JvmField val width: Int, + @JvmField val height: Int, + @JvmField val binX: Int, + @JvmField val binY: Int, + @JvmField val gain: Double, + @JvmField val exposureTime: Long, + @JvmField val type: FrameType, + @JvmField val group: StackerGroupType, +) { + + constructor(header: ReadableHeader) : this( + header.width, header.height, header.binX, header.binY, header.gain, header.exposureTimeInMicroseconds, + header.frameType ?: FrameType.LIGHT, StackerGroupType.from(header) + ) +} diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackerController.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackerController.kt index 382b5aa3a..66a95ff33 100644 --- a/api/src/main/kotlin/nebulosa/api/stacker/StackerController.kt +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackerController.kt @@ -1,21 +1,46 @@ package nebulosa.api.stacker import jakarta.validation.Valid +import nebulosa.common.concurrency.cancel.CancellationToken import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* import java.nio.file.Path +import java.util.concurrent.atomic.AtomicReference @Validated @RestController @RequestMapping("stacker") -class StackerController(private val stackerService: StackerService) { +class StackerController( + private val stackerService: StackerService, +) { - @PutMapping - fun stack(@RequestBody @Valid body: StackingRequest): Path? { - return stackerService.stack(body) + private val cancellationToken = AtomicReference() + + @PutMapping("start") + fun start(@RequestBody @Valid body: StackingRequest): Path? { + return if (cancellationToken.compareAndSet(null, CancellationToken())) { + try { + stackerService.stack(body, cancellationToken.get()) + } finally { + cancellationToken.getAndSet(null)?.unlistenAll() + } + } else { + null + } + } + + @GetMapping("running") + fun isRunning(): Boolean { + return cancellationToken.get() != null + } + + @PutMapping("stop") + fun stop() { + cancellationToken.get()?.cancel() } @PutMapping("analyze") - fun analyze(@RequestParam path: Path) { + fun analyze(@RequestParam path: Path): AnalyzedTarget? { + return stackerService.analyze(path) } } diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackerGroupType.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackerGroupType.kt index 067f1430c..641984102 100644 --- a/api/src/main/kotlin/nebulosa/api/stacker/StackerGroupType.kt +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackerGroupType.kt @@ -1,10 +1,25 @@ package nebulosa.api.stacker +import nebulosa.fits.filter +import nebulosa.image.format.ReadableHeader + enum class StackerGroupType { LUMINANCE, RED, GREEN, BLUE, MONO, - RGB, + RGB; + + companion object { + + @JvmStatic + fun from(header: ReadableHeader) = header.filter?.let { + if (it.contains("RED", true) || it.equals("R", true)) RED + else if (it.contains("GREEN", true) || it.equals("G", true)) GREEN + else if (it.contains("BLUE", true) || it.equals("B", true)) BLUE + else if (it.contains("LUMINANCE", true) || it.equals("L", true)) LUMINANCE + else MONO + } ?: MONO + } } diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt index 32b277406..36ee93bd1 100644 --- a/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt @@ -1,13 +1,22 @@ package nebulosa.api.stacker +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.fits.fits +import nebulosa.fits.isFits import nebulosa.stacker.AutoStacker +import nebulosa.xisf.isXisf +import nebulosa.xisf.xisf import org.springframework.stereotype.Service import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.isDirectory @Service class StackerService { - fun stack(request: StackingRequest): Path? { + fun stack(request: StackingRequest, cancellationToken: CancellationToken = CancellationToken.NONE): Path? { + require(request.outputDirectory != null && request.outputDirectory.exists() && request.outputDirectory.isDirectory()) + val luminance = request.targets.filter { it.enabled && it.group == StackerGroupType.LUMINANCE } val red = request.targets.filter { it.enabled && it.group == StackerGroupType.RED } val green = request.targets.filter { it.enabled && it.group == StackerGroupType.GREEN } @@ -21,23 +30,31 @@ class StackerService { return if (luminance.size + red.size + green.size + blue.size > 1) { val stacker = request.get() - val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE) - val stackedRedPath = red.stack(request, stacker, name, StackerGroupType.RED) - val stackedGreenPath = green.stack(request, stacker, name, StackerGroupType.GREEN) - val stackedBluePath = blue.stack(request, stacker, name, StackerGroupType.BLUE) + cancellationToken.listen { stacker.stop() } + + val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE, cancellationToken) + val stackedRedPath = red.stack(request, stacker, name, StackerGroupType.RED, cancellationToken) + val stackedGreenPath = green.stack(request, stacker, name, StackerGroupType.GREEN, cancellationToken) + val stackedBluePath = blue.stack(request, stacker, name, StackerGroupType.BLUE, cancellationToken) - val combinedPath = Path.of("${request.outputDirectory}", "$name.LRGB.fits") - stacker.combineLRGB(combinedPath, stackedLuminancePath, stackedRedPath, stackedGreenPath, stackedBluePath) - combinedPath + if (cancellationToken.isCancelled) { + null + } else { + val combinedPath = Path.of("${request.outputDirectory}", "$name.LRGB.fits") + stacker.combineLRGB(combinedPath, stackedLuminancePath, stackedRedPath, stackedGreenPath, stackedBluePath) + combinedPath + } } // LRGB else if (rgb.size > 1 || luminance.size + rgb.size > 1) { val stacker = request.get() - val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE) - val stackedRGBPath = rgb.stack(request, stacker, name, StackerGroupType.RGB) + val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE, cancellationToken) + val stackedRGBPath = rgb.stack(request, stacker, name, StackerGroupType.RGB, cancellationToken) - if (stackedLuminancePath != null && stackedRGBPath != null) { + if (cancellationToken.isCancelled) { + null + } else if (stackedLuminancePath != null && stackedRGBPath != null) { val combinedPath = Path.of("${request.outputDirectory}", "$name.LRGB.fits") stacker.combineLuminance(combinedPath, stackedLuminancePath, stackedRGBPath, false) combinedPath @@ -49,10 +66,12 @@ class StackerService { else if (mono.size > 1 || luminance.size + mono.size > 1) { val stacker = request.get() - val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE) - val stackedMonoPath = mono.stack(request, stacker, name, StackerGroupType.MONO) + val stackedLuminancePath = luminance.stack(request, stacker, name, StackerGroupType.LUMINANCE, cancellationToken) + val stackedMonoPath = mono.stack(request, stacker, name, StackerGroupType.MONO, cancellationToken) - if (stackedLuminancePath != null && stackedMonoPath != null) { + if (cancellationToken.isCancelled) { + null + } else if (stackedLuminancePath != null && stackedMonoPath != null) { val combinedPath = Path.of("${request.outputDirectory}", "$name.LRGB.fits") stacker.combineLuminance(combinedPath, stackedLuminancePath, stackedMonoPath, true) combinedPath @@ -64,8 +83,13 @@ class StackerService { } } - private fun List.stack(request: StackingRequest, stacker: AutoStacker, name: String, group: StackerGroupType): Path? { - return if (size > 1) { + private fun List.stack( + request: StackingRequest, stacker: AutoStacker, + name: String, group: StackerGroupType, cancellationToken: CancellationToken, + ): Path? { + return if (cancellationToken.isCancelled) { + null + } else if (size > 1) { val outputPath = Path.of("${request.outputDirectory}", "$name.$group.fits") if (stacker.stack(map { it.path!! }, outputPath, request.referencePath!!)) outputPath else null } else if (isNotEmpty()) { @@ -75,4 +99,12 @@ class StackerService { null } } + + fun analyze(path: Path): AnalyzedTarget? { + val image = if (path.isFits()) path.fits() + else if (path.isXisf()) path.xisf() + else return null + + return image.use { it.firstOrNull()?.header }?.let(::AnalyzedTarget) + } } diff --git a/api/src/test/kotlin/StackerServiceTest.kt b/api/src/test/kotlin/StackerServiceTest.kt new file mode 100644 index 000000000..abfcbd3b5 --- /dev/null +++ b/api/src/test/kotlin/StackerServiceTest.kt @@ -0,0 +1,69 @@ +import io.kotest.core.annotation.EnabledIf +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import nebulosa.api.stacker.* +import nebulosa.fits.fits +import nebulosa.image.Image +import nebulosa.image.algorithms.transformation.AutoScreenTransformFunction +import nebulosa.test.AbstractFitsAndXisfTest +import nebulosa.test.NonGitHubOnlyCondition +import java.nio.file.Path +import kotlin.io.path.createDirectories + +@EnabledIf(NonGitHubOnlyCondition::class) +class StackerServiceTest : AbstractFitsAndXisfTest() { + + init { + val service = StackerService() + + val paths = listOf( + Path.of("$BASE_DIR/20240513.213424625-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213436506-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213448253-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213500627-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213512554-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213524278-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213535967-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213547683-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213559416-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213611421-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213624939-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213636654-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213648389-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213701880-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213713546-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213725316-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213738803-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213750501-LIGHT.fits"), + Path.of("$BASE_DIR/20240513.213802188-LIGHT.fits"), + ) + + val darkPath = Path.of("/home/tiagohm/Imagens/Astrophotos/Dark/2024-06-08/ASI294_BIN4_G120_O80/10-DARK.fits") + + "stack LRGB" { + val targets = paths.map { + val analyzed = service.analyze(it)!! + StackingTarget(true, it, analyzed.group, true) + } + + targets.count { it.group == StackerGroupType.LUMINANCE } shouldBeExactly 10 + targets.count { it.group == StackerGroupType.RED } shouldBeExactly 3 + targets.count { it.group == StackerGroupType.GREEN } shouldBeExactly 3 + targets.count { it.group == StackerGroupType.BLUE } shouldBeExactly 3 + + val request = StackingRequest( + Path.of(BASE_DIR, "stacker").createDirectories(), StackerType.PIXINSIGHT, + Path.of("PixInsight"), darkPath, null, null, false, 1, paths[0], targets + ) + + val image = service.stack(request).shouldNotBeNull().fits().use(Image::open) + image.transform(AutoScreenTransformFunction).save("stacker-lrgb").second shouldBe "465a296bb4582ab2f938757347500eb8" + } + } + + companion object { + + const val BASE_DIR = "/home/tiagohm/Imagens/Astrophotos/Light/Algieba/2024-05-13" + } +} diff --git a/desktop/app/window.manager.ts b/desktop/app/window.manager.ts index 9d25ba4ca..965cf851c 100644 --- a/desktop/app/window.manager.ts +++ b/desktop/app/window.manager.ts @@ -288,13 +288,19 @@ export class WindowManager { const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) if (window) { + const properties: Electron.OpenDialogOptions['properties'] = ['openFile'] + + if (command.multiple) { + properties.push('multiSelections') + } + const ret = await dialog.showOpenDialog(window.browserWindow, { filters: command.filters, - properties: ['openFile'], + properties, defaultPath: command.defaultPath || undefined, }) - return !ret.canceled && ret.filePaths[0] + return !ret.canceled && (command.multiple ? ret.filePaths : ret.filePaths[0]) } else { return false } diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index 3d918cf3f..bbd3239fd 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -217,6 +217,15 @@
Auto Focus
+
+ + +
Stacker
+
+
+
+
+
+ + + + +
+
+ +
+
+ + + + +
+
+
diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index 3c5677e8d..79441a195 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -7,6 +7,7 @@ import { PrimeService } from '../../shared/services/prime.service' import { EMPTY_LOCATION, Location } from '../../shared/types/atlas.types' import { FrameType, LiveStackerType, LiveStackingRequest } from '../../shared/types/camera.types' import { DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, PlateSolverRequest, PlateSolverType, resetCameraCaptureNamingFormat, SettingsTabKey, StarDetectionRequest, StarDetectorType } from '../../shared/types/settings.types' +import { StackerType, StackingRequest } from '../../shared/types/stacker.types' import { AppComponent } from '../app.component' @Component({ @@ -29,6 +30,9 @@ export class SettingsComponent { liveStackerType: LiveStackerType = 'SIRIL' readonly liveStackers = new Map() + stackerType: StackerType = 'PIXINSIGHT' + readonly stackers = new Map() + readonly cameraCaptureNamingFormat = structuredClone(DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT) constructor( @@ -52,6 +56,9 @@ export class SettingsComponent { for (const type of dropdownOptions.transform('LIVE_STACKER')) { this.liveStackers.set(type, preference.liveStackingRequest(type).get()) } + for (const type of dropdownOptions.transform('STACKER')) { + this.stackers.set(type, preference.stackingRequest(type).get()) + } Object.assign(this.cameraCaptureNamingFormat, preference.cameraCaptureNamingFormatPreference.get(this.cameraCaptureNamingFormat)) } @@ -125,6 +132,9 @@ export class SettingsComponent { for (const type of this.dropdownOptions.transform('LIVE_STACKER')) { this.preference.liveStackingRequest(type).set(this.liveStackers.get(type)) } + for (const type of this.dropdownOptions.transform('STACKER')) { + this.preference.stackingRequest(type).set(this.stackers.get(type)) + } this.preference.cameraCaptureNamingFormatPreference.set(this.cameraCaptureNamingFormat) } diff --git a/desktop/src/app/stacker/stacker.component.html b/desktop/src/app/stacker/stacker.component.html index e69de29bb..e42e550a9 100644 --- a/desktop/src/app/stacker/stacker.component.html +++ b/desktop/src/app/stacker/stacker.component.html @@ -0,0 +1,119 @@ +
+
+ +
+
+ +
+
+ + + + +
+
+
+ {{ item.type }} + +
+ Debayer + +
+
+ Reference + +
+
+ Enabled + +
+
+ @if (item.analyzed) { +
+ EXP: {{ item.analyzed.exposureTime | exposureTime }} + WIDTH: {{ item.analyzed.width }} + HEIGHT: {{ item.analyzed.height }} + BIN: {{ item.analyzed.binX }}x{{ item.analyzed.binY }} + GAIN: {{ item.analyzed.gain }} +
+ } +
+ {{ item.path }} +
+
+
+ +
+
+
+
+
+
+
+
+ + +
+
diff --git a/desktop/src/app/stacker/stacker.component.ts b/desktop/src/app/stacker/stacker.component.ts index 3d2357a8c..c992a7df1 100644 --- a/desktop/src/app/stacker/stacker.component.ts +++ b/desktop/src/app/stacker/stacker.component.ts @@ -1,7 +1,152 @@ -import { Component } from '@angular/core' +import { AfterViewInit, Component } from '@angular/core' +import { dirname } from 'path' +import { ApiService } from '../../shared/services/api.service' +import { BrowserWindowService } from '../../shared/services/browser-window.service' +import { ElectronService } from '../../shared/services/electron.service' +import { PreferenceService } from '../../shared/services/preference.service' +import { StackerGroupType, StackingRequest, StackingTarget } from '../../shared/types/stacker.types' +import { AppComponent } from '../app.component' @Component({ selector: 'neb-stacker', templateUrl: './stacker.component.html', }) -export class StackerComponent {} +export class StackerComponent implements AfterViewInit { + running = false + readonly request: StackingRequest = { + outputDirectory: '', + type: 'PIXINSIGHT', + executablePath: '', + use32Bits: false, + slot: 0, + referencePath: '', + targets: [], + } + + get referenceTarget() { + return this.request.targets.find((e) => e.enabled && e.reference && e.type === 'LIGHT') + } + + get hasReference() { + return !!this.referenceTarget + } + + get canStart() { + return !!this.request.outputDirectory && this.hasReference + } + + constructor( + app: AppComponent, + private readonly electron: ElectronService, + private readonly api: ApiService, + private readonly preference: PreferenceService, + private readonly browserWindow: BrowserWindowService, + ) { + app.title = 'Stacker' + } + + async ngAfterViewInit() { + this.loadPreference() + + this.running = await this.api.stackerIsRunning() + } + + async openImages() { + try { + this.running = true + + const stackerPreference = this.preference.stackerPreference.get() + const images = await this.electron.openImages({ defaultPath: stackerPreference.defaultPath }) + + if (images && images.length) { + const targets: StackingTarget[] = [...this.request.targets] + + for (const path of images) { + const analyzed = await this.api.stackerAnalyze(path) + + if (analyzed) { + targets.push({ + enabled: true, + path, + analyzed, + type: analyzed.type, + group: analyzed.group, + reference: analyzed.type === 'LIGHT' && !targets.length && !this.referenceTarget, + debayer: analyzed.type === 'LIGHT' && analyzed.group === 'RGB', + }) + } + } + + this.request.targets = targets + + stackerPreference.defaultPath = dirname(images[0]) + this.preference.stackerPreference.set(stackerPreference) + } + } finally { + this.running = false + } + } + + targetGroupChanged(target: StackingTarget, group: StackerGroupType) { + if (group === 'RGB') { + target.debayer = true + } + } + + referenceChanged(target: StackingTarget, enabled: boolean) { + if (enabled) { + for (const item of this.request.targets) { + if (item.reference && item !== target) { + item.reference = false + } + } + } + } + + deleteTarget(target: StackingTarget) { + const index = this.request.targets.findIndex((e) => e === target) + + if (index >= 0) { + this.request.targets.splice(index, 1) + } + } + + async startStacking() { + const stackingRequest = this.preference.stackingRequest(this.request.type).get() + this.request.executablePath = stackingRequest.executablePath + this.request.slot = stackingRequest.slot || 1 + this.request.referencePath = this.referenceTarget!.path + + const request: StackingRequest = { + ...this.request, + targets: this.request.targets.filter((e) => e.enabled), + } + + try { + this.running = true + const path = await this.api.stackerStart(request) + + if (path) { + await this.browserWindow.openImage({ path, source: 'STACKER' }) + } + } finally { + this.running = false + } + } + + stopStacking() { + return this.api.stackerStop() + } + + private loadPreference() { + const stackerPreference = this.preference.stackerPreference.get() + + this.request.outputDirectory = stackerPreference.outputDirectory ?? '' + } + + savePreference() { + const stackerPreference = this.preference.stackerPreference.get() + stackerPreference.outputDirectory = this.request.outputDirectory + this.preference.stackerPreference.set(stackerPreference) + } +} diff --git a/desktop/src/shared/pipes/dropdown-options.pipe.ts b/desktop/src/shared/pipes/dropdown-options.pipe.ts index 213b5c4e7..cc5b4c34b 100644 --- a/desktop/src/shared/pipes/dropdown-options.pipe.ts +++ b/desktop/src/shared/pipes/dropdown-options.pipe.ts @@ -7,7 +7,7 @@ import { Bitpix, ImageChannel, ImageFormat, SCNRProtectionMethod } from '../type import { MountRemoteControlType } from '../types/mount.types' import { SequenceCaptureMode } from '../types/sequencer.types' import { PlateSolverType, SettingsTabKey, StarDetectorType } from '../types/settings.types' -import { StackerType } from '../types/stacker.types' +import { StackerGroupType, StackerType } from '../types/stacker.types' export interface DropdownOptions { STAR_DETECTOR: StarDetectorType[] @@ -31,6 +31,7 @@ export interface DropdownOptions { SEQUENCE_CAPTURE_MODE: SequenceCaptureMode[] STACKER: StackerType[] SETTINGS_TAB: SettingsTabKey[] + STACKER_GROUP_TYPE: StackerGroupType[] } @Pipe({ name: 'dropdownOptions' }) @@ -79,6 +80,8 @@ export class DropdownOptionsPipe implements PipeTransform { return ['PIXINSIGHT'] as DropdownOptions[K] case 'SETTINGS_TAB': return ['LOCATION', 'PLATE_SOLVER', 'STAR_DETECTOR', 'LIVE_STACKER', 'STACKER', 'CAPTURE_NAMING_FORMAT'] as DropdownOptions[K] + case 'STACKER_GROUP_TYPE': + return ['LUMINANCE', 'RED', 'GREEN', 'BLUE', 'MONO', 'RGB'] as DropdownOptions[K] } return [] diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 3763e9ddb..b7efe8e25 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -16,7 +16,7 @@ import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlTyp import { Rotator } from '../types/rotator.types' import { SequencePlan } from '../types/sequencer.types' import { PlateSolverRequest, StarDetectionRequest } from '../types/settings.types' -import { StackingRequest } from '../types/stacker.types' +import { AnalyzedTarget, StackingRequest } from '../types/stacker.types' import { FilterWheel } from '../types/wheel.types' import { Undefinable } from '../utils/types' import { HttpService } from './http.service' @@ -683,8 +683,20 @@ export class ApiService { // STACKER - stacker(request: StackingRequest) { - return this.http.put('stacker', request) + stackerStart(request: StackingRequest) { + return this.http.put('stacker/start', request) + } + + stackerIsRunning() { + return this.http.get('stacker/running') + } + + stackerStop() { + return this.http.put('stacker/stop') + } + + stackerAnalyze(path: string) { + return this.http.put(`stacker/analyze?path=${path}`) } // CONFIRMATION diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 201f8877a..9504f266d 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -144,7 +144,7 @@ export class BrowserWindowService { } openStacker(preference: WindowPreference = {}) { - Object.assign(preference, { icon: 'stack', width: 420, height: 400 }) + Object.assign(preference, { icon: 'stack', width: 370, height: 460 }) return this.openWindow({ preference, id: 'stacker', path: 'stacker' }) } diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index f54a7bf0b..06d1637e7 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -23,7 +23,12 @@ import { Mount } from '../types/mount.types' import { Rotator } from '../types/rotator.types' import { SequencerEvent } from '../types/sequencer.types' import { FilterWheel, WheelRenamed } from '../types/wheel.types' -import { Undefinable } from '../utils/types' + +export const IMAGE_FILE_FILTER: Electron.FileFilter[] = [ + { name: 'All', extensions: ['fits', 'fit', 'xisf'] }, + { name: 'FITS', extensions: ['fits', 'fit'] }, + { name: 'XISF', extensions: ['xisf'] }, +] interface EventMappedType { NOTIFICATION: NotificationEvent @@ -128,23 +133,31 @@ export class ElectronService { }) } - openFile(data?: OpenFile): Promise> { - return this.send('FILE.OPEN', { ...data, windowId: data?.windowId ?? window.id }) + openFile(data?: OpenFile): Promise { + return this.send('FILE.OPEN', { ...data, windowId: data?.windowId ?? window.id, multiple: false }) + } + + openFiles(data?: OpenFile): Promise { + return this.send('FILE.OPEN', { ...data, windowId: data?.windowId ?? window.id, multiple: true }) } - saveFile(data?: OpenFile): Promise> { + saveFile(data?: OpenFile): Promise { return this.send('FILE.SAVE', { ...data, windowId: data?.windowId ?? window.id }) } - openImage(data?: OpenFile): Promise> { + openImage(data?: OpenFile) { return this.openFile({ ...data, windowId: data?.windowId ?? window.id, - filters: [ - { name: 'All', extensions: ['fits', 'fit', 'xisf'] }, - { name: 'FITS', extensions: ['fits', 'fit'] }, - { name: 'XISF', extensions: ['xisf'] }, - ], + filters: IMAGE_FILE_FILTER, + }) + } + + openImages(data?: OpenFile) { + return this.openFiles({ + ...data, + windowId: data?.windowId ?? window.id, + filters: IMAGE_FILE_FILTER, }) } @@ -166,7 +179,7 @@ export class ElectronService { } async saveJson(data: SaveJson): Promise | false> { - data.path = data.path || (await this.saveFile({ ...data, windowId: data.windowId ?? window.id, filters: [{ name: 'JSON files', extensions: ['json'] }] })) + data.path = data.path || (await this.saveFile({ ...data, windowId: data.windowId ?? window.id, filters: [{ name: 'JSON files', extensions: ['json'] }] })) || undefined if (data.path) { if (await this.writeJson(data)) { diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index a346fb1a5..5d30d03a4 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -12,6 +12,7 @@ import { EMPTY_MOUNT_PREFERENCE, Mount, MountPreference } from '../types/mount.t import { Rotator, RotatorPreference } from '../types/rotator.types' import { EMPTY_SEQUENCER_PREFERENCE, SequencerPreference } from '../types/sequencer.types' import { CameraCaptureNamingFormat, DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, EMPTY_PLATE_SOLVER_REQUEST, EMPTY_STAR_DETECTION_REQUEST, PlateSolverRequest, PlateSolverType, StarDetectionRequest, StarDetectorType } from '../types/settings.types' +import { EMPTY_STACKER_PREFERENCE, EMPTY_STACKING_REQUEST, StackerPreference, StackerType, StackingRequest } from '../types/stacker.types' import { FilterWheel, WheelPreference } from '../types/wheel.types' import { Undefinable } from '../utils/types' import { LocalStorageService } from './local-storage.service' @@ -84,6 +85,10 @@ export class PreferenceService { return new PreferenceData(this.storage, `liveStacking.${type}`, () => ({ ...EMPTY_LIVE_STACKING_REQUEST, type }) as LiveStackingRequest) } + stackingRequest(type: StackerType) { + return new PreferenceData(this.storage, `stacking.${type}`, () => ({ ...EMPTY_STACKING_REQUEST, type }) as StackingRequest) + } + equipmentForDevice(device: Device) { return new PreferenceData(this.storage, `equipment.${device.name}`, () => ({}) as Equipment) } @@ -112,4 +117,5 @@ export class PreferenceService { readonly autoFocusPreference = new PreferenceData(this.storage, 'autoFocus', () => structuredClone(EMPTY_AUTO_FOCUS_PREFERENCE)) readonly sequencerPreference = new PreferenceData(this.storage, 'sequencer', () => structuredClone(EMPTY_SEQUENCER_PREFERENCE)) readonly cameraCaptureNamingFormatPreference = new PreferenceData(this.storage, 'camera.namingFormat', () => structuredClone(DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT)) + readonly stackerPreference = new PreferenceData(this.storage, 'stacker', () => structuredClone(EMPTY_STACKER_PREFERENCE)) } diff --git a/desktop/src/shared/types/app.types.ts b/desktop/src/shared/types/app.types.ts index a819efe98..5c95403fe 100644 --- a/desktop/src/shared/types/app.types.ts +++ b/desktop/src/shared/types/app.types.ts @@ -80,6 +80,7 @@ export interface OpenDirectory extends WindowCommand { export interface OpenFile extends OpenDirectory { filters?: Electron.FileFilter[] + multiple?: boolean } export interface JsonFile { diff --git a/desktop/src/shared/types/home.types.ts b/desktop/src/shared/types/home.types.ts index 3af6d20fb..14a1c01f3 100644 --- a/desktop/src/shared/types/home.types.ts +++ b/desktop/src/shared/types/home.types.ts @@ -5,7 +5,7 @@ import type { Mount } from './mount.types' import type { Rotator } from './rotator.types' import type { FilterWheel } from './wheel.types' -export type HomeWindowType = DeviceType | 'GUIDER' | 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' | 'AUTO_FOCUS' +export type HomeWindowType = DeviceType | 'GUIDER' | 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' | 'AUTO_FOCUS' | 'STACKER' export const CONNECTION_TYPES = ['INDI', 'ALPACA'] as const diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index e9ca592d8..d1b4610d4 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -7,7 +7,7 @@ export type ImageChannel = 'RED' | 'GREEN' | 'BLUE' | 'GRAY' export type SCNRProtectionMethod = 'MAXIMUM_MASK' | 'ADDITIVE_MASK' | 'AVERAGE_NEUTRAL' | 'MAXIMUM_NEUTRAL' | 'MINIMUM_NEUTRAL' -export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' | 'FLAT_WIZARD' | 'SEQUENCER' | 'ALIGNMENT' | 'AUTO_FOCUS' +export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' | 'FLAT_WIZARD' | 'SEQUENCER' | 'ALIGNMENT' | 'AUTO_FOCUS' | 'STACKER' export type ImageFormat = 'FITS' | 'XISF' | 'PNG' | 'JPG' diff --git a/desktop/src/shared/types/stacker.types.ts b/desktop/src/shared/types/stacker.types.ts index 135f3f371..44d4f4683 100644 --- a/desktop/src/shared/types/stacker.types.ts +++ b/desktop/src/shared/types/stacker.types.ts @@ -1,3 +1,5 @@ +import type { FrameType } from './camera.types' + export type StackerType = 'PIXINSIGHT' export type StackerGroupType = 'LUMINANCE' | 'RED' | 'GREEN' | 'BLUE' | 'MONO' | 'RGB' @@ -15,9 +17,40 @@ export interface StackingRequest { targets: StackingTarget[] } +export const EMPTY_STACKING_REQUEST: StackingRequest = { + outputDirectory: '', + type: 'PIXINSIGHT', + executablePath: '', + use32Bits: false, + slot: 1, + referencePath: '', + targets: [], +} + export interface StackingTarget { enabled: boolean path: string + type: FrameType group: StackerGroupType debayer: boolean + reference: boolean + analyzed?: AnalyzedTarget } + +export interface AnalyzedTarget { + width: number + height: number + binX: number + binY: number + gain: number + exposureTime: number + type: FrameType + group: StackerGroupType +} + +export interface StackerPreference { + outputDirectory?: string + defaultPath?: string +} + +export const EMPTY_STACKER_PREFERENCE: StackerPreference = {} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationToken.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationToken.kt index 3ee538fb8..837a300e4 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationToken.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationToken.kt @@ -2,6 +2,7 @@ package nebulosa.common.concurrency.cancel import nebulosa.common.concurrency.latch.Pauser import java.io.Closeable +import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture import java.util.concurrent.Future import java.util.concurrent.TimeUnit @@ -43,6 +44,11 @@ class CancellationToken private constructor(private val completable: Completable listeners.remove(listener) } + @Synchronized + fun unlistenAll() { + listeners.clear() + } + fun cancel() { cancel(true) } @@ -74,6 +80,10 @@ class CancellationToken private constructor(private val completable: Completable return completable?.get(timeout, unit) ?: CancellationSource.None } + fun throwIfCancelled() { + if (isCancelled) throw CancellationException() + } + override fun close() { super.close() 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 5a2f54ff2..f6e746c39 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -61,7 +61,9 @@ abstract class AbstractPixInsightScript : PixInsightScript, CommandLineLis } @JvmStatic - internal fun execute(slot: Int, scriptPath: Path, data: Any?): String { + internal fun PixInsightScript<*>.execute(slot: Int, scriptPath: Path, data: Any?): String { + LOG.info("{} will be executed. slot={}, script={}, data={}", this::class.simpleName, slot, scriptPath, data) + return buildString { if (slot > 0) append("$slot:") append("\"$scriptPath") diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightFileFormatConversion.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightFileFormatConversion.kt new file mode 100644 index 000000000..024b3aff4 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightFileFormatConversion.kt @@ -0,0 +1,65 @@ +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 PixInsightFileFormatConversion( + private val slot: Int, + private val inputPath: Path, + private val outputPath: Path, +) : AbstractPixInsightScript() { + + private data class Input( + @JvmField val inputPath: Path, + @JvmField val outputPath: Path, + @JvmField val statusPath: Path, + ) + + data class Output( + override val success: Boolean = false, + override val errorMessage: String? = null, + @JvmField val outputImage: Path? = null, + ) : PixInsightOutput { + + companion object { + + @JvmStatic val FAILED = Output() + } + } + + private val scriptPath = Files.createTempFile("pi-", ".js") + private val statusPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/FileFormatConversion.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = + listOf("-x=${execute(slot, scriptPath, Input(inputPath, outputPath, statusPath))}") + + override fun processOnComplete(exitCode: Int): Output { + if (exitCode == 0) { + repeat(30) { + 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/stacker/PixInsightAutoStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt index ac5028c90..8637dca85 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightAutoStacker.kt @@ -1,10 +1,12 @@ package nebulosa.pixinsight.stacker +import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.pixinsight.script.PixInsightScript import nebulosa.pixinsight.script.PixInsightScriptRunner import nebulosa.stacker.AutoStacker import java.nio.file.Path -import kotlin.io.path.copyTo +import java.util.concurrent.CancellationException +import java.util.concurrent.atomic.AtomicReference import kotlin.io.path.deleteIfExists data class PixInsightAutoStacker( @@ -16,10 +18,12 @@ data class PixInsightAutoStacker( private val slot: Int = PixInsightScript.UNSPECIFIED_SLOT, ) : AutoStacker { + private val cancellationToken = AtomicReference() private val stacker = PixInsightStacker(runner, workingDirectory, slot) override fun stack(paths: Collection, outputPath: Path, referencePath: Path): Boolean { if (paths.isEmpty()) return false + if (!cancellationToken.compareAndSet(null, CancellationToken())) return false val calibratedPath = Path.of("$workingDirectory", "calibrated.xisf") val alignedPath = Path.of("$workingDirectory", "aligned.xisf") @@ -27,26 +31,52 @@ data class PixInsightAutoStacker( try { var stackCount = 0 - paths.forEach { + val realPaths = paths.map { it.toRealPath() } + val referenceRealPath = referencePath.toRealPath() + + realPaths.forEach { var targetPath = it - if (stacker.calibrate(targetPath, calibratedPath, darkPath, flatPath, biasPath)) { + cancellationToken.get().throwIfCancelled() + + if (calibrate(targetPath, calibratedPath, darkPath, flatPath, biasPath)) { targetPath = calibratedPath } + cancellationToken.get().throwIfCancelled() + if (stackCount > 0) { - if (stacker.align(referencePath, targetPath, alignedPath)) { - stacker.integrate(stackCount, outputPath, alignedPath, outputPath) + if (align(referenceRealPath, targetPath, alignedPath)) { + cancellationToken.get().throwIfCancelled() + integrate(stackCount, outputPath, alignedPath, outputPath) stackCount++ } } else { - targetPath.copyTo(outputPath, true) + if (referenceRealPath != it) { + if (align(referenceRealPath, targetPath, alignedPath)) { + cancellationToken.get().throwIfCancelled() + saveAs(alignedPath, outputPath) + cancellationToken.get().throwIfCancelled() + integrate(0, outputPath, alignedPath, outputPath) + } else { + saveAs(targetPath, outputPath) + } + } else { + saveAs(targetPath, outputPath) + } + stackCount = 1 } + + cancellationToken.get().throwIfCancelled() } + } catch (e: CancellationException) { + return false } finally { calibratedPath.deleteIfExists() alignedPath.deleteIfExists() + + cancellationToken.getAndSet(null) } return true @@ -71,4 +101,12 @@ data class PixInsightAutoStacker( override fun combineLuminance(outputPath: Path, luminancePath: Path, targetPath: Path, mono: Boolean): Boolean { return stacker.combineLuminance(outputPath, luminancePath, targetPath, mono) } + + override fun saveAs(inputPath: Path, outputPath: Path): Boolean { + return stacker.saveAs(inputPath, outputPath) + } + + override fun stop() { + cancellationToken.get()?.cancel() + } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt index 56909d1f6..91a85b51a 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/stacker/PixInsightStacker.kt @@ -48,4 +48,9 @@ data class PixInsightStacker( .use { it.runSync(runner).outputImage != null } } } + + override fun saveAs(inputPath: Path, outputPath: Path): Boolean { + return PixInsightFileFormatConversion(slot, inputPath, outputPath) + .use { it.runSync(runner).outputImage != null } + } } diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/FileFormatConversion.js b/nebulosa-pixinsight/src/main/resources/pixinsight/FileFormatConversion.js new file mode 100644 index 000000000..060a1b4bd --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/FileFormatConversion.js @@ -0,0 +1,46 @@ +function decodeParams(hex) { + const buffer = new Uint8Array(hex.length / 4) + + for (let i = 0; i < hex.length; i += 4) { + buffer[i / 4] = parseInt(hex.substr(i, 4), 16) + } + + return JSON.parse(String.fromCharCode.apply(null, buffer)) +} + +function fileFormatConversion() { + const data = { + success: true, + errorMessage: null, + outputImage: null, + } + + try { + const input = decodeParams(jsArguments[0]) + + const outputPath = input.outputPath + const statusPath = input.statusPath + const inputPath = input.inputPath + + console.writeln("Format conversion started") + console.writeln("outputPath=" + outputPath) + console.writeln("statusPath=" + statusPath) + console.writeln("inputPath=" + inputPath) + + const window = ImageWindow.open(inputPath)[0] + window.saveAs(outputPath, false, false, false, false) + window.forceClose() + + data.outputImage = outputPath + + console.writeln("Format conversion finished") + } catch (e) { + data.success = false + data.errorMessage = e.message + console.writeln(data.errorMessage) + } finally { + File.writeTextFile(statusPath, "@" + JSON.stringify(data) + "#") + } +} + +fileFormatConversion() diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index 2e5f8f381..cf1ed2a92 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -34,89 +34,98 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { } "calibrate" { PixInsightCalibrate(UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-calibrate").second shouldBe "731562ee12f45bf7c1095f4773f70e71" } "align" { PixInsightAlign(UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_02_LIGHT) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-align").second shouldBe "483ebaf15afa5957fe099f3ee2beff78" } "detect stars" { PixInsightDetectStars(UNSPECIFIED_SLOT, PI_FOCUS_0) - .use { it.runSync(runner).also(::println).stars } + .use { it.runSync(runner).stars } .map { it.hfd } .average() shouldBe (8.43 plusOrMinus 1e-2) PixInsightDetectStars(UNSPECIFIED_SLOT, PI_FOCUS_30000) - .use { it.runSync(runner).also(::println).stars } + .use { it.runSync(runner).stars } .map { it.hfd } .average() shouldBe (1.85 plusOrMinus 1e-2) PixInsightDetectStars(UNSPECIFIED_SLOT, PI_FOCUS_100000) - .use { it.runSync(runner).also(::println).stars } + .use { it.runSync(runner).stars } .map { it.hfd } .average() shouldBe (18.35 plusOrMinus 1e-2) } "pixel math" { val outputPath = tempfile("pi-stacked-", ".fits").toPath() PixInsightPixelMath(UNSPECIFIED_SLOT, listOf(PI_01_LIGHT, PI_02_LIGHT), outputPath, "{{0}} + {{1}}") - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-pixelmath").second shouldBe "cafc8138e2ce17614dcfa10edf410b07" } "abe" { val outputPath = tempfile("pi-abe-", ".fits").toPath() PixInsightAutomaticBackgroundExtractor(UNSPECIFIED_SLOT, PI_01_LIGHT, outputPath) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-abe").second shouldBe "bf62207dc17190009ba215da7c011297" } "lrgb combination" { val outputPath = tempfile("pi-lrgb-", ".fits").toPath() PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-lrgb").second shouldBe "99db35d78f7b360e7592217f4179b189" val weights = doubleArrayOf(1.0, 0.2470588, 0.31764705, 0.709803921) // LRGB #3F51B5 PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, weights) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-weighted-lrgb").second shouldBe "1148ee222fbfb382ad2d708df5b0f79f" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, null, null) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-lr").second shouldBe "9100d3ce892f05f4b832b2fb5f35b5a1" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, null, PI_01_LIGHT, null) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-lg").second shouldBe "b4e8d8f7e289db60b41ba2bbe0035344" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, null, null, PI_01_LIGHT) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-lb").second shouldBe "1760e7cb1d139b63022dd975fe84897d" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, null, PI_01_LIGHT, PI_01_LIGHT, null) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-rg").second shouldBe "8c59307b5943932aefdf2dedfe1c8178" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, null, PI_01_LIGHT, null, PI_01_LIGHT) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-rb").second shouldBe "1bdf9cada6a33f76dceaccdaacf30fef" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, null, null, PI_01_LIGHT, PI_01_LIGHT) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-bg").second shouldBe "4a9c81c71fd37546fd300d1037742fa2" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, PI_01_LIGHT, null) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-lrg").second shouldBe "06c32c8679d409302423baa3a07fb241" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, PI_01_LIGHT, null, PI_01_LIGHT) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-lrb").second shouldBe "f6d026cb63f7a58fc325e422c277ff89" PixInsightLRGBCombination(UNSPECIFIED_SLOT, outputPath, PI_01_LIGHT, null, PI_01_LIGHT, PI_01_LIGHT) - .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().openAsImage() } + .use { it.runSync(runner).outputImage.shouldNotBeNull().openAsImage() } .transform(AutoScreenTransformFunction).save("pi-lbg").second shouldBe "67f961110fb4b9f0033b3b8dbc8b1638" } + "file format conversion" { + val xisfPath = tempfile("pi-ffc", ".xisf").toPath() + PixInsightFileFormatConversion(UNSPECIFIED_SLOT, PI_01_LIGHT, xisfPath) + .use { it.runSync(runner).outputImage.shouldNotBeNull().isXisf().shouldBeTrue() } + + val fitsPath = tempfile("pi-ffc", ".fits").toPath() + PixInsightFileFormatConversion(UNSPECIFIED_SLOT, xisfPath, fitsPath) + .use { it.runSync(runner).outputImage.shouldNotBeNull().isFits().shouldBeTrue() } + } } companion object { diff --git a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt index 845c0d96e..d13fa3fc5 100644 --- a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt +++ b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/AutoStacker.kt @@ -5,4 +5,6 @@ import java.nio.file.Path interface AutoStacker : Stacker { fun stack(paths: Collection, outputPath: Path, referencePath: Path = paths.first()): Boolean + + fun stop() } diff --git a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt index 76a74fcef..31cf7f719 100644 --- a/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt +++ b/nebulosa-stacker/src/main/kotlin/nebulosa/stacker/Stacker.kt @@ -16,4 +16,6 @@ interface Stacker { fun combineLRGB(outputPath: Path, luminancePath: Path? = null, redPath: Path? = null, greenPath: Path? = null, bluePath: Path? = null): Boolean fun combineLuminance(outputPath: Path, luminancePath: Path, targetPath: Path, mono: Boolean): Boolean + + fun saveAs(inputPath: Path, outputPath: Path): Boolean } From 5e0ae0dc19559b92dbfdc52c6e9fcdb57d9e18c2 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 13 Jul 2024 17:06:42 -0300 Subject: [PATCH 4/5] [api][desktop]: Implement PixInsight Stacker --- .../nebulosa/api/stacker/StackerService.kt | 3 + .../nebulosa/api/stacker/StackingRequest.kt | 9 +++ desktop/README.md | 4 + desktop/src/app/camera/camera.component.html | 6 +- .../src/app/stacker/stacker.component.html | 75 +++++++++++++++--- desktop/src/app/stacker/stacker.component.ts | 39 ++++----- desktop/src/shared/types/stacker.types.ts | 20 ++++- desktop/stacker.png | Bin 0 -> 44140 bytes 8 files changed, 122 insertions(+), 34 deletions(-) create mode 100644 desktop/stacker.png diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt index 36ee93bd1..f56ababd2 100644 --- a/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackerService.kt @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service import java.nio.file.Path import kotlin.io.path.exists import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile @Service class StackerService { @@ -101,6 +102,8 @@ class StackerService { } fun analyze(path: Path): AnalyzedTarget? { + if (!path.exists() || !path.isRegularFile()) return null + val image = if (path.isFits()) path.fits() else if (path.isXisf()) path.xisf() else return null diff --git a/api/src/main/kotlin/nebulosa/api/stacker/StackingRequest.kt b/api/src/main/kotlin/nebulosa/api/stacker/StackingRequest.kt index 9afe5a710..66b00312a 100644 --- a/api/src/main/kotlin/nebulosa/api/stacker/StackingRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/stacker/StackingRequest.kt @@ -9,14 +9,19 @@ import nebulosa.stacker.AutoStacker import java.nio.file.Files import java.nio.file.Path import java.util.function.Supplier +import kotlin.io.path.exists +import kotlin.io.path.isRegularFile data class StackingRequest( @JvmField @field:NotNull val outputDirectory: Path? = null, @JvmField val type: StackerType = StackerType.PIXINSIGHT, @JvmField @field:NotNull val executablePath: Path? = null, @JvmField val darkPath: Path? = null, + @JvmField val darkEnabled: Boolean = false, @JvmField val flatPath: Path? = null, + @JvmField val flatEnabled: Boolean = false, @JvmField val biasPath: Path? = null, + @JvmField val biasEnabled: Boolean = false, @JvmField val use32Bits: Boolean = false, @JvmField val slot: Int = 1, @JvmField @field:NotNull val referencePath: Path? = null, @@ -26,6 +31,10 @@ data class StackingRequest( override fun get(): AutoStacker { val workingDirectory = Files.createTempDirectory("as-") + val darkPath = darkPath?.takeIf { darkEnabled && it.exists() && it.isRegularFile() } + val flatPath = flatPath?.takeIf { flatEnabled && it.exists() && it.isRegularFile() } + val biasPath = biasPath?.takeIf { biasEnabled && it.exists() && it.isRegularFile() } + return when (type) { StackerType.PIXINSIGHT -> { val runner = startPixInsight(executablePath!!, slot) diff --git a/desktop/README.md b/desktop/README.md index 2f0bae907..244054bc3 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -58,6 +58,10 @@ The complete integrated solution for all of your astronomical imaging needs. ![](sequencer.png) +## Stacker + +![](stacker.png) + ## INDI ![](indi.png) diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 4eed5b90b..ae4338c98 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -608,7 +608,7 @@ [disabled]="!liveStacking.request.enabled" [directory]="false" label="Dark File" - key="LS_DARK_PATH" + key="LIVE_STACKER_DARK_PATH" [(path)]="liveStacking.request.darkPath" class="w-full" (pathChange)="savePreference()" /> @@ -618,7 +618,7 @@ [disabled]="!liveStacking.request.enabled" [directory]="false" label="Flat File" - key="LS_FLAT_PATH" + key="LIVE_STACKER_FLAT_PATH" [(path)]="liveStacking.request.flatPath" class="w-full" (pathChange)="savePreference()" /> @@ -628,7 +628,7 @@ [disabled]="!liveStacking.request.enabled || liveStacking.request.type !== 'PIXINSIGHT'" [directory]="false" label="Bias File" - key="LS_BIAS_PATH" + key="LIVE_STACKER_BIAS_PATH" [(path)]="liveStacking.request.biasPath" class="w-full" (pathChange)="savePreference()" /> diff --git a/desktop/src/app/stacker/stacker.component.html b/desktop/src/app/stacker/stacker.component.html index e42e550a9..086859656 100644 --- a/desktop/src/app/stacker/stacker.component.html +++ b/desktop/src/app/stacker/stacker.component.html @@ -1,7 +1,7 @@
+ leftIcon="mdi mdi-file-multiple"> -
- Debayer - -
Reference + +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+
diff --git a/desktop/src/app/stacker/stacker.component.ts b/desktop/src/app/stacker/stacker.component.ts index c992a7df1..4692a91f0 100644 --- a/desktop/src/app/stacker/stacker.component.ts +++ b/desktop/src/app/stacker/stacker.component.ts @@ -4,7 +4,7 @@ import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { StackerGroupType, StackingRequest, StackingTarget } from '../../shared/types/stacker.types' +import { EMPTY_STACKING_REQUEST, StackingRequest, StackingTarget } from '../../shared/types/stacker.types' import { AppComponent } from '../app.component' @Component({ @@ -13,15 +13,7 @@ import { AppComponent } from '../app.component' }) export class StackerComponent implements AfterViewInit { running = false - readonly request: StackingRequest = { - outputDirectory: '', - type: 'PIXINSIGHT', - executablePath: '', - use32Bits: false, - slot: 0, - referencePath: '', - targets: [], - } + readonly request = structuredClone(EMPTY_STACKING_REQUEST) get referenceTarget() { return this.request.targets.find((e) => e.enabled && e.reference && e.type === 'LIGHT') @@ -64,15 +56,14 @@ export class StackerComponent implements AfterViewInit { for (const path of images) { const analyzed = await this.api.stackerAnalyze(path) - if (analyzed) { + if (analyzed && analyzed.type === 'LIGHT') { targets.push({ enabled: true, path, analyzed, type: analyzed.type, group: analyzed.group, - reference: analyzed.type === 'LIGHT' && !targets.length && !this.referenceTarget, - debayer: analyzed.type === 'LIGHT' && analyzed.group === 'RGB', + reference: !targets.length && !this.referenceTarget, }) } } @@ -87,12 +78,6 @@ export class StackerComponent implements AfterViewInit { } } - targetGroupChanged(target: StackingTarget, group: StackerGroupType) { - if (group === 'RGB') { - target.debayer = true - } - } - referenceChanged(target: StackingTarget, enabled: boolean) { if (enabled) { for (const item of this.request.targets) { @@ -122,6 +107,8 @@ export class StackerComponent implements AfterViewInit { targets: this.request.targets.filter((e) => e.enabled), } + this.savePreference() + try { this.running = true const path = await this.api.stackerStart(request) @@ -142,11 +129,25 @@ export class StackerComponent implements AfterViewInit { const stackerPreference = this.preference.stackerPreference.get() this.request.outputDirectory = stackerPreference.outputDirectory ?? '' + this.request.darkPath = stackerPreference.darkPath + this.request.darkEnabled = stackerPreference.darkEnabled ?? false + this.request.flatPath = stackerPreference.flatPath + this.request.flatEnabled = stackerPreference.flatEnabled ?? false + this.request.biasPath = stackerPreference.biasPath + this.request.biasEnabled = stackerPreference.biasEnabled ?? false + this.request.type = stackerPreference.type ?? 'PIXINSIGHT' } savePreference() { const stackerPreference = this.preference.stackerPreference.get() stackerPreference.outputDirectory = this.request.outputDirectory + stackerPreference.darkPath = this.request.darkPath + stackerPreference.darkEnabled = this.request.darkEnabled + stackerPreference.flatPath = this.request.flatPath + stackerPreference.flatEnabled = this.request.flatEnabled + stackerPreference.biasPath = this.request.biasPath + stackerPreference.biasEnabled = this.request.biasEnabled + stackerPreference.type = this.request.type this.preference.stackerPreference.set(stackerPreference) } } diff --git a/desktop/src/shared/types/stacker.types.ts b/desktop/src/shared/types/stacker.types.ts index 44d4f4683..f2971eeff 100644 --- a/desktop/src/shared/types/stacker.types.ts +++ b/desktop/src/shared/types/stacker.types.ts @@ -9,8 +9,11 @@ export interface StackingRequest { type: StackerType executablePath: string darkPath?: string + darkEnabled: boolean flatPath?: string + flatEnabled: boolean biasPath?: string + biasEnabled: boolean use32Bits: boolean slot: number referencePath: string @@ -25,6 +28,9 @@ export const EMPTY_STACKING_REQUEST: StackingRequest = { slot: 1, referencePath: '', targets: [], + darkEnabled: false, + flatEnabled: false, + biasEnabled: false, } export interface StackingTarget { @@ -32,7 +38,6 @@ export interface StackingTarget { path: string type: FrameType group: StackerGroupType - debayer: boolean reference: boolean analyzed?: AnalyzedTarget } @@ -49,8 +54,19 @@ export interface AnalyzedTarget { } export interface StackerPreference { + type?: StackerType outputDirectory?: string defaultPath?: string + darkPath?: string + darkEnabled?: boolean + flatPath?: string + flatEnabled?: boolean + biasPath?: string + biasEnabled?: boolean } -export const EMPTY_STACKER_PREFERENCE: StackerPreference = {} +export const EMPTY_STACKER_PREFERENCE: StackerPreference = { + darkEnabled: false, + flatEnabled: false, + biasEnabled: false, +} diff --git a/desktop/stacker.png b/desktop/stacker.png new file mode 100644 index 0000000000000000000000000000000000000000..f7dabfdd505232d3871b26c38e4836fbe62568f6 GIT binary patch literal 44140 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�rIWLc(LY);tIe}T@A2Rf5b3S~EWFmGksUXP579NeqRzc|`rxpQjG zIww+WSMjTBYeM|}yLhM@^}os_%offc8$Tae5z5XdP_@J(ASCLv|IVEW=r(?IZ35&Z zpnef(ATsC36z46LFRHjIyScbXCl~D7mt<>suBpk%cFvXD>3{gmP7EbF4*dt4h-Bsa z&z~n{jCRDb%kJh^J$KHWwVO7f5xQu80hUv88f&SBhF9@X07C`qmiiM;5t9O|m#V6s zy>Ow6f>NN^sRMkRM34;Fin+Gj&{L{$7pntgt!f%_&hJD3DU_b5s3GF}qp?B#24o3c zn2r7mC1kDUPr<3UFI&-ZYW6woy`iWi8-NVWSQkny`Q(d<Egk3wgNiF2pORzeR0^US?2U-&*phgH z3Ait*`U+f0()xGmWlm=1eQp9GRFrg=&YLpDo%uloidoL3g1(&BAVmj+X~dYhoPItf z;maTVUHY=_sY+uFtH?k5_um?J!&x`t4;oR%t;)QK%y@}2`7KZL@1invkR+{cL{;PPr_-)=qy=j8hjEQjjN#C`XdMFkx1J* z(fQ1EN4;Ep1tZQq6CQi9!8wNtg|rVz?q*3K{x;#4_=afX#xp-xe^{X6PjB{tVWhm>Z!x$NA?Z#&(g?&u*tyh@k!E&5ze&d$%{e_W)g=X7WM`isQMH#%fnp*--pRX{+u!BTv^Qeo-tt z5f5QfaLTk=Nu+n!92lauGus3iB=_&h8__s$dc(au2aUSFU%0A3P+Nt#P?0bS_;I% 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 E0UDMe_y7O^ literal 0 HcmV?d00001 From 7ad094cc608962a048f15b3e52fa0bddfdb2e486 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 13 Jul 2024 17:44:32 -0300 Subject: [PATCH 5/5] [api]: Fix failed tests --- api/src/test/kotlin/SkyAtlasServiceTest.kt | 2 +- api/src/test/kotlin/StackerServiceTest.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/test/kotlin/SkyAtlasServiceTest.kt b/api/src/test/kotlin/SkyAtlasServiceTest.kt index 6a25036af..90ae1268f 100644 --- a/api/src/test/kotlin/SkyAtlasServiceTest.kt +++ b/api/src/test/kotlin/SkyAtlasServiceTest.kt @@ -143,7 +143,7 @@ class SkyAtlasServiceTest : StringSpec() { position.declinationJ2000.formatSignedDMS() shouldBe "-017°22'47.2\"" position.rightAscension.formatHMS() shouldBe "14h49m00.4s" position.declination.formatSignedDMS() shouldBe "-017°29'00.1\"" - position.azimuth.formatDMS() shouldBe "144°36'58.8\"" + position.azimuth.formatDMS() shouldBe "144°36'58.9\"" position.altitude.formatSignedDMS() shouldBe "-045°07'44.9\"" position.constellation shouldBe Constellation.LIB position.distance shouldBe (9633.950 plusOrMinus 1e-3) diff --git a/api/src/test/kotlin/StackerServiceTest.kt b/api/src/test/kotlin/StackerServiceTest.kt index abfcbd3b5..77a0f664d 100644 --- a/api/src/test/kotlin/StackerServiceTest.kt +++ b/api/src/test/kotlin/StackerServiceTest.kt @@ -54,7 +54,8 @@ class StackerServiceTest : AbstractFitsAndXisfTest() { val request = StackingRequest( Path.of(BASE_DIR, "stacker").createDirectories(), StackerType.PIXINSIGHT, - Path.of("PixInsight"), darkPath, null, null, false, 1, paths[0], targets + Path.of("PixInsight"), darkPath, true, null, false, null, false, false, + 1, paths[0], targets ) val image = service.stack(request).shouldNotBeNull().fits().use(Image::open)