From 098a3e8b5f3ab63268b8f07599aa6ec1d1debcff Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 3 Jun 2024 11:31:16 -0300 Subject: [PATCH 01/26] [api]: Create pixinsight module --- nebulosa-pixinsight/build.gradle.kts | 21 +++++++++++++++++++++ settings.gradle.kts | 1 + 2 files changed, 22 insertions(+) create mode 100644 nebulosa-pixinsight/build.gradle.kts diff --git a/nebulosa-pixinsight/build.gradle.kts b/nebulosa-pixinsight/build.gradle.kts new file mode 100644 index 000000000..fe459eb39 --- /dev/null +++ b/nebulosa-pixinsight/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + kotlin("jvm") + id("maven-publish") +} + +dependencies { + api(project(":nebulosa-common")) + api(project(":nebulosa-math")) + api(project(":nebulosa-plate-solving")) + api(project(":nebulosa-star-detection")) + api(project(":nebulosa-livestacking")) + implementation(project(":nebulosa-log")) +} + +publishing { + publications { + create("pluginMaven") { + from(components["java"]) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c9c622236..3a55fd073 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -77,6 +77,7 @@ include(":nebulosa-nasa") include(":nebulosa-netty") include(":nebulosa-nova") include(":nebulosa-phd2-client") +include(":nebulosa-pixinsight") include(":nebulosa-plate-solving") include(":nebulosa-retrofit") include(":nebulosa-sbd") From 586d6e7e4051fea915f0bfe7b55f6de6acc23c55 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 3 Jun 2024 16:52:37 -0300 Subject: [PATCH 02/26] [api]: Integrate PixInsight --- .../nebulosa/common/exec/CommandLine.kt | 12 ++-- nebulosa-pixinsight/build.gradle.kts | 1 + .../pixinsight/PixInsightIsRunning.kt | 64 +++++++++++++++++++ .../nebulosa/pixinsight/PixInsightScript.kt | 26 ++++++++ .../pixinsight/PixInsightScriptRunner.kt | 57 +++++++++++++++++ .../nebulosa/pixinsight/PixInsightStartup.kt | 55 ++++++++++++++++ .../src/main/resources/pixinsight/Startup.js | 1 + .../src/test/kotlin/PixInsightScriptTest.kt | 23 +++++++ 8 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightIsRunning.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScript.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScriptRunner.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightStartup.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/Startup.js create mode 100644 nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt index cf799f08a..f584b2d60 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt @@ -31,6 +31,9 @@ data class CommandLine internal constructor( val pid get() = process?.pid() ?: -1L + val exitCode + get() = process?.takeIf { !it.isAlive }?.exitValue() ?: -1 + val writer = PrintStream(object : OutputStream() { override fun write(b: Int) { @@ -84,9 +87,6 @@ data class CommandLine internal constructor( @Synchronized fun stop() { - process?.destroyForcibly() - process = null - waiter?.interrupt() waiter = null @@ -95,6 +95,10 @@ data class CommandLine internal constructor( errorReader?.interrupt() errorReader = null + + process?.destroyForcibly() + process?.waitFor() + process = null } fun get(timeout: Duration): Int { @@ -139,7 +143,7 @@ data class CommandLine internal constructor( private inner class StreamLineReader( stream: InputStream, private val isError: Boolean, - ) : Thread("Command Line Stream Line Reader") { + ) : Thread("Command Line ${if (isError) "Error" else "Input"} Stream Line Reader") { private val reader = stream.bufferedReader() private val completable = CompletableFuture() diff --git a/nebulosa-pixinsight/build.gradle.kts b/nebulosa-pixinsight/build.gradle.kts index fe459eb39..717e48918 100644 --- a/nebulosa-pixinsight/build.gradle.kts +++ b/nebulosa-pixinsight/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { api(project(":nebulosa-star-detection")) api(project(":nebulosa-livestacking")) implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-test")) } publishing { diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightIsRunning.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightIsRunning.kt new file mode 100644 index 000000000..c03d9dec2 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightIsRunning.kt @@ -0,0 +1,64 @@ +package nebulosa.pixinsight + +import nebulosa.log.debug +import nebulosa.log.loggerFor +import java.util.concurrent.atomic.AtomicBoolean + +data class PixInsightIsRunning(private val slot: Int = 0) : PixInsightScript { + + override val arguments = listOf(if (slot > 0) "-y=$slot" else "-y") + + private val completed = AtomicBoolean() + private val running = AtomicBoolean() + + private val slotIsNotRunning = "The requested application instance #$slot is not running" + private val slotCrashed = "The requested application instance #$slot has crashed" + private val yieldedExecutionInstance = "$YIELDED_EXECUTION_INSTANCE$slot" + + override fun onInputRead(line: String) { + processLine(line) + } + + override fun onErrorRead(line: String) { + processLine(line) + } + + private fun processLine(line: String) { + if (completed.get()) return + + if (slot > 0) { + if (line.contains(slotIsNotRunning, true) || line.contains(slotCrashed, true)) { + completeWith(false) + } else if (line.contains(yieldedExecutionInstance, true)) { + completeWith(true) + } else { + return + } + } else if (line.contains(YIELDED_EXECUTION_INSTANCE, true)) { + completeWith(true) + } else if (line.contains(NO_RUNNING_PROCESS, true)) { + completeWith(false) + } else { + return + } + + LOG.debug { line } + } + + private fun completeWith(running: Boolean) { + this.running.set(running) + completed.set(true) + } + + override fun processOnComplete(pid: Long, exitCode: Int): Boolean { + return exitCode == 0 && completed.get() && running.get() + } + + companion object { + + private const val NO_RUNNING_PROCESS = "There is no running (and alive) instance of the PixInsight" + private const val YIELDED_EXECUTION_INSTANCE = "Yielded execution to running application instance #" + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScript.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScript.kt new file mode 100644 index 000000000..7145d9482 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScript.kt @@ -0,0 +1,26 @@ +package nebulosa.pixinsight + +import nebulosa.common.exec.CommandLine +import nebulosa.common.exec.LineReadListener +import java.util.concurrent.CompletableFuture + +interface PixInsightScript : LineReadListener { + + val arguments: List + + override fun onInputRead(line: String) = Unit + + override fun onErrorRead(line: String) = Unit + + fun beforeRun(commandLine: CommandLine, result: CompletableFuture) = Unit + + fun processOnComplete(pid: Long, exitCode: Int): T + + fun run(runner: PixInsightScriptRunner): CompletableFuture { + return runner.run(this) + } + + fun runSync(runner: PixInsightScriptRunner): T { + return run(runner).get() + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScriptRunner.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScriptRunner.kt new file mode 100644 index 000000000..f502bdc5c --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScriptRunner.kt @@ -0,0 +1,57 @@ +package nebulosa.pixinsight + +import nebulosa.common.exec.CommandLine +import nebulosa.common.exec.commandLine +import java.nio.file.Path +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap + +data class PixInsightScriptRunner(private val executablePath: Path) { + + private val runningScripts = ConcurrentHashMap, CommandLine>() + + @Synchronized + fun run(script: PixInsightScript): CompletableFuture { + require(!runningScripts.containsKey(script)) { "script is already running" } + + val result = CompletableFuture() + + val commandLine = commandLine { + executablePath(executablePath) + script.arguments.forEach(::putArg) + DEFAULT_ARGS.forEach(::putArg) + } + + commandLine.whenComplete { exitCode, exception -> + try { + if (result.isDone) return@whenComplete + else if (exception != null) result.completeExceptionally(exception) + else result.complete(script.processOnComplete(commandLine.pid, exitCode)) + } finally { + commandLine.unregisterLineReadListener(script) + runningScripts.remove(script) + } + } + + commandLine.registerLineReadListener(script) + script.beforeRun(commandLine, result) + + if (!commandLine.isDone) { + runningScripts[script] = commandLine + commandLine.start() + } else { + result.complete(script.processOnComplete(commandLine.pid, commandLine.exitCode)) + } + + return result + } + + fun stop(script: PixInsightScript<*>) { + runningScripts[script]?.stop() + } + + companion object { + + @JvmStatic private val DEFAULT_ARGS = arrayOf("--automation-mode", "--no-startup-scripts") + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightStartup.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightStartup.kt new file mode 100644 index 000000000..50aedb176 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightStartup.kt @@ -0,0 +1,55 @@ +package nebulosa.pixinsight + +import nebulosa.common.exec.CommandLine +import nebulosa.io.resource +import nebulosa.io.transferAndClose +import java.nio.file.Files +import java.util.concurrent.CompletableFuture +import kotlin.concurrent.thread +import kotlin.io.path.outputStream +import kotlin.io.path.readText + +data class PixInsightStartup(private val slot: Int = SLOT) : PixInsightScript { + + private val scriptPath = Files.createTempFile("pi-", ".js") + private val outputPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/Startup.js")!!.transferAndClose(scriptPath.outputStream()) + + scriptPath.toFile().deleteOnExit() + outputPath.toFile().deleteOnExit() + } + + override val arguments = listOf("-r=\"$scriptPath,$outputPath\"", if (slot > 0) "-n=$slot" else "-n") + + override fun beforeRun(commandLine: CommandLine, result: CompletableFuture) { + thread(isDaemon = true, name = "Startup Path Watcher") { + var count = 0 + + try { + while (count++ < 60) { + Thread.sleep(500) + + if (outputPath.readText() == "STARTED") { + result.complete(true) + return@thread + } + } + + result.complete(false) + } catch (e: Throwable) { + result.completeExceptionally(e) + } + } + } + + override fun processOnComplete(pid: Long, exitCode: Int): Boolean { + return exitCode == 0 + } + + companion object { + + const val SLOT = 256 + } +} diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Startup.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Startup.js new file mode 100644 index 000000000..3d9a7c5eb --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Startup.js @@ -0,0 +1 @@ +File.writeTextFile(jsArguments[0], "STARTED") diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt new file mode 100644 index 000000000..12e4496f0 --- /dev/null +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -0,0 +1,23 @@ +import io.kotest.core.annotation.EnabledIf +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.booleans.shouldBeTrue +import nebulosa.pixinsight.PixInsightIsRunning +import nebulosa.pixinsight.PixInsightScriptRunner +import nebulosa.pixinsight.PixInsightStartup +import nebulosa.test.NonGitHubOnlyCondition +import java.nio.file.Path + +@EnabledIf(NonGitHubOnlyCondition::class) +class PixInsightScriptTest : StringSpec() { + + init { + val runner = PixInsightScriptRunner(Path.of("PixInsight")) + + "startup" { + PixInsightStartup().runSync(runner).shouldBeTrue() + } + "is running" { + PixInsightIsRunning().runSync(runner).shouldBeTrue() + } + } +} From f3523cd3240e1f8303ac44cd816b3492464749a0 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 4 Jun 2024 13:35:42 -0300 Subject: [PATCH 03/26] [api]: Implement PixInsight Image Calibration --- .../nebulosa/pixinsight/PixInsightScript.kt | 26 ----- .../pixinsight/PixInsightScriptRunner.kt | 57 ----------- .../nebulosa/pixinsight/PixInsightStartup.kt | 55 ----------- .../livestacking/PixInsightLiveStacker.kt | 57 +++++++++++ .../script/AbstractPixInsightScript.kt | 34 +++++++ .../pixinsight/script/PixInsightCalibrate.kt | 60 ++++++++++++ .../{ => script}/PixInsightIsRunning.kt | 27 ++---- .../pixinsight/script/PixInsightScript.kt | 31 ++++++ .../script/PixInsightScriptRunner.kt | 26 +++++ .../pixinsight/script/PixInsightStartup.kt | 44 +++++++++ .../main/resources/pixinsight/Calibrate.js | 95 +++++++++++++++++++ .../src/test/kotlin/PixInsightScriptTest.kt | 20 ++-- .../nebulosa/test/AbstractFitsAndXisfTest.kt | 14 +++ 13 files changed, 383 insertions(+), 163 deletions(-) delete mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScript.kt delete mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScriptRunner.kt delete mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightStartup.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt rename nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/{ => script}/PixInsightIsRunning.kt (66%) create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScript.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScript.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScript.kt deleted file mode 100644 index 7145d9482..000000000 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScript.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nebulosa.pixinsight - -import nebulosa.common.exec.CommandLine -import nebulosa.common.exec.LineReadListener -import java.util.concurrent.CompletableFuture - -interface PixInsightScript : LineReadListener { - - val arguments: List - - override fun onInputRead(line: String) = Unit - - override fun onErrorRead(line: String) = Unit - - fun beforeRun(commandLine: CommandLine, result: CompletableFuture) = Unit - - fun processOnComplete(pid: Long, exitCode: Int): T - - fun run(runner: PixInsightScriptRunner): CompletableFuture { - return runner.run(this) - } - - fun runSync(runner: PixInsightScriptRunner): T { - return run(runner).get() - } -} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScriptRunner.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScriptRunner.kt deleted file mode 100644 index f502bdc5c..000000000 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightScriptRunner.kt +++ /dev/null @@ -1,57 +0,0 @@ -package nebulosa.pixinsight - -import nebulosa.common.exec.CommandLine -import nebulosa.common.exec.commandLine -import java.nio.file.Path -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap - -data class PixInsightScriptRunner(private val executablePath: Path) { - - private val runningScripts = ConcurrentHashMap, CommandLine>() - - @Synchronized - fun run(script: PixInsightScript): CompletableFuture { - require(!runningScripts.containsKey(script)) { "script is already running" } - - val result = CompletableFuture() - - val commandLine = commandLine { - executablePath(executablePath) - script.arguments.forEach(::putArg) - DEFAULT_ARGS.forEach(::putArg) - } - - commandLine.whenComplete { exitCode, exception -> - try { - if (result.isDone) return@whenComplete - else if (exception != null) result.completeExceptionally(exception) - else result.complete(script.processOnComplete(commandLine.pid, exitCode)) - } finally { - commandLine.unregisterLineReadListener(script) - runningScripts.remove(script) - } - } - - commandLine.registerLineReadListener(script) - script.beforeRun(commandLine, result) - - if (!commandLine.isDone) { - runningScripts[script] = commandLine - commandLine.start() - } else { - result.complete(script.processOnComplete(commandLine.pid, commandLine.exitCode)) - } - - return result - } - - fun stop(script: PixInsightScript<*>) { - runningScripts[script]?.stop() - } - - companion object { - - @JvmStatic private val DEFAULT_ARGS = arrayOf("--automation-mode", "--no-startup-scripts") - } -} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightStartup.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightStartup.kt deleted file mode 100644 index 50aedb176..000000000 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightStartup.kt +++ /dev/null @@ -1,55 +0,0 @@ -package nebulosa.pixinsight - -import nebulosa.common.exec.CommandLine -import nebulosa.io.resource -import nebulosa.io.transferAndClose -import java.nio.file.Files -import java.util.concurrent.CompletableFuture -import kotlin.concurrent.thread -import kotlin.io.path.outputStream -import kotlin.io.path.readText - -data class PixInsightStartup(private val slot: Int = SLOT) : PixInsightScript { - - private val scriptPath = Files.createTempFile("pi-", ".js") - private val outputPath = Files.createTempFile("pi-", ".txt") - - init { - resource("pixinsight/Startup.js")!!.transferAndClose(scriptPath.outputStream()) - - scriptPath.toFile().deleteOnExit() - outputPath.toFile().deleteOnExit() - } - - override val arguments = listOf("-r=\"$scriptPath,$outputPath\"", if (slot > 0) "-n=$slot" else "-n") - - override fun beforeRun(commandLine: CommandLine, result: CompletableFuture) { - thread(isDaemon = true, name = "Startup Path Watcher") { - var count = 0 - - try { - while (count++ < 60) { - Thread.sleep(500) - - if (outputPath.readText() == "STARTED") { - result.complete(true) - return@thread - } - } - - result.complete(false) - } catch (e: Throwable) { - result.completeExceptionally(e) - } - } - } - - override fun processOnComplete(pid: Long, exitCode: Int): Boolean { - return exitCode == 0 - } - - companion object { - - const val SLOT = 256 - } -} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt new file mode 100644 index 000000000..a1e7c6e6d --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt @@ -0,0 +1,57 @@ +package nebulosa.pixinsight.livestacking + +import nebulosa.livestacking.LiveStacker +import nebulosa.pixinsight.script.PixInsightIsRunning +import nebulosa.pixinsight.script.PixInsightScript +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.pixinsight.script.PixInsightStartup +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +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 use32Bits: Boolean = false, +) : LiveStacker { + + private val running = AtomicBoolean() + + override val isRunning + get() = running.get() + + override val isStacking: Boolean + get() = TODO("Not yet implemented") + + @Synchronized + override fun start() { + val isPixInsightRunning = PixInsightIsRunning(PixInsightScript.DEFAULT_SLOT).runSync(runner) + + if (!isPixInsightRunning) { + try { + check(PixInsightStartup(PixInsightScript.DEFAULT_SLOT).runSync(runner, 30, TimeUnit.SECONDS)) + } catch (e: Throwable) { + throw IllegalStateException("Unable to start PixInsight") + } + + running.set(true) + } + } + + @Synchronized + override fun add(path: Path): Path? { + return null + } + + @Synchronized + override fun stop() { + + } + + override fun 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 new file mode 100644 index 000000000..55c12c346 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -0,0 +1,34 @@ +package nebulosa.pixinsight.script + +import nebulosa.common.exec.CommandLine +import nebulosa.common.exec.LineReadListener +import java.util.concurrent.CompletableFuture + +abstract class AbstractPixInsightScript : PixInsightScript, LineReadListener, CompletableFuture() { + + override fun onInputRead(line: String) = Unit + + override fun onErrorRead(line: String) = Unit + + protected open fun beforeRun() = Unit + + protected abstract fun processOnComplete(exitCode: Int): T? + + final override fun run(runner: PixInsightScriptRunner) = runner.run(this) + + final override fun handleCommandLine(commandLine: CommandLine) { + commandLine.whenComplete { exitCode, exception -> + try { + if (isDone) return@whenComplete + else if (exception != null) completeExceptionally(exception) + else complete(processOnComplete(exitCode)) + } finally { + commandLine.unregisterLineReadListener(this) + } + } + + commandLine.registerLineReadListener(this) + beforeRun() + commandLine.start() + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt new file mode 100644 index 000000000..81409660e --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -0,0 +1,60 @@ +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.deleteRecursively +import kotlin.io.path.outputStream +import kotlin.io.path.readText + +data class PixInsightCalibrate( + private val slot: Int, + private val path: Path, + private val dark: Path? = null, + private val flat: Path? = null, + private val bias: Path? = null, + private val compress: Boolean = false, + private val use32Bit: Boolean = false, +) : AbstractPixInsightScript() { + + private val outputDirectory = Files.createTempDirectory("pi-calibrate-") + private val scriptPath = Files.createTempFile("pi-", ".js") + private val outputPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/Calibrate.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = + listOf("-x=\"${if (slot > 0) "$slot:" else ""}$scriptPath,$path,$outputDirectory,$outputPath,$dark,$flat,$bias,$compress,$use32Bit\"") + + override fun processOnComplete(exitCode: Int): Path? { + if (exitCode == 0) { + repeat(5) { + val text = outputPath.readText() + + if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { + return Path.of(text.substring(1, text.length - 1)) + } + + Thread.sleep(1000) + } + } + + return null + } + + override fun close() { + scriptPath.deleteIfExists() + outputPath.deleteIfExists() + outputDirectory.deleteRecursively() + } + + companion object { + + private const val START_FILE = "@" + private const val END_FILE = "#" + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightIsRunning.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightIsRunning.kt similarity index 66% rename from nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightIsRunning.kt rename to nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightIsRunning.kt index c03d9dec2..d1a987ae9 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/PixInsightIsRunning.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightIsRunning.kt @@ -1,16 +1,12 @@ -package nebulosa.pixinsight +package nebulosa.pixinsight.script import nebulosa.log.debug import nebulosa.log.loggerFor -import java.util.concurrent.atomic.AtomicBoolean -data class PixInsightIsRunning(private val slot: Int = 0) : PixInsightScript { +data class PixInsightIsRunning(private val slot: Int) : AbstractPixInsightScript() { override val arguments = listOf(if (slot > 0) "-y=$slot" else "-y") - private val completed = AtomicBoolean() - private val running = AtomicBoolean() - private val slotIsNotRunning = "The requested application instance #$slot is not running" private val slotCrashed = "The requested application instance #$slot has crashed" private val yieldedExecutionInstance = "$YIELDED_EXECUTION_INSTANCE$slot" @@ -24,20 +20,20 @@ data class PixInsightIsRunning(private val slot: Int = 0) : PixInsightScript 0) { if (line.contains(slotIsNotRunning, true) || line.contains(slotCrashed, true)) { - completeWith(false) + complete(false) } else if (line.contains(yieldedExecutionInstance, true)) { - completeWith(true) + complete(true) } else { return } } else if (line.contains(YIELDED_EXECUTION_INSTANCE, true)) { - completeWith(true) + complete(true) } else if (line.contains(NO_RUNNING_PROCESS, true)) { - completeWith(false) + complete(false) } else { return } @@ -45,14 +41,9 @@ data class PixInsightIsRunning(private val slot: Int = 0) : PixInsightScript : Future, Closeable { + + val arguments: Iterable + + fun handleCommandLine(commandLine: CommandLine) + + fun run(runner: PixInsightScriptRunner) + + fun runSync(runner: PixInsightScriptRunner): T { + run(runner) + return get() + } + + fun runSync(runner: PixInsightScriptRunner, timeout: Long, unit: TimeUnit = TimeUnit.MILLISECONDS): T { + run(runner) + return get(timeout, unit) + } + + companion object { + + const val DEFAULT_SLOT = 256 + const val UNSPECIFIED_SLOT = 0 + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt new file mode 100644 index 000000000..52b61b2de --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt @@ -0,0 +1,26 @@ +package nebulosa.pixinsight.script + +import nebulosa.common.exec.commandLine +import nebulosa.log.loggerFor +import java.nio.file.Path + +data class PixInsightScriptRunner(private val executablePath: Path) { + + fun run(script: PixInsightScript<*>) { + val commandLine = commandLine { + executablePath(executablePath) + script.arguments.forEach(::putArg) + DEFAULT_ARGS.forEach(::putArg) + } + + LOG.info("running PixInsight script: {}", commandLine.command) + + script.handleCommandLine(commandLine) + } + + companion object { + + @JvmStatic private val DEFAULT_ARGS = arrayOf("--automation-mode", "--no-startup-scripts") + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt new file mode 100644 index 000000000..b9252d99c --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt @@ -0,0 +1,44 @@ +package nebulosa.pixinsight.script + +import nebulosa.io.resource +import nebulosa.io.transferAndClose +import java.nio.file.Files +import kotlin.concurrent.timer +import kotlin.io.path.deleteIfExists +import kotlin.io.path.outputStream +import kotlin.io.path.readText + +data class PixInsightStartup(private val slot: Int) : AbstractPixInsightScript() { + + private val scriptPath = Files.createTempFile("pi-", ".js") + private val outputPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/Startup.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = listOf("-r=\"$scriptPath,$outputPath\"", if (slot > 0) "-n=$slot" else "-n") + + override fun beforeRun() { + var count = 0 + + timer("PixInsight Startup Timer", true, 1000L, 500L) { + if (outputPath.readText() == "STARTED") { + complete(true) + cancel() + } else if (count >= 60) { + complete(false) + cancel() + } + + count++ + } + } + + override fun processOnComplete(exitCode: Int) = false + + override fun close() { + scriptPath.deleteIfExists() + outputPath.deleteIfExists() + } +} diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js new file mode 100644 index 000000000..d85cf9f3b --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -0,0 +1,95 @@ +function calibrate() { + const targetPath = jsArguments[0] + const outputDirectory = jsArguments[1] + const statusPath = jsArguments[2] + const masterDark = jsArguments[3] + const masterFlat = jsArguments[4] + const masterBias = jsArguments[5] + const compress = jsArguments[6].toLowerCase() === 'true' + const use32Bit = jsArguments[7].toLowerCase() === 'true' + + console.writeln("targetPath=" + targetPath) + console.writeln("outputDirectory=" + outputDirectory) + console.writeln("statusPath=" + statusPath) + console.writeln("masterDark=" + masterDark) + console.writeln("masterFlat=" + masterFlat) + console.writeln("masterBias=" + masterBias) + console.writeln("compress=" + compress) + console.writeln("use32Bit=" + use32Bit) + + const P = new ImageCalibration + + P.targetFrames = [ // enabled, path + [true, targetPath] + ] + P.enableCFA = true + P.cfaPattern = ImageCalibration.prototype.Auto + P.inputHints = "fits-keywords normalize raw cfa signed-is-physical" + P.outputHints = "properties fits-keywords no-compress-data no-embedded-data no-resolution" + P.pedestal = 0 + P.pedestalMode = ImageCalibration.prototype.Keyword + P.pedestalKeyword = "" + P.overscanEnabled = false + P.overscanImageX0 = 0 + P.overscanImageY0 = 0 + P.overscanImageX1 = 0 + P.overscanImageY1 = 0 + P.overscanRegions = [ // enabled, sourceX0, sourceY0, sourceX1, sourceY1, targetX0, targetY0, targetX1, targetY1 + [false, 0, 0, 0, 0, 0, 0, 0, 0], + [false, 0, 0, 0, 0, 0, 0, 0, 0], + [false, 0, 0, 0, 0, 0, 0, 0, 0], + [false, 0, 0, 0, 0, 0, 0, 0, 0] + ] + P.masterBiasEnabled = !!masterBias + P.masterBiasPath = masterBias + P.masterDarkEnabled = !!masterDark + P.masterDarkPath = masterDark + P.masterFlatEnabled = !!masterFlat + P.masterFlatPath = masterFlat + P.calibrateBias = false + P.calibrateDark = false + P.calibrateFlat = false + P.optimizeDarks = false + P.darkOptimizationThreshold = 0.00000 + P.darkOptimizationLow = 3.0000 + P.darkOptimizationWindow = 0 + P.darkCFADetectionMode = ImageCalibration.prototype.DetectCFA + P.separateCFAFlatScalingFactors = true + P.flatScaleClippingFactor = 0.05 + P.evaluateNoise = false + P.noiseEvaluationAlgorithm = ImageCalibration.prototype.NoiseEvaluation_MRS + P.evaluateSignal = false + P.structureLayers = 5 + P.saturationThreshold = 1.00 + P.saturationRelative = false + P.noiseLayers = 1 + P.hotPixelFilterRadius = 1 + P.noiseReductionFilterRadius = 0 + P.minStructureSize = 0 + P.psfType = ImageCalibration.prototype.PSFType_Moffat4 + P.psfGrowth = 1.00 + P.maxStars = 24576 + P.outputDirectory = outputDirectory + P.outputExtension = ".fits" + P.outputPrefix = "" + P.outputPostfix = "_c" + P.outputSampleFormat = use32Bit ? ImageCalibration.prototype.f32 : ImageCalibration.prototype.i16 + P.outputPedestal = 0 + P.outputPedestalMode = ImageCalibration.prototype.OutputPedestal_Literal + P.autoPedestalLimit = 0.00010 + P.overwriteExistingFiles = true + P.onError = ImageCalibration.prototype.Continue + P.noGUIMessages = true + P.useFileThreads = true + P.fileThreadOverload = 1.00 + P.maxFileReadThreads = 0 + P.maxFileWriteThreads = 0 + + P.executeGlobal() + + console.writeln("calibration finished") + + File.writeTextFile(statusPath, "@" + P.outputData[0][0] + "#") +} + +calibrate() diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index 12e4496f0..b5c5f8dc8 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -1,23 +1,29 @@ import io.kotest.core.annotation.EnabledIf -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.booleans.shouldBeTrue -import nebulosa.pixinsight.PixInsightIsRunning -import nebulosa.pixinsight.PixInsightScriptRunner -import nebulosa.pixinsight.PixInsightStartup +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.paths.shouldExist +import nebulosa.pixinsight.script.* +import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.test.NonGitHubOnlyCondition import java.nio.file.Path @EnabledIf(NonGitHubOnlyCondition::class) -class PixInsightScriptTest : StringSpec() { +class PixInsightScriptTest : AbstractFitsAndXisfTest() { init { val runner = PixInsightScriptRunner(Path.of("PixInsight")) "startup" { - PixInsightStartup().runSync(runner).shouldBeTrue() + PixInsightStartup(PixInsightScript.DEFAULT_SLOT) + .use { it.runSync(runner).shouldBeTrue() } } "is running" { - PixInsightIsRunning().runSync(runner).shouldBeTrue() + PixInsightIsRunning(PixInsightScript.DEFAULT_SLOT) + .use { it.runSync(runner).shouldBeTrue() } + } + "calibrate" { + PixInsightCalibrate(PixInsightScript.DEFAULT_SLOT, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) + .use { it.runSync(runner).shouldNotBeNull().shouldExist() } } } } diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt index 7fda567c9..befb2806d 100644 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt @@ -109,6 +109,20 @@ abstract class AbstractFitsAndXisfTest : StringSpec() { protected val STAR_FOCUS_16 by lazy { download("STAR.FOCUS.16.fits", GITHUB_FITS_URL) } protected val STAR_FOCUS_17 by lazy { download("STAR.FOCUS.17.fits", GITHUB_FITS_URL) } + protected val PI_01_LIGHT by lazy { download("PI.01.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_02_LIGHT by lazy { download("PI.02.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_03_LIGHT by lazy { download("PI.03.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_04_LIGHT by lazy { download("PI.04.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_05_LIGHT by lazy { download("PI.05.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_06_LIGHT by lazy { download("PI.06.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_07_LIGHT by lazy { download("PI.07.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_08_LIGHT by lazy { download("PI.08.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_09_LIGHT by lazy { download("PI.09.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_10_LIGHT by lazy { download("PI.10.LIGHT.fits", GITHUB_FITS_URL) } + protected val PI_BIAS by lazy { download("PI.BIAS.fits", GITHUB_FITS_URL) } + protected val PI_DARK by lazy { download("PI.DARK.fits", GITHUB_FITS_URL) } + protected val PI_FLAT by lazy { download("PI.FLAT.fits", GITHUB_FITS_URL) } + private val afterEach = AfterEach() init { From 33d124af4176fb3cf0dd8c05926f829946486bf1 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 4 Jun 2024 14:19:05 -0300 Subject: [PATCH 04/26] [api]: Implement PixInsight Image Alignment --- nebulosa-pixinsight/build.gradle.kts | 1 + .../script/AbstractPixInsightScript.kt | 14 +++ .../pixinsight/script/PixInsightAlign.kt | 84 +++++++++++++ .../pixinsight/script/PixInsightCalibrate.kt | 23 +++- .../src/main/resources/pixinsight/Align.js | 114 ++++++++++++++++++ .../main/resources/pixinsight/Calibrate.js | 6 +- .../src/test/kotlin/PixInsightScriptTest.kt | 7 +- 7 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/Align.js diff --git a/nebulosa-pixinsight/build.gradle.kts b/nebulosa-pixinsight/build.gradle.kts index 717e48918..4c7d645c6 100644 --- a/nebulosa-pixinsight/build.gradle.kts +++ b/nebulosa-pixinsight/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { api(project(":nebulosa-plate-solving")) api(project(":nebulosa-star-detection")) api(project(":nebulosa-livestacking")) + api(libs.bundles.jackson) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) } 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 55c12c346..1976b8eaa 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -1,7 +1,11 @@ package nebulosa.pixinsight.script +import com.fasterxml.jackson.module.kotlin.jsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import nebulosa.common.exec.CommandLine import nebulosa.common.exec.LineReadListener +import nebulosa.common.json.PathDeserializer +import java.nio.file.Path import java.util.concurrent.CompletableFuture abstract class AbstractPixInsightScript : PixInsightScript, LineReadListener, CompletableFuture() { @@ -31,4 +35,14 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen beforeRun() commandLine.start() } + + companion object { + + @JvmStatic private val KOTLIN_MODULE = kotlinModule() + .addDeserializer(Path::class.java, PathDeserializer) + + @JvmStatic internal val OBJECT_MAPPER = jsonMapper { + addModule(KOTLIN_MODULE) + } + } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt new file mode 100644 index 000000000..c8f942aed --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt @@ -0,0 +1,84 @@ +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.deleteRecursively +import kotlin.io.path.outputStream +import kotlin.io.path.readText + +data class PixInsightAlign( + private val slot: Int, + private val referencePath: Path, + private val targetPath: Path, +) : AbstractPixInsightScript() { + + data class Result( + @JvmField val outputImage: Path? = null, + @JvmField val outputMaskImage: Path? = null, + @JvmField val totalPairMatches: Int = 0, + @JvmField val inliers: Int = 0, + @JvmField val overlapping: Int = 0, + @JvmField val regularity: Double = 0.0, + @JvmField val quality: Double = 0.0, + @JvmField val rmsError: Double = 0.0, + @JvmField val rmsErrorDev: Double = 0.0, + @JvmField val peakErrorX: Double = 0.0, + @JvmField val peakErrorY: Double = 0.0, + @JvmField val h11: Double = 0.0, + @JvmField val h12: Double = 0.0, + @JvmField val h13: Double = 0.0, + @JvmField val h21: Double = 0.0, + @JvmField val h22: Double = 0.0, + @JvmField val h23: Double = 0.0, + @JvmField val h31: Double = 0.0, + @JvmField val h32: Double = 0.0, + @JvmField val h33: Double = 0.0, + ) { + + companion object { + + @JvmStatic val FAILED = Result() + } + } + + private val outputDirectory = Files.createTempDirectory("pi-align-") + private val scriptPath = Files.createTempFile("pi-", ".js") + private val outputPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/Align.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = listOf("-x=\"${if (slot > 0) "$slot:" else ""}$scriptPath,$referencePath,$targetPath,$outputDirectory,$outputPath\"") + + override fun processOnComplete(exitCode: Int): Result { + if (exitCode == 0) { + repeat(5) { + val text = outputPath.readText() + + if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) + } + + Thread.sleep(1000) + } + } + + return Result.FAILED + } + + override fun close() { + scriptPath.deleteIfExists() + outputPath.deleteIfExists() + outputDirectory.deleteRecursively() + } + + companion object { + + private const val START_FILE = "@" + private const val END_FILE = "#" + } +} 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 81409660e..8844434a5 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -11,13 +11,23 @@ import kotlin.io.path.readText data class PixInsightCalibrate( private val slot: Int, - private val path: Path, + private val targetPath: Path, private val dark: Path? = null, private val flat: Path? = null, private val bias: Path? = null, private val compress: Boolean = false, private val use32Bit: Boolean = false, -) : AbstractPixInsightScript() { +) : AbstractPixInsightScript() { + + data class Result( + @JvmField val outputImage: Path? = null, + ) { + + companion object { + + @JvmStatic val FAILED = Result() + } + } private val outputDirectory = Files.createTempDirectory("pi-calibrate-") private val scriptPath = Files.createTempFile("pi-", ".js") @@ -28,22 +38,22 @@ data class PixInsightCalibrate( } override val arguments = - listOf("-x=\"${if (slot > 0) "$slot:" else ""}$scriptPath,$path,$outputDirectory,$outputPath,$dark,$flat,$bias,$compress,$use32Bit\"") + listOf("-x=\"${if (slot > 0) "$slot:" else ""}$scriptPath,$targetPath,$outputDirectory,$outputPath,$dark,$flat,$bias,$compress,$use32Bit\"") - override fun processOnComplete(exitCode: Int): Path? { + override fun processOnComplete(exitCode: Int): Result { if (exitCode == 0) { repeat(5) { val text = outputPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { - return Path.of(text.substring(1, text.length - 1)) + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) } Thread.sleep(1000) } } - return null + return Result.FAILED } override fun close() { @@ -56,5 +66,6 @@ data class PixInsightCalibrate( private const val START_FILE = "@" private const val END_FILE = "#" + } } diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js new file mode 100644 index 000000000..6638b4b23 --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js @@ -0,0 +1,114 @@ +function alignment() { + const referencePath = jsArguments[0] + const targetPath = jsArguments[1] + const outputDirectory = jsArguments[2] + const statusPath = jsArguments[3] + + console.writeln("referencePath=" + referencePath) + console.writeln("targetPath=" + targetPath) + console.writeln("outputDirectory=" + outputDirectory) + console.writeln("statusPath=" + statusPath) + + var P = new StarAlignment + P.structureLayers = 5 + P.noiseLayers = 0 + P.hotPixelFilterRadius = 1 + P.noiseReductionFilterRadius = 0 + P.minStructureSize = 0 + P.sensitivity = 0.50 + P.peakResponse = 0.50 + P.brightThreshold = 3.00 + P.maxStarDistortion = 0.60 + P.allowClusteredSources = false + P.localMaximaDetectionLimit = 0.75 + P.upperLimit = 1.000 + P.invert = false + P.distortionModel = "" + P.undistortedReference = false + P.distortionCorrection = false + P.distortionMaxIterations = 20 + P.distortionMatcherExpansion = 1.00 + P.splineOrder = 2 + P.splineSmoothness = 0.005 + P.matcherTolerance = 0.0500 + P.ransacMaxIterations = 2000 + P.ransacMaximizeInliers = 1.00 + P.ransacMaximizeOverlapping = 1.00 + P.ransacMaximizeRegularity = 1.00 + P.ransacMinimizeError = 1.00 + P.maxStars = 0 + P.fitPSF = StarAlignment.prototype.FitPSF_DistortionOnly + P.psfTolerance = 0.50 + P.useTriangles = false + P.polygonSides = 5 + P.descriptorsPerStar = 20 + P.restrictToPreviews = true + P.intersection = StarAlignment.prototype.MosaicOnly + P.useBrightnessRelations = false + P.useScaleDifferences = false + P.scaleTolerance = 0.100 + P.referenceImage = referencePath + P.referenceIsFile = true + P.targets = [ // enabled, isFile, image + [true, true, targetPath] + ] + P.inputHints = "" + P.outputHints = "" + P.mode = StarAlignment.prototype.RegisterMatch + P.writeKeywords = true + P.generateMasks = false + P.generateDrizzleData = false + P.generateDistortionMaps = false + P.inheritAstrometricSolution = false + P.frameAdaptation = false + P.randomizeMosaic = false + P.pixelInterpolation = StarAlignment.prototype.Auto + P.clampingThreshold = 0.30 + P.outputDirectory = outputDirectory + P.outputExtension = ".fits" + P.outputPrefix = "" + P.outputPostfix = "_sa" + P.maskPostfix = "_m" + P.distortionMapPostfix = "_dm" + P.outputSampleFormat = StarAlignment.prototype.SameAsTarget + P.overwriteExistingFiles = true + P.onError = StarAlignment.prototype.Continue + P.useFileThreads = true + P.noGUIMessages = true + P.fileThreadOverload = 1.00 + P.maxFileReadThreads = 0 + P.maxFileWriteThreads = 0 + P.memoryLoadControl = true + P.memoryLoadLimit = 0.85 + + P.executeGlobal() + + console.writeln("alignment finished") + + const json = { + outputImage: P.outputData[0][0] || null, + outputMaskImage: P.outputData[0][1] || null, + totalPairMatches: P.outputData[0][2], + inliers: P.outputData[0][3], + overlapping: P.outputData[0][4], + regularity: P.outputData[0][5], + quality: P.outputData[0][6], + rmsError: P.outputData[0][7], + rmsErrorDev: P.outputData[0][8], + peakErrorX: P.outputData[0][9], + peakErrorY: P.outputData[0][10], + h11: P.outputData[0][11], + h12: P.outputData[0][12], + h13: P.outputData[0][13], + h21: P.outputData[0][14], + h22: P.outputData[0][15], + h23: P.outputData[0][16], + h31: P.outputData[0][17], + h32: P.outputData[0][18], + h33: P.outputData[0][19], + } + + File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") +} + +alignment() diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js index d85cf9f3b..87e6f6ff3 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -89,7 +89,11 @@ function calibrate() { console.writeln("calibration finished") - File.writeTextFile(statusPath, "@" + P.outputData[0][0] + "#") + const json = { + outputImage: P.outputData[0][0] || null, + } + + File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") } calibrate() diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index b5c5f8dc8..7ae6fc8c2 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -23,7 +23,12 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { } "calibrate" { PixInsightCalibrate(PixInsightScript.DEFAULT_SLOT, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) - .use { it.runSync(runner).shouldNotBeNull().shouldExist() } + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().shouldExist() } + } + "align" { + PixInsightAlign(PixInsightScript.DEFAULT_SLOT, PI_01_LIGHT, PI_02_LIGHT) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().shouldExist() } + } } } From 5e73190c0f39975286feae7f7a586e20d3d9065a Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 4 Jun 2024 17:49:08 -0300 Subject: [PATCH 05/26] [api]: Implement PixInsight Star Detection --- .../script/AbstractPixInsightScript.kt | 23 + .../pixinsight/script/PixInsightAlign.kt | 8 +- .../pixinsight/script/PixInsightCalibrate.kt | 9 +- .../script/PixInsightDetectStars.kt | 73 ++ .../pixinsight/script/PixInsightStartup.kt | 2 +- .../star/detection/PixInsightStarDetector.kt | 18 + .../main/resources/pixinsight/Calibrate.js | 6 +- .../main/resources/pixinsight/DetectStars.js | 852 ++++++++++++++++++ .../src/test/kotlin/PixInsightScriptTest.kt | 21 +- .../test/kotlin/PixInsightStarDetectorTest.kt | 16 + .../nebulosa/test/AbstractFitsAndXisfTest.kt | 6 + 11 files changed, 1013 insertions(+), 21 deletions(-) create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/star/detection/PixInsightStarDetector.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js create mode 100644 nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt 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 1976b8eaa..8ce7461cd 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -38,11 +38,34 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen companion object { + internal const val START_FILE = "@" + internal const val END_FILE = "#" + @JvmStatic private val KOTLIN_MODULE = kotlinModule() .addDeserializer(Path::class.java, PathDeserializer) @JvmStatic internal val OBJECT_MAPPER = jsonMapper { addModule(KOTLIN_MODULE) } + + @JvmStatic + internal fun parameterize(slot: Int, scriptPath: Path, vararg parameters: Any?): String { + return buildString { + if (slot > 0) append("$slot:") + + append("\"$scriptPath,") + + parameters.forEachIndexed { i, parameter -> + if (i > 0) append(',') + + if (parameter is Path) append("'$parameter'") + else if (parameter is CharSequence) append("'$parameter'") + else if (parameter != null) append("$parameter") + else append('0') + } + + append('"') + } + } } } 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 c8f942aed..2a40a49bc 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt @@ -52,7 +52,7 @@ data class PixInsightAlign( resource("pixinsight/Align.js")!!.transferAndClose(scriptPath.outputStream()) } - override val arguments = listOf("-x=\"${if (slot > 0) "$slot:" else ""}$scriptPath,$referencePath,$targetPath,$outputDirectory,$outputPath\"") + override val arguments = listOf("-x=${parameterize(slot, scriptPath, referencePath, targetPath, outputDirectory, outputPath)}") override fun processOnComplete(exitCode: Int): Result { if (exitCode == 0) { @@ -75,10 +75,4 @@ data class PixInsightAlign( outputPath.deleteIfExists() outputDirectory.deleteRecursively() } - - companion object { - - private const val START_FILE = "@" - private const val END_FILE = "#" - } } 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 8844434a5..b8e781707 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -38,7 +38,7 @@ data class PixInsightCalibrate( } override val arguments = - listOf("-x=\"${if (slot > 0) "$slot:" else ""}$scriptPath,$targetPath,$outputDirectory,$outputPath,$dark,$flat,$bias,$compress,$use32Bit\"") + listOf("-x=${parameterize(slot, scriptPath, targetPath, outputDirectory, outputPath, dark, flat, bias, compress, use32Bit)}") override fun processOnComplete(exitCode: Int): Result { if (exitCode == 0) { @@ -61,11 +61,4 @@ data class PixInsightCalibrate( outputPath.deleteIfExists() outputDirectory.deleteRecursively() } - - companion object { - - private const val START_FILE = "@" - private const val END_FILE = "#" - - } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt new file mode 100644 index 000000000..fa6fceba3 --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt @@ -0,0 +1,73 @@ +package nebulosa.pixinsight.script + +import nebulosa.io.resource +import nebulosa.io.transferAndClose +import nebulosa.star.detection.ImageStar +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 PixInsightDetectStars( + private val slot: Int, + private val targetPath: Path, + private val minSNR: Double = 0.0, + private val invert: Boolean = false, +) : AbstractPixInsightScript() { + + @Suppress("ArrayInDataClass") + data class Result(@JvmField val stars: Array = emptyArray()) { + + companion object { + + @JvmStatic val FAILED = Result() + } + } + + data class Star( + override val x: Double = 0.0, + override val y: Double = 0.0, + override val flux: Double = 0.0, + @JvmField val size: Double = 0.0, + @JvmField val bkg: Double = 0.0, + @JvmField val x0: Int = 0, + @JvmField val y0: Int = 0, + @JvmField val x1: Int = 0, + @JvmField val y1: Int = 0, + @JvmField val nmax: Int = 0, + override val snr: Double = 0.0, + @JvmField val peak: Double = 0.0, + override val hfd: Double = 0.0, + ) : ImageStar + + private val scriptPath = Files.createTempFile("pi-", ".js") + private val outputPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/DetectStars.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = listOf("-x=${parameterize(slot, scriptPath, targetPath, outputPath, minSNR, invert)}") + + override fun processOnComplete(exitCode: Int): Result { + if (exitCode == 0) { + repeat(5) { + val text = outputPath.readText() + + if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) + } + + Thread.sleep(1000) + } + } + + return Result.FAILED + } + + override fun close() { + scriptPath.deleteIfExists() + outputPath.deleteIfExists() + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt index b9252d99c..ef009556f 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt @@ -17,7 +17,7 @@ data class PixInsightStartup(private val slot: Int) : AbstractPixInsightScript 0) "-n=$slot" else "-n") + override val arguments = listOf("-r=${parameterize(0, scriptPath, outputPath)}", if (slot > 0) "-n=$slot" else "-n") override fun beforeRun() { var count = 0 diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/star/detection/PixInsightStarDetector.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/star/detection/PixInsightStarDetector.kt new file mode 100644 index 000000000..c423a363a --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/star/detection/PixInsightStarDetector.kt @@ -0,0 +1,18 @@ +package nebulosa.pixinsight.star.detection + +import nebulosa.pixinsight.script.PixInsightDetectStars +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.star.detection.ImageStar +import nebulosa.star.detection.StarDetector +import java.nio.file.Path + +data class PixInsightStarDetector( + private val runner: PixInsightScriptRunner, + private val slot: Int, + private val minSNR: Double = 0.0, +) : StarDetector { + + override fun detect(input: Path): List { + return PixInsightDetectStars(slot, input, minSNR).use { it.runSync(runner).stars.toList() } + } +} diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js index 87e6f6ff3..4ac72ab8d 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -40,11 +40,11 @@ function calibrate() { [false, 0, 0, 0, 0, 0, 0, 0, 0], [false, 0, 0, 0, 0, 0, 0, 0, 0] ] - P.masterBiasEnabled = !!masterBias + P.masterBiasEnabled = masterBias !== "0" P.masterBiasPath = masterBias - P.masterDarkEnabled = !!masterDark + P.masterDarkEnabled = masterDark !== "0" P.masterDarkPath = masterDark - P.masterFlatEnabled = !!masterFlat + P.masterFlatEnabled = masterFlat !== "0" P.masterFlatPath = masterFlat P.calibrateBias = false P.calibrateDark = false diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js new file mode 100644 index 000000000..721fd5b22 --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js @@ -0,0 +1,852 @@ +// ____ __ _____ ____ +// / __ \ / // ___/ / __ \ +// / /_/ /__ / / \__ \ / /_/ / +// / ____// /_/ / ___/ // _, _/ PixInsight JavaScript Runtime +// /_/ \____/ /____//_/ |_| PJSR Version 1.0 +// ---------------------------------------------------------------------------- +// pjsr/StarDetector.jsh - Released 2024-02-28T16:25:35Z +// ---------------------------------------------------------------------------- +// This file is part of the PixInsight JavaScript Runtime (PJSR). +// PJSR is an ECMA-262-5 compliant framework for development of scripts on the +// PixInsight platform. +// +// Copyright (c) 2003-2024 Pleiades Astrophoto S.L. All Rights Reserved. +// +// Redistribution and use in both source and binary forms, with or without +// modification, is permitted provided that the following conditions are met: +// +// 1. All redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. All redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// 3. Neither the names "PixInsight" and "Pleiades Astrophoto", nor the names +// of their contributors, may be used to endorse or promote products derived +// from this software without specific prior written permission. For written +// permission, please contact info@pixinsight.com. +// +// 4. All products derived from this software, in any form whatsoever, must +// reproduce the following acknowledgment in the end-user documentation +// and/or other materials provided with the product: +// +// "This product is based on software from the PixInsight project, developed +// by Pleiades Astrophoto and its contributors (https://pixinsight.com/)." +// +// Alternatively, if that is where third-party acknowledgments normally +// appear, this acknowledgment must be reproduced in the product itself. +// +// THIS SOFTWARE IS PROVIDED BY PLEIADES ASTROPHOTO AND ITS CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL PLEIADES ASTROPHOTO OR ITS +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, BUSINESS +// INTERRUPTION; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; AND LOSS OF USE, +// DATA OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// ---------------------------------------------------------------------------- + +#include +#include + +function Star( pos, flux, bkg, rect, size, nmax, snr, peak ) +{ + // Centroid position in pixels, image coordinates. This property is an + // object with x and y Number properties. + this.pos = pos + // Total flux, normalized intensity units. + this.flux = flux + // Mean local background, normalized intensity units. + this.bkg = bkg + // Detection region, image coordinates. + this.rect = rect + // Area of detected star structure in square pixels. + this.size = size + // Number of local maxima in the detection structure. A value greater than + // one denotes a double/multiple star or a crowded source. A value of zero + // signals that detection of local maxima has been disabled, either globally + // or for this particular structure. + this.nmax = nmax + + this.snr = snr + this.peak = peak +} + +function StarDetector() +{ + this.__base__ = Object + this.__base__() + + /* + * Number of wavelet layers for structure detection. (default=5) + */ + this.structureLayers = 5 + + /* + * Half size in pixels of a morphological median filter, for hot pixel + * removal. (default=1) + */ + this.hotPixelFilterRadius = 1 + + /* + * Whether the hot pixel filter removal should be applied to the image used + * for star detection, or only to the working image used to build the + * structure map. (default=false) + * + * By setting this parameter to true, the detection algorithm is completely + * robust to hot pixels (of sizes not larger than hotPixelFilterRadius), but + * it is also less sensitive, so less stars will in general be detected. + * With the default value of false, some hot pixels may be wrongly detected + * as stars but the number of true stars detected will generally be larger. + */ + this.applyHotPixelFilterToDetectionImage = false + + /* + * Half size in pixels of a Gaussian convolution filter applied for noise + * reduction. Useful for star detection in low-SNR images. (default=0) + * + * N.B. Setting the value of this parameter > 0 implies + * applyHotPixelFilterToDetectionImage=true. + */ + this.noiseReductionFilterRadius = 0 + + /* + * Sensitivity of the star detection device. + * + * Internally, the sensitivity of the star detection algorithm is expressed + * in signal-to-noise ratio units with respect to the evaluated dispersion + * of local background pixels for each detected structure. Given a source + * with estimated brightness s, local background b and local background + * dispersion n, sensitivity is the minimum value of (s - b)/n necessary to + * trigger star detection. + * + * To isolate the public interface of this class from its internal + * implementation, this parameter is normalized to the [0,1] range, where 0 + * and 1 represent minimum and maximum sensitivity, respectively. This + * abstraction allows us to change the star detection engine without + * breaking dependent tools and processes. + * + * Increase this value to favor detection of fainter stars. Decrease it to + * restrict detection to brighter stars. (default=0.5). + */ + this.sensitivity = 0.5 + + /*! + * Peak sensitivity of the star detection device. + * + * Internally, the peak response property of the star detection algorithm is + * expressed in kurtosis units. For each detected structure, kurtosis is + * evaluated from all significant pixels with values greater than the + * estimated mean local background. Peak response is the minimum value of + * kurtosis necessary to trigger star detection. + * + * To isolate the public interface of this class from its internal + * implementation, this parameter is normalized to the [0,1] range, where 0 + * and 1 represent minimum and maximum peak response, respectively. This + * abstraction allows us to change the star detection engine without + * breaking dependent tools and processes. + * + * If you decrease this parameter, stars will need to have stronger (or more + * prominent) peaks to be detected. This is useful to prevent detection of + * saturated stars, as well as small nonstellar features. By increasing this + * parameter, the star detection algorithm will be more sensitive to + * 'peakedness', and hence more tolerant with relatively flat image + * features. (default=0.5). + */ + this.peakResponse = 0.5 + + /*! + * If this parameter is false, a local maxima map will be generated to + * identify and prevent detection of multiple sources that are too close to + * be separated as individual structures, such as double and multiple stars. + * In general, barycenter positions cannot be accurately determined for + * sources with several local maxima. If this parameter is true, + * non-separable multiple sources will be detectable as single objects. + * (default=false) + */ + this.allowClusteredSources = false + + /* + * Half size in pixels of the local maxima detection filter. (default=2) + */ + this.localDetectionFilterRadius = 2 + + /*! + * This parameter is a normalized pixel value in the [0,1] range. Structures + * with pixels above this value will be excluded for local maxima detection. + * (default=0.75) + */ + this.localMaximaDetectionLimit = 0.75 + + /* + * Set this flag true to avoid detection of local maxima. (default=false) + * Setting this parameter to true implies allowClusteredSources = true. + */ + this.noLocalMaximaDetection = false + + /*! + * Maximum star distortion. + * + * Internally, star distortion is evaluated in units of coverage of a square + * region circumscribed to each detected structure. The coverage of a + * perfectly circular star is pi/4 (about 0.8). Lower values denote + * elongated or irregular sources. + * + * To isolate the public interface of this class from its internal + * implementation, this parameter is normalized to the [0,1] range, where 0 + * and 1 represent minimum and maximum distortion, respectively. This + * abstraction allows us to change the star detection engine without + * breaking dependent tools and processes. + * + * Use this parameter, if necessary, to control inclusion of elongated + * stars, complex clusters of stars, and nonstellar image features. + * (default=0.6) + */ + this.maxDistortion = 0.6 + + /*! + * Stars with measured SNR above this parameter in units of the minimum + * detection level (as defined by the sensitivity parameter) will always be + * detected, even if their profiles are too flat for the current peak + * response. This allows us to force inclusion of bright stars. (default=3) + */ + this.brightThreshold = 3 + + /* + * Minimum signal-to-noise ratio of a detectable star. + * + * Given a source with estimated brightness s, local background b and local + * background dispersion n, SNR is evaluated as (s - b)/n. Stars with + * measured SNR below this parameter won't be detected. (default=0) + * + * The value of this parameter can be increased to limit star detection to a + * subset of the brightest sources in the image adaptively, instead of + * imposing an arbitrary limit on the number of detected stars. + */ + this.minSNR = 0 + + /*! + * Minimum size of a detectable star structure in square pixels. + * + * This parameter can be used to prevent detection of small and bright image + * artifacts as stars. This can be useful to work with uncalibrated or + * wrongly calibrated data, especially demosaiced CFA frames where hot + * pixels have generated large bright artifacts that cannot be removed with + * a median filter, poorly focused images, and images with poor tracking. + * (default=1) + */ + this.minStructureSize = 1 + + /* + * Stars with peak values greater than this value won't be detected. + * (default=1) + */ + this.upperLimit = 1.0 + + /* + * Detect dark structures over a bright background, instead of bright + * structures over a dark background. (default=false) + */ + this.invert = false + + /* + * Optional callback progress function with the following signature: + * + * Boolean progressCallback( int count, int total ) + * + * If defined, this function will be called by the stars() method for each + * row of its target image. The count argument is the current number of + * processed pixel rows, and total is the height of the target image. If the + * function returns false, the star detection task will be aborted. If the + * function returns true, the task will continue. (default=undefined) + */ + this.progressCallback = undefined + + /* + * Optional mask image. If defined, star detection will be restricted to + * nonzero mask pixels. (default=undefined) + */ + this.mask = undefined + + /* + * Stretch factor for the barycenter search algorithm, in sigma units. + * Increase it to make the algorithm more robust to nearby structures, such + * as multiple/crowded stars and small nebular features. However, too large + * of a stretch factor will make the algorithm less accurate. (default=1.5) + */ + this.xyStretch = 1.5 + + /* + * Square structuring element + */ + function BoxStructure( size ) + { + let B = new Array( size*size ) + for ( let i = 0; i < B.length; ++i ) + B[i] = 1 + let S = new Array + S.push( B ) + return S + } + + /* + * Circular structuring element + */ + function CircularStructure( size ) + { + size |= 1 + let C = new Array( size*size ) + let s2 = size >> 1 + let n2 = size/2 + let n22 = n2*n2 + for ( let i = 0; i < s2; ++i ) + { + let di = i+0.5 - n2 + let di2 = di*di + let i2 = i*size + let i1 = i2 + size - 1 + let i3 = (size - i - 1)*size + let i4 = i3 + size - 1 + for ( let j = 0; j < s2; ++j ) + { + let dj = j+0.5 - n2 + C[i1-j] = C[i2+j] = C[i3+j] = C[i4-j] = (di2 + dj*dj <= n22) ? 1 : 0 + } + } + for ( let i = 0; i < size; ++i ) + C[i*size + s2] = C[s2*size + i] = 1 + let S = new Array + S.push( C ) + return S + } + + /* + * Hot pixel removal with a median filter + */ + this.hotPixelFilter = function( image ) + { + if ( this.hotPixelFilterRadius > 0 ) + if ( this.hotPixelFilterRadius > 1 ) + image.morphologicalTransformation( MorphOp_Median, CircularStructure( 2*this.hotPixelFilterRadius + 1 ) ) + else + image.morphologicalTransformation( MorphOp_Median, BoxStructure( 3 ) ) + } + + /* + * Isolate star detection structures in an image. Replaces the specified map + * image with its binary star detection map. + */ + this.getStructureMap = function( map ) + { + // Flatten the image with a high-pass filter + let s = new Image( map ) + let G = Matrix.gaussianFilterBySize( 1 + (1 << this.structureLayers) ) + s.convolveSeparable( G.rowVector( G.rows >> 1 ), G.rowVector( G.rows >> 1 ) ) + map.apply( s, ImageOp_Sub ) + s.free() + map.truncate() + map.rescale() + + // Strength the smallest structures with a dilation filter + map.morphologicalTransformation( MorphOp_Dilation, BoxStructure( 3 ) ) + + // Adaptive binarization + let m = map.median() + if ( 1 + m == 1 ) + { + // Black background - probably a noiseless synthetic star field + let wasRangeClippingEnabled = map.rangeClippingEnabled + let wasRangeClipLow = map.rangeClipLow + let wasRangeClipHigh = map.rangeClipHigh + map.rangeClippingEnabled = true + map.rangeClipLow = 0 + map.rangeClipHigh = 1 + if ( !wasRangeClippingEnabled || wasRangeClipLow != 0 || wasRangeClipHigh != 1 ) + m = map.median() + map.binarize( m + map.MAD( m ) ) + map.rangeClippingEnabled = wasRangeClippingEnabled + map.rangeClipLow = wasRangeClipLow + map.rangeClipHigh = wasRangeClipHigh + } + else + { + // A "natural" image - binarize at 3*noise_stdDev + let n = map.noiseKSigma( 1 )[0] + map.binarize( m + 3*n ) + } + + // Remove noise residuals with an erosion filter + map.morphologicalTransformation( MorphOp_Erosion, BoxStructure( 3 ) ) + + // Optional star detection mask + if ( this.mask != undefined ) + map.apply( this.mask, ImageOp_Mul ) + } + + /* + * Local maxima detection. + */ + this.getLocalMaximaMap = function( map ) + { + // We apply a dilation filter with a flat structuring element without its + // central element. Local maxima are those pixels in the input image with + // values greater than the dilated image. + // The localMaximaDetectionLimit parameter allows us to prevent detection of + // false multiple maxima on saturated or close to saturated structures. + let Bh = BoxStructure( (this.localDetectionFilterRadius << 1)|1 ) + Bh[0][Bh[0].length >> 1] = 0 + let l = new Image( map ) + l.binarize( this.localMaximaDetectionLimit ) + l.invert() + let t = new Image( map ) + t.morphologicalTransformation( MorphOp_Dilation, Bh ) + map.apply( t, ImageOp_Sub ) + t.free() + map.binarize( 0 ) + map.apply( l, ImageOp_Mul ) + l.free() + } + + /* + * Compute star parameters + */ + this.starParameters = function( image, rect, starPoints, lmMap ) + { + let params = { pos: { x:0, y:0 }, // barycenter image coordinates + rect: { x0:0, y0:0, x1:0, y1:0 }, // detection rectangle + bkg: 0, // local background + sigma: 0, // local background dispersion + flux: 0, // total flux + max: 0, // maximum pixel value + nmax: 0, // number of local maxima in structure + peak: 0, // robust peak value + kurt: 0, // kurtosis + count: 0, // sample length + size: 0 } // structure size in square pixels + + // Mean local background and local background dispersion + for ( let delta = 4, it = 0, m0 = 1;; ++delta, ++it ) + { + let r = rect.inflatedBy( delta ) + let a = [], b = [], c = [], d = [] + image.getSamples( a, new Rect( r.x0, r.y0, r.x1, rect.y0 ) ) + image.getSamples( b, new Rect( r.x0, rect.y0, rect.x0, rect.y1 ) ) + image.getSamples( c, new Rect( r.x0, rect.y1, r.x1, r.y1 ) ) + image.getSamples( d, new Rect( rect.x1, rect.y0, r.x1, rect.y1 ) ) + let B = a.concat( b, c, d ) + let m = Math.median( B ) + if ( m > m0 || (m0 - m)/m0 < 0.01 ) + { + params.bkg = m + params.sigma = Math.max( 1.4826*Math.MAD( B ), Math.EPSILON32 ) + break + } + // Guard us against rare ill-posed conditions + if ( it == 200 ) + return null + m0 = m + } + + // Detection region + params.rect = rect.inflatedBy( 2 ).intersection( image.bounds ) + + // Structure size + params.size = starPoints.length + + // Significant subset + let v = [] + for ( let i = 0; i < starPoints.length; ++i ) + { + let p = starPoints[i] + let f = image.sample( p.x, p.y ) + if ( f > params.bkg ) + { + // Local maxima + if ( !this.noLocalMaximaDetection ) + if ( lmMap.sample( p.x, p.y ) != 0 ) + ++params.nmax + v.push( f ) + // Total flux above local background + params.flux += f + } + } + + // Fail if no significant data + if ( v.length == 0 ) + return null + + // Fail if we have multiple maxima and those are not allowed + if ( params.nmax > 1 ) + if ( !this.allowClusteredSources ) + return null + + // Barycenter coordinates + let M = Matrix.fromImage( image, rect ) + M.truncate( Math.range( M.median() + this.xyStretch*M.stdDev(), 0.0, 1.0 ), 1.0 ) + M.rescale() + let sx = 0, sy = 0, sz = 0 + for ( let y = rect.y0, i = 0; i < M.rows; ++y, ++i ) + for ( let x = rect.x0, j = 0; j < M.cols; ++x, ++j ) + { + let z = M.at( i, j ) + if ( z > 0 ) + { + sx += z*x + sy += z*y + sz += z + } + } + params.pos.x = sx/sz + 0.5 + params.pos.y = sy/sz + 0.5 + + // Sort significant pixels in decreasing flux order + v.sort( (a, b) => (a < b) ? +1 : ((b < a) ? -1 : 0) ) + // Maximum pixel value + params.max = v[0] + + // Find subset of significant high pixel values + let mn = 0 + for ( let i = 0; i < v.length && (mn < 5 || v[i] == v[i-1]); ++i, ++mn ) {} + for ( let i = 0; i < mn; ++i ) + params.peak += v[i] + // Significant peak value + params.peak /= mn + // Significant sample length + params.count = v.length + + // Kurtosis + let s = Math.stdDev( v ) + if ( 1 + s != 1 ) + { + let m = params.flux/v.length + let k = 0 + for ( let i = 0; i < v.length; ++i ) + { + let d = (v[i] - m)/s + d *= d + k += d*d + } + params.kurt = k/params.count + } + + return params + } + + /* + * Finds all the stars in an image. Returns an array of Star objects. + */ + this.stars = function( image ) + { + // We work on a duplicate of the source grayscale image, or on its HSI + // intensity component if it is a color image. + let wrk = Image.newFloatImage() + image.getIntensity( wrk ) + + // Hot pixel removal, if applied to the image where we are going to find + // stars, not just to the image used to build the structure map. + // When noise reduction is enabled, always remove hot pixels first, or + // hot pixels would be promoted to "stars". + let alreadyFixedHotPixels = false + if ( this.applyHotPixelFilterToDetectionImage || this.noiseReductionFilterRadius > 0 ) + { + this.hotPixelFilter( wrk ) + alreadyFixedHotPixels = true + } + + // If the invert flag is set, then we are looking for dark structures on + // a bright background. + if ( this.invert ) + wrk.invert() + + // Optional noise reduction + if ( this.noiseReductionFilterRadius > 0 ) + { + let G = Matrix.gaussianFilterBySize( (this.noiseReductionFilterRadius << 1)|1 ) + wrk.convolveSeparable( G.rowVector( G.rows >> 1 ), G.rowVector( G.rows >> 1 ) ) + } + + // Structure map + let map = Image.newFloatImage() + map.assign( wrk ) + // Hot pixel removal, if applied just to the image used to build the + // structure map. + if ( !alreadyFixedHotPixels ) + this.hotPixelFilter( map ) + this.getStructureMap( map ) + + // Use matrices instead of images for faster access + let M = map.toMatrix() + map.free() + + // Local maxima map + let lmMap + if ( !this.noLocalMaximaDetection ) + { + lmMap = Image.newFloatImage() + lmMap.assign( wrk ) + this.getLocalMaximaMap( lmMap ) + } + + /* + * Internal detection parameters + */ + // Signal detection threshold in local sigma units. + let snrThreshold = 0.1 + 4.8*(1 - Math.range( this.sensitivity, 0, 1 )) + // Peak detection threshold in kurtosis units. + let peakThreshold = 0.1 + 9.8*(1 - Math.range( this.peakResponse, 0, 1 )) + // Maximum distortion in coverage units. + let minCoverage = Math.PI4*(1 - Math.range( this.maxDistortion, 0, 1 )) + + // The detected stars + let S = new Array + + // Structure scanner + for ( let y0 = 0, x1 = M.cols-1, y1 = M.rows-1; y0 < y1; ++y0 ) + { + if ( this.progressCallback != undefined ) + if ( !this.progressCallback( y0, M.rows ) ) + return null + + for ( let x0 = 0; x0 < x1; ++x0 ) + { + // Exclude background pixels and already visited pixels + if ( M.at( y0, x0 ) == 0 ) + continue + + // Star pixel coordinates + let starPoints = new Array + + // Star bounding rectangle + let r = new Rect( x0, y0, x0+1, y0+1 ) + + // Grow star region downward + for ( let y = y0, x = x0, xa, xb; ; ) + { + // Add this pixel to the current star + starPoints.push( { x:x, y:y } ) + + // Explore the left segment of this row + for ( xa = x; xa > 0; ) + { + if ( M.at( y, xa-1 ) == 0 ) + break + --xa + starPoints.push( { x:xa, y:y } ) + } + + // Explore the right segment of this row + for ( xb = x; xb < x1; ) + { + if ( M.at( y, xb+1 ) == 0 ) + break + ++xb + starPoints.push( { x:xb, y:y } ) + } + + // xa and xb are now the left and right boundary limits, + // respectively, of this row in the current star. + + if ( xa < r.x0 ) // update left boundary + r.x0 = xa + + if ( xb >= r.x1 ) // update right boundary + r.x1 = xb + 1 // bottom-right corner excluded (PCL-specific) + + // Prepare for next row + ++y + + // Decide whether we are done with this star now, or if + // there is at least one more row that must be explored. + + let nextRow = false + + // Explore the next row from left to right. We'll continue + // gathering pixels if we find at least one nonzero map pixel. + for ( x = xa; x <= xb; ++x ) + if ( M.at( y, x ) != 0 ) + { + nextRow = true + break + } + + if ( !nextRow ) + break + + // Update bottom boundary + r.y1 = y + 1 // Rect *excludes* the bottom-right corner + + // Terminate if we reach the last row of the image + if ( y == y1 ) + break + } + + /* + * If this is a reliable star, compute its barycenter coordinates + * and add it to the star list. + * + * Rejection criteria: + * + * * Stars whose peak values are greater than the upperLimit + * parameter are rejected. + * + * * If this structure is touching a border of the image, reject + * it. We cannot compute an accurate position for a clipped star. + * + * * Too small structures are rejected. This mainly prevents + * inclusion of hot (or cold) pixels. This condition is enforced + * by the hot pixel removal and noise reduction steps performed + * during the structure detection phase, and optionally by + * increasing the minStructureSize parameter. + * + * * Too large structures are rejected. This prevents inclusion of + * extended nonstellar objects and saturated bright stars. This + * is also part of the structure detection algorithm. + * + * * Too elongated stars are rejected. The minCoverage parameter + * determines the maximum distortion allowed. A perfect square + * has coverage = 1. The coverage of a perfect circle is pi/4. + * + * * Too sparse sources are rejected. This prevents detection of + * multiple stars where centroids cannot be well determined. + * + * * Too dim structures are rejected. The sensitivity parameter + * defines the sensitivity of the star detection algorithm in + * local sigma units. The minSNR parameter can be used to limit + * star detection to a subset of the brightest stars adaptively. + * + * * Too flat structures are rejected. The peakThreshold parameter + * defines the peak sensitivity of the star detection algorithm + * in kurtosis units. + */ + if ( r.width > 1 && r.height > 1 ) + if ( r.y0 > 0 && r.y1 <= y1 && r.x0 > 0 && r.x1 <= x1 ) + if ( starPoints.length >= this.minStructureSize ) + { + let p = this.starParameters( wrk, r, starPoints, lmMap ) + if ( p != null ) + if ( p.max <= this.upperLimit ) + { + let d = Math.max( r.width, r.height ) + if ( p.count/d/d >= minCoverage ) + { + let ix = Math.trunc( p.pos.x )|0 + let iy = Math.trunc( p.pos.y )|0 + if ( this.mask == undefined || this.mask.sample( ix, iy ) != 0 ) + { + let snr = (p.peak - p.bkg)/p.sigma + if ( snr >= this.minSNR ) + { + let s1 = snr/snrThreshold + if ( s1 >= 1 ) + if ( s1 >= this.brightThreshold || p.kurt == 0 || p.kurt/peakThreshold >= 1 ) + S.push( new Star( p.pos, p.flux, p.bkg, p.rect, p.size, p.nmax, snr, p.peak ) ) + } + } + } + } + } + + // Erase this structure. + for ( let i = 0; i < starPoints.length; ++i ) + { + let p = starPoints[i] + M.at( p.y, p.x, 0 ) + } + } + } + + // Sort the list of detected sources in descending brightness order. + S.sort( (a, b) => (a.flux < b.flux) ? +1 : ((b.flux < a.flux) ? -1 : 0) ) + + // Perform a soft garbage collection. This eases integration with very + // long batch tasks and has no measurable performance penalty. + gc( false/*hard*/ ) + + if ( this.progressCallback != undefined ) + if ( !this.progressCallback( M.rows, M.rows ) ) + return null + + return S + } +} + +StarDetector.prototype = new Object + +function computeHfr(image, s) { + let a = 0 + let b = 0 + + for(let y = s.rect.y0; y <= s.rect.y1; y++) { + for(let x = s.rect.x0; x <= s.rect.x1; x++) { + if(x >= 0 && x < image.width && y >= 0 && y < image.height) { + const p = image.sample(x, y) + const v = p - s.bkg + const d = Math.sqrt((x - s.pos.x) * (x - s.pos.x) + (y - s.pos.y) * (y - s.pos.y)) + a += v * d + b += v + } + } + } + + s.hfr = b > 0.0 ? a / b : 0.0 +} + +function detectStars() { + const targetPath = jsArguments[0] + const statusPath = jsArguments[1] + const minSNR = parseFloat(jsArguments[2]) + const invert = jsArguments[3].toLowerCase() === 'true' + + console.writeln("targetPath=" + targetPath) + console.writeln("statusPath=" + statusPath) + console.writeln("minSNR=" + minSNR) + console.writeln("invert=" + invert) + + const P = new StarDetector + P.structureLayers = 5 + P.hotPixelFilterRadius = 1 + P.applyHotPixelFilterToDetectionImage = false + P.noiseReductionFilterRadius = 0 + P.sensitivity = 0.5 + P.peakResponse = 0.5 + P.allowClusteredSources = false + P.localDetectionFilterRadius = 2 + P.localMaximaDetectionLimit = 0.75 + P.noLocalMaximaDetection = false + P.maxDistortion = 0.6 + P.brightThreshold = 3 + P.minSNR = minSNR + P.minStructureSize = 1 + P.upperLimit = 1.0 + P.invert = invert + P.xyStretch = 1.5 + + const window = ImageWindow.open(targetPath) + const image = window[0].mainView.image + + const sl = P.stars(image) + + console.writeln("star detection finished. stars=", sl.length) + + const stars = [] + + for(let i = 0; i < sl.length; i++) { + const s = sl[i] + computeHfr(image, s) + stars.push({ x: s.pos.x, y: s.pos.y, flux: s.flux, size: s.size, nmax: s.nmax, bkg: s.bkg, x0: s.rect.x0, y0: s.rect.y0, x1: s.rect.x1, y1: s.rect.y1, snr: s.snr, peak: s.peak, hfd: 2 * s.hfr }) + } + + window[0].forceClose() + + const json = { + stars: stars + } + + File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") +} + +detectStars() diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index 7ae6fc8c2..c0b43c404 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -1,7 +1,9 @@ import io.kotest.core.annotation.EnabledIf 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.pixinsight.script.* import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.test.NonGitHubOnlyCondition @@ -22,13 +24,28 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { .use { it.runSync(runner).shouldBeTrue() } } "calibrate" { - PixInsightCalibrate(PixInsightScript.DEFAULT_SLOT, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) + PixInsightCalibrate(PixInsightScript.UNSPECIFIED_SLOT, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().shouldExist() } } "align" { - PixInsightAlign(PixInsightScript.DEFAULT_SLOT, PI_01_LIGHT, PI_02_LIGHT) + PixInsightAlign(PixInsightScript.UNSPECIFIED_SLOT, PI_01_LIGHT, PI_02_LIGHT) .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().shouldExist() } + } + "detect stars" { + PixInsightDetectStars(PixInsightScript.UNSPECIFIED_SLOT, PI_FOCUS_0) + .use { it.runSync(runner).also(::println).stars } + .map { it.hfd } + .average() shouldBe (9.03 plusOrMinus 1e-2) + + PixInsightDetectStars(PixInsightScript.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) + .use { it.runSync(runner).also(::println).stars } + .map { it.hfd } + .average() shouldBe (20.88 plusOrMinus 1e-2) } } } diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt new file mode 100644 index 000000000..e16408ef5 --- /dev/null +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt @@ -0,0 +1,16 @@ +import io.kotest.matchers.collections.shouldHaveSize +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.pixinsight.star.detection.PixInsightStarDetector +import nebulosa.test.AbstractFitsAndXisfTest +import java.nio.file.Path + +class PixInsightStarDetectorTest : AbstractFitsAndXisfTest() { + + init { + "detect stars" { + val runner = PixInsightScriptRunner(Path.of("PixInsight")) + val detectedStars = PixInsightStarDetector(runner, 0).detect(NGC3344_MONO_8_FITS) + detectedStars shouldHaveSize 15 + } + } +} diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt index befb2806d..f14f90738 100644 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt @@ -123,6 +123,12 @@ abstract class AbstractFitsAndXisfTest : StringSpec() { protected val PI_DARK by lazy { download("PI.DARK.fits", GITHUB_FITS_URL) } protected val PI_FLAT by lazy { download("PI.FLAT.fits", GITHUB_FITS_URL) } + protected val PI_FOCUS_0 by lazy { download("PI.FOCUS.0.fits", GITHUB_FITS_URL) } + protected val PI_FOCUS_10000 by lazy { download("PI.FOCUS.10000.fits", GITHUB_FITS_URL) } + protected val PI_FOCUS_20000 by lazy { download("PI.FOCUS.20000.fits", GITHUB_FITS_URL) } + protected val PI_FOCUS_30000 by lazy { download("PI.FOCUS.30000.fits", GITHUB_FITS_URL) } + protected val PI_FOCUS_100000 by lazy { download("PI.FOCUS.100000.fits", GITHUB_FITS_URL) } + private val afterEach = AfterEach() init { From 353d288892788dcb7364ca098eba40e15edbc460 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 4 Jun 2024 19:08:27 -0300 Subject: [PATCH 06/26] [api][desktop]: Support PixInsight Star Detector --- api/build.gradle.kts | 1 + .../api/stardetection/ImageStarSerializer.kt | 24 ++++++++++++++ .../api/stardetection/StarDetectionRequest.kt | 16 ++++++++++ .../api/stardetection/StarDetectorType.kt | 3 +- desktop/src/app/app.module.ts | 5 ++- .../app/autofocus/autofocus.component.html | 8 ++--- desktop/src/app/image/image.component.html | 19 ++++++------ desktop/src/app/image/image.component.ts | 19 ++++++++---- .../src/app/settings/settings.component.html | 8 ++--- .../src/app/settings/settings.component.ts | 2 ++ desktop/src/shared/pipes/dropdown-options.ts | 31 +++++++++++++++++++ .../shared/services/browser-window.service.ts | 2 +- desktop/src/shared/types/image.types.ts | 5 ++- desktop/src/shared/types/settings.types.ts | 2 +- .../nebulosa/common/exec/CommandLine.kt | 7 ++++- .../livestacking/PixInsightLiveStacker.kt | 3 +- .../script/AbstractPixInsightScript.kt | 7 ++++- .../script/PixInsightDetectStars.kt | 14 +++++++-- .../pixinsight/script/PixInsightScript.kt | 8 +---- .../script/PixInsightScriptRunner.kt | 2 +- .../star/detection/PixInsightStarDetector.kt | 5 ++- .../main/resources/pixinsight/DetectStars.js | 2 +- 22 files changed, 147 insertions(+), 46 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/ImageStarSerializer.kt create mode 100644 desktop/src/shared/pipes/dropdown-options.ts diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 47b11a299..05557ac63 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(project(":nebulosa-log")) implementation(project(":nebulosa-lx200-protocol")) implementation(project(":nebulosa-nova")) + implementation(project(":nebulosa-pixinsight")) implementation(project(":nebulosa-sbd")) implementation(project(":nebulosa-simbad")) implementation(project(":nebulosa-siril")) diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/ImageStarSerializer.kt b/api/src/main/kotlin/nebulosa/api/stardetection/ImageStarSerializer.kt new file mode 100644 index 000000000..f4e190ecd --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stardetection/ImageStarSerializer.kt @@ -0,0 +1,24 @@ +package nebulosa.api.stardetection + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import nebulosa.star.detection.ImageStar +import org.springframework.stereotype.Component + +@Component +class ImageStarSerializer : StdSerializer(ImageStar::class.java) { + + override fun serialize(star: ImageStar?, gen: JsonGenerator, provider: SerializerProvider) { + if (star == null) gen.writeNull() + else { + gen.writeStartObject() + gen.writeNumberField("x", star.x) + gen.writeNumberField("y", star.y) + gen.writeNumberField("hfd", star.hfd) + gen.writeNumberField("snr", star.snr) + gen.writeNumberField("flux", star.flux) + gen.writeEndObject() + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt index dcdd0ab97..d71e1f10a 100644 --- a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt @@ -1,6 +1,11 @@ package nebulosa.api.stardetection import nebulosa.astap.star.detection.AstapStarDetector +import nebulosa.pixinsight.script.PixInsightIsRunning +import nebulosa.pixinsight.script.PixInsightScript +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.pixinsight.script.PixInsightStartup +import nebulosa.pixinsight.star.detection.PixInsightStarDetector import nebulosa.star.detection.StarDetector import java.nio.file.Path import java.time.Duration @@ -15,6 +20,17 @@ data class StarDetectionRequest( override fun get() = when (type) { StarDetectorType.ASTAP -> AstapStarDetector(executablePath!!, minSNR) + StarDetectorType.PIXINSIGHT -> { + val runner = PixInsightScriptRunner(executablePath!!) + + if (!PixInsightIsRunning(PixInsightScript.DEFAULT_SLOT).use { it.runSync(runner) }) { + if (!PixInsightStartup(PixInsightScript.DEFAULT_SLOT).use { it.runSync(runner) }) { + throw IllegalStateException("unable to start PixInsight") + } + } + + PixInsightStarDetector(runner, PixInsightScript.DEFAULT_SLOT, minSNR, timeout) + } } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt index 31ae2f97c..d34427b0b 100644 --- a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt @@ -1,5 +1,6 @@ package nebulosa.api.stardetection enum class StarDetectorType { - ASTAP + ASTAP, + PIXINSIGHT } diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 0826d8f46..f0cd52ca6 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -49,6 +49,7 @@ import { HistogramComponent } from '../shared/components/histogram/histogram.com import { MapComponent } from '../shared/components/map/map.component' import { MenuItemComponent } from '../shared/components/menu-item/menu-item.component' import { MoonComponent } from '../shared/components/moon/moon.component' +import { PathChooserComponent } from '../shared/components/path-chooser/path-chooser.component' import { SlideMenuComponent } from '../shared/components/slide-menu/slide-menu.component' import { LocationDialog } from '../shared/dialogs/location/location.dialog' import { ScrollableNumberDirective } from '../shared/directives/input-number-scrollable' @@ -56,6 +57,7 @@ import { NoDropdownDirective } from '../shared/directives/no-dropdown.directive' import { StopPropagationDirective } from '../shared/directives/stop-propagation.directive' import { LocationInterceptor } from '../shared/interceptors/location.interceptor' import { AnglePipe } from '../shared/pipes/angle.pipe' +import { DropdownOptionsPipe } from '../shared/pipes/dropdown-options' import { EnumPipe } from '../shared/pipes/enum.pipe' import { EnvPipe } from '../shared/pipes/env.pipe' import { ExposureTimePipe } from '../shared/pipes/exposureTime.pipe' @@ -84,7 +86,6 @@ 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 { PathChooserComponent } from '../shared/components/path-chooser/path-chooser.component' @NgModule({ declarations: [ @@ -101,6 +102,7 @@ import { PathChooserComponent } from '../shared/components/path-chooser/path-cho DeviceChooserComponent, DeviceListMenuComponent, DialogMenuComponent, + DropdownOptionsPipe, EnumPipe, EnvPipe, ExposureTimePipe, @@ -179,6 +181,7 @@ import { PathChooserComponent } from '../shared/components/path-chooser/path-cho AnglePipe, ConfirmationService, DialogService, + DropdownOptionsPipe, EnumPipe, EnvPipe, ExposureTimePipe, diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 2e39c859f..7162a69c9 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -58,7 +58,7 @@
- @@ -66,8 +66,8 @@
- +
@@ -83,7 +83,7 @@
- diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index ef6ee2804..a0056fee1 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -159,7 +159,8 @@
- + @@ -295,7 +296,7 @@
+ [options]="'SCNR_PROTECTION_METHOD' | dropdownOptions" styleClass="p-inputtext-sm border-0" [autoDisplayFirst]="false">
{{ item | enum }} @@ -421,7 +422,7 @@
- @@ -457,7 +458,7 @@
+ value="{{ starDetection.computed.minFlux.toFixed(0) }} | {{ starDetection.computed.maxFlux.toFixed(0) }}" />
@@ -478,7 +479,7 @@
- +
@@ -490,7 +491,7 @@
- +
@@ -498,7 +499,7 @@
- + @@ -673,13 +674,13 @@
-
diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 5a2a2f04f..82ff751a2 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -18,7 +18,7 @@ import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' -import { DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, SCNR_PROTECTION_METHODS, StarDetectionDialog } from '../../shared/types/image.types' +import { DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, StarDetectionDialog } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' @@ -59,7 +59,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { { name: 'Green', value: 'GREEN' }, { name: 'Blue', value: 'BLUE' }, ] - readonly scnrMethods = Array.from(SCNR_PROTECTION_METHODS) readonly scnr: ImageSCNRDialog = { showDialog: false, amount: 0.5, @@ -99,6 +98,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { useSimbad: false } + detecting = false readonly starDetection: StarDetectionDialog = { showDialog: false, type: 'ASTAP', @@ -128,7 +128,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { centerDEC: '', radius: 4, solved: structuredClone(EMPTY_IMAGE_SOLVED), - types: ['ASTAP', 'ASTROMETRY_NET_ONLINE'], type: 'ASTAP' } @@ -776,9 +775,16 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } async detectStars() { + this.detecting = true + const options = this.preference.starDetectionRequest(this.starDetection.type).get() options.minSNR = this.starDetection.minSNR - this.starDetection.stars = await this.api.detectStars(this.imagePath!, options) + + try { + this.starDetection.stars = await this.api.detectStars(this.imagePath!, options) + } finally { + this.detecting = false + } let hfd = 0 let snr = 0 @@ -1220,9 +1226,9 @@ export class ImageComponent implements AfterViewInit, OnDestroy { private loadPreference() { const preference = this.preference.imagePreference.get() this.solver.radius = preference.solverRadius ?? this.solver.radius - this.solver.type = preference.solverType ?? this.solver.types[0] + this.solver.type = preference.solverType ?? 'ASTAP' this.starDetection.type = preference.starDetectionType ?? this.starDetection.type - this.starDetection.minSNR = this.preference.starDetectionRequest(this.starDetection.type).get().minSNR ?? this.starDetection.type + this.starDetection.minSNR = preference.starDetectionMinSNR ?? this.preference.starDetectionRequest(this.starDetection.type).get().minSNR ?? this.starDetection.minSNR this.fov.fovs = this.preference.imageFOVs.get() this.fov.fovs.forEach(e => { e.enabled = false; e.computed = undefined }) @@ -1233,6 +1239,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { preference.solverRadius = this.solver.radius preference.solverType = this.solver.type preference.starDetectionType = this.starDetection.type + preference.starDetectionMinSNR = this.starDetection.minSNR this.preference.imagePreference.set(preference) } diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index e7f18020c..14c632fce 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -31,7 +31,7 @@
- @@ -78,7 +78,7 @@
- @@ -100,7 +100,7 @@
-
+
@@ -111,7 +111,7 @@
- diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index dfd22b51c..e999409a5 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -62,6 +62,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.plateSolvers.set('ASTROMETRY_NET_ONLINE', preference.plateSolverRequest('ASTROMETRY_NET_ONLINE').get()) this.starDetectors.set('ASTAP', preference.starDetectionRequest('ASTAP').get()) + this.starDetectors.set('PIXINSIGHT', preference.starDetectionRequest('PIXINSIGHT').get()) this.liveStackers.set('SIRIL', preference.liveStackingRequest('SIRIL').get()) } @@ -130,6 +131,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.preference.plateSolverRequest('ASTROMETRY_NET_ONLINE').set(this.plateSolvers.get('ASTROMETRY_NET_ONLINE')) this.preference.starDetectionRequest('ASTAP').set(this.starDetectors.get('ASTAP')) + this.preference.starDetectionRequest('PIXINSIGHT').set(this.starDetectors.get('PIXINSIGHT')) this.preference.liveStackingRequest('SIRIL').set(this.liveStackers.get('SIRIL')) } diff --git a/desktop/src/shared/pipes/dropdown-options.ts b/desktop/src/shared/pipes/dropdown-options.ts new file mode 100644 index 000000000..f0e819e55 --- /dev/null +++ b/desktop/src/shared/pipes/dropdown-options.ts @@ -0,0 +1,31 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { AutoFocusFittingMode, BacklashCompensationMode } from '../types/autofocus.type' +import { LiveStackerType } from '../types/camera.types' +import { Bitpix, ImageChannel, ImageFormat, SCNRProtectionMethod } from '../types/image.types' +import { PlateSolverType, StarDetectorType } from '../types/settings.types' + +export type DropdownOptionType = 'STAR_DETECTOR' | 'PLATE_SOLVER' | 'LIVE_STACKER' + | 'AUTO_FOCUS_FITTING_MODE' | 'AUTO_FOCUS_BACKLASH_COMPENSATION_MODE' | 'SCNR_PROTECTION_METHOD' + | 'IMAGE_FORMAT' | 'IMAGE_BITPIX' | 'IMAGE_CHANNEL' + +export type DropdownOptionReturnType = StarDetectorType[] | PlateSolverType[] | LiveStackerType[] + | AutoFocusFittingMode[] | BacklashCompensationMode[] | SCNRProtectionMethod[] + | ImageFormat[] | Bitpix[] | ImageChannel[] + +@Pipe({ name: 'dropdownOptions' }) +export class DropdownOptionsPipe implements PipeTransform { + + transform(type: DropdownOptionType): DropdownOptionReturnType | undefined { + switch (type) { + case 'STAR_DETECTOR': return ['ASTAP', 'PIXINSIGHT'] + case 'PLATE_SOLVER': return ['ASTAP', 'ASTROMETRY_NET_ONLINE'] + case 'AUTO_FOCUS_FITTING_MODE': return ['TRENDLINES', 'PARABOLIC', 'TREND_PARABOLIC', 'HYPERBOLIC', 'TREND_HYPERBOLIC'] + case 'AUTO_FOCUS_BACKLASH_COMPENSATION_MODE': return ['NONE', 'ABSOLUTE', 'OVERSHOOT'] + case 'LIVE_STACKER': return ['SIRIL'] + case 'SCNR_PROTECTION_METHOD': return ['MAXIMUM_MASK', 'ADDITIVE_MASK', 'AVERAGE_NEUTRAL', 'MAXIMUM_NEUTRAL', 'MINIMUM_NEUTRAL'] + case 'IMAGE_FORMAT': return ['FITS', 'XISF', 'PNG', 'JPG'] + case 'IMAGE_BITPIX': return ['BYTE', 'SHORT', 'INTEGER', 'FLOAT', 'DOUBLE'] + case 'IMAGE_CHANNEL': return ['RED', 'GREEN', 'BLUE', 'GRAY'] + } + } +} diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 37bb0d4d6..215f407de 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -116,7 +116,7 @@ export class BrowserWindowService { openSettings(options: OpenWindowOptions = {}) { Object.assign(options, { icon: 'settings', width: 340, height: 440 }) - this.openWindow({ ...options, id: 'settings', path: 'settings', data: undefined }) + this.openWindow({ ...options, id: 'settings', path: 'settings', data: undefined, autoResizable: false }) } openCalculator(options: OpenWindowOptions = {}) { diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index 06346d2c0..1aa7d5442 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -5,8 +5,7 @@ import { PlateSolverType, StarDetectorType } from './settings.types' export type ImageChannel = 'RED' | 'GREEN' | 'BLUE' | 'GRAY' -export const SCNR_PROTECTION_METHODS = ['MAXIMUM_MASK', 'ADDITIVE_MASK', 'AVERAGE_NEUTRAL', 'MAXIMUM_NEUTRAL', 'MINIMUM_NEUTRAL'] as const -export type SCNRProtectionMethod = (typeof SCNR_PROTECTION_METHODS)[number] +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' @@ -117,6 +116,7 @@ export interface ImagePreference { solverType?: PlateSolverType savePath?: string starDetectionType?: StarDetectorType + starDetectionMinSNR?: number } export const EMPTY_IMAGE_PREFERENCE: ImagePreference = { @@ -215,7 +215,6 @@ export interface ImageSolverDialog { centerDEC: Angle radius: number readonly solved: ImageSolved - readonly types: PlateSolverType[] type: PlateSolverType } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index b9226023d..7e6b3fc4b 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -18,7 +18,7 @@ export const EMPTY_PLATE_SOLVER_OPTIONS: PlateSolverOptions = { timeout: 600, } -export type StarDetectorType = 'ASTAP' +export type StarDetectorType = 'ASTAP' | 'PIXINSIGHT' export interface StarDetectionOptions { type: StarDetectorType diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt index f584b2d60..550b722e6 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt @@ -68,7 +68,12 @@ data class CommandLine internal constructor( @Synchronized fun start(timeout: Duration = Duration.ZERO): CommandLine { if (process == null) { - process = builder.start() + process = try { + builder.start() + } catch (e: Throwable) { + completeExceptionally(e) + return this + } if (listeners.isNotEmpty()) { inputReader = StreamLineReader(process!!.inputStream, false) diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt index a1e7c6e6d..036a5adca 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt @@ -6,7 +6,6 @@ import nebulosa.pixinsight.script.PixInsightScript import nebulosa.pixinsight.script.PixInsightScriptRunner import nebulosa.pixinsight.script.PixInsightStartup import java.nio.file.Path -import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean data class PixInsightLiveStacker( @@ -32,7 +31,7 @@ data class PixInsightLiveStacker( if (!isPixInsightRunning) { try { - check(PixInsightStartup(PixInsightScript.DEFAULT_SLOT).runSync(runner, 30, TimeUnit.SECONDS)) + check(PixInsightStartup(PixInsightScript.DEFAULT_SLOT).runSync(runner)) } catch (e: Throwable) { throw IllegalStateException("Unable to start PixInsight") } 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 8ce7461cd..76cb56794 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.module.kotlin.kotlinModule import nebulosa.common.exec.CommandLine import nebulosa.common.exec.LineReadListener import nebulosa.common.json.PathDeserializer +import nebulosa.log.loggerFor import java.nio.file.Path import java.util.concurrent.CompletableFuture @@ -20,9 +21,11 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen final override fun run(runner: PixInsightScriptRunner) = runner.run(this) - final override fun handleCommandLine(commandLine: CommandLine) { + final override fun startCommandLine(commandLine: CommandLine) { commandLine.whenComplete { exitCode, exception -> try { + LOG.info("PixInsight script finished. done={}, exitCode={}", isDone, exitCode, exception) + if (isDone) return@whenComplete else if (exception != null) completeExceptionally(exception) else complete(processOnComplete(exitCode)) @@ -41,6 +44,8 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen internal const val START_FILE = "@" internal const val END_FILE = "#" + @JvmStatic private val LOG = loggerFor>() + @JvmStatic private val KOTLIN_MODULE = kotlinModule() .addDeserializer(Path::class.java, PathDeserializer) 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 fa6fceba3..5a64f52e1 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt @@ -5,6 +5,7 @@ import nebulosa.io.transferAndClose import nebulosa.star.detection.ImageStar import java.nio.file.Files import java.nio.file.Path +import java.time.Duration import kotlin.io.path.deleteIfExists import kotlin.io.path.outputStream import kotlin.io.path.readText @@ -14,6 +15,7 @@ data class PixInsightDetectStars( private val targetPath: Path, private val minSNR: Double = 0.0, private val invert: Boolean = false, + private val timeout: Duration = Duration.ZERO, ) : AbstractPixInsightScript() { @Suppress("ArrayInDataClass") @@ -51,15 +53,23 @@ data class PixInsightDetectStars( override val arguments = listOf("-x=${parameterize(slot, scriptPath, targetPath, outputPath, minSNR, invert)}") override fun processOnComplete(exitCode: Int): Result { + val timeoutInMillis = timeout.toMillis() + if (exitCode == 0) { - repeat(5) { + val startTime = System.currentTimeMillis() + + repeat(600) { val text = outputPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) } - Thread.sleep(1000) + if (timeoutInMillis == 0L || System.currentTimeMillis() - startTime < timeoutInMillis) { + Thread.sleep(500) + } else { + return@repeat + } } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScript.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScript.kt index 7456b95da..58a39d50e 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScript.kt @@ -3,13 +3,12 @@ package nebulosa.pixinsight.script import nebulosa.common.exec.CommandLine import java.io.Closeable import java.util.concurrent.Future -import java.util.concurrent.TimeUnit interface PixInsightScript : Future, Closeable { val arguments: Iterable - fun handleCommandLine(commandLine: CommandLine) + fun startCommandLine(commandLine: CommandLine) fun run(runner: PixInsightScriptRunner) @@ -18,11 +17,6 @@ interface PixInsightScript : Future, Closeable { return get() } - fun runSync(runner: PixInsightScriptRunner, timeout: Long, unit: TimeUnit = TimeUnit.MILLISECONDS): T { - run(runner) - return get(timeout, unit) - } - companion object { const val DEFAULT_SLOT = 256 diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt index 52b61b2de..74be7d3cb 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt @@ -15,7 +15,7 @@ data class PixInsightScriptRunner(private val executablePath: Path) { LOG.info("running PixInsight script: {}", commandLine.command) - script.handleCommandLine(commandLine) + script.startCommandLine(commandLine) } companion object { diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/star/detection/PixInsightStarDetector.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/star/detection/PixInsightStarDetector.kt index c423a363a..b729ba3ab 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/star/detection/PixInsightStarDetector.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/star/detection/PixInsightStarDetector.kt @@ -5,14 +5,17 @@ import nebulosa.pixinsight.script.PixInsightScriptRunner import nebulosa.star.detection.ImageStar import nebulosa.star.detection.StarDetector import java.nio.file.Path +import java.time.Duration data class PixInsightStarDetector( private val runner: PixInsightScriptRunner, private val slot: Int, private val minSNR: Double = 0.0, + private val timeout: Duration = Duration.ZERO, ) : StarDetector { override fun detect(input: Path): List { - return PixInsightDetectStars(slot, input, minSNR).use { it.runSync(runner).stars.toList() } + return PixInsightDetectStars(slot, input, minSNR, false, timeout) + .use { it.runSync(runner).stars.toList() } } } diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js index 721fd5b22..60864248f 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js @@ -837,7 +837,7 @@ function detectStars() { for(let i = 0; i < sl.length; i++) { const s = sl[i] computeHfr(image, s) - stars.push({ x: s.pos.x, y: s.pos.y, flux: s.flux, size: s.size, nmax: s.nmax, bkg: s.bkg, x0: s.rect.x0, y0: s.rect.y0, x1: s.rect.x1, y1: s.rect.y1, snr: s.snr, peak: s.peak, hfd: 2 * s.hfr }) + stars.push({ x: s.pos.x, y: s.pos.y, flux: s.flux * 65536, size: s.size, nmax: s.nmax, bkg: s.bkg, x0: s.rect.x0, y0: s.rect.y0, x1: s.rect.x1, y1: s.rect.y1, snr: s.snr, peak: s.peak, hfd: 2 * s.hfr }) } window[0].forceClose() From 96a1e6e718acddba1eb8b1146983e3a6809b98c0 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 5 Jun 2024 18:53:05 -0300 Subject: [PATCH 07/26] [api][desktop]: Implement PixInsight Pixel Math --- .../calibration/CalibrationFrameService.kt | 37 +++++--- desktop/src/app/image/image.component.html | 61 ++++++------ desktop/src/app/image/image.component.ts | 53 ++++++----- desktop/src/shared/types/image.types.ts | 11 ++- nebulosa-pixinsight/build.gradle.kts | 1 + .../livestacking/PixInsightLiveStacker.kt | 89 ++++++++++++++---- .../script/AbstractPixInsightScript.kt | 17 +--- .../pixinsight/script/PixInsightAlign.kt | 27 ++++-- .../pixinsight/script/PixInsightCalibrate.kt | 31 ++++-- .../script/PixInsightDetectStars.kt | 30 +++--- .../pixinsight/script/PixInsightPixelMath.kt | 71 ++++++++++++++ .../pixinsight/script/PixInsightStartup.kt | 2 +- .../src/main/resources/pixinsight/Align.js | 20 +++- .../main/resources/pixinsight/Calibrate.js | 40 +++++--- .../main/resources/pixinsight/DetectStars.js | 20 +++- .../main/resources/pixinsight/PixelMath.js | 94 +++++++++++++++++++ .../src/test/kotlin/PixInsightScriptTest.kt | 6 ++ .../test/kotlin/PixInsightStarDetectorTest.kt | 3 + 18 files changed, 460 insertions(+), 153 deletions(-) create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt index 25ecd51d6..012a7dbd7 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt @@ -31,26 +31,37 @@ class CalibrationFrameService( fun calibrate(name: String, image: Image, createNew: Boolean = false): Image { return synchronized(image) { val darkFrame = findBestDarkFrames(name, image).firstOrNull() - val biasFrame = findBestBiasFrames(name, image).firstOrNull() + val biasFrame = if (darkFrame == null) findBestBiasFrames(name, image).firstOrNull() else null val flatFrame = findBestFlatFrames(name, image).firstOrNull() - if (darkFrame != null || biasFrame != null || flatFrame != null) { + val darkImage = darkFrame?.path?.fits()?.use(Image::open) + val biasImage = biasFrame?.path?.fits()?.use(Image::open) + var flatImage = flatFrame?.path?.fits()?.use(Image::open) + + if (darkImage != null || biasImage != null || flatImage != null) { var transformedImage = if (createNew) image.clone() else image - if (biasFrame != null) { - val calibrationImage = biasFrame.path!!.fits().use(Image::open) - transformedImage = transformedImage.transform(BiasSubtraction(calibrationImage)) + // If not using dark frames. + if (biasImage != null) { + // Subtract Master Bias from Flat Frames. + if (flatImage != null) { + flatImage = flatImage.transform(BiasSubtraction(biasImage)) + LOG.info("bias frame subtraction applied to flat frame. frame={}", biasFrame) + } + + // Subtract the Master Bias frame. + transformedImage = transformedImage.transform(BiasSubtraction(biasImage)) LOG.info("bias frame subtraction applied. frame={}", biasFrame) - } else { + } else if (darkFrame == null) { LOG.info( "no bias frames found. width={}, height={}, bin={}, gain={}", image.width, image.height, image.header.binX, image.header.gain ) } - if (darkFrame != null) { - val calibrationImage = darkFrame.path!!.fits().use(Image::open) - transformedImage = transformedImage.transform(DarkSubtraction(calibrationImage)) + // Subtract Master Dark frame. + if (darkImage != null) { + transformedImage = transformedImage.transform(DarkSubtraction(darkImage)) LOG.info("dark frame subtraction applied. frame={}", darkFrame) } else { LOG.info( @@ -59,9 +70,9 @@ class CalibrationFrameService( ) } - if (flatFrame != null) { - val calibrationImage = flatFrame.path!!.fits().use(Image::open) - transformedImage = transformedImage.transform(FlatCorrection(calibrationImage)) + // Divide the Dark-subtracted Light frame by the Master Flat frame to correct for variations in the optical path. + if (flatImage != null) { + transformedImage = transformedImage.transform(FlatCorrection(flatImage)) LOG.info("flat frame correction applied. frame={}", flatFrame) } else { LOG.info( @@ -177,7 +188,7 @@ class CalibrationFrameService( name: String, width: Int, height: Int, binX: Int, binY: Int, filter: String? ): List { - // TODO: Generate master from matched frames. + // TODO: Generate master from matched frames. (Subtract the master bias frame from each flat frame) return calibrationFrameRepository .flatFrames(name, filter, width, height, binX) } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index a0056fee1..e9acad941 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -10,9 +10,9 @@ - - + {{ (a.star ?? a.dso ?? a.minorPlanet) | skyObject:'name' }} @@ -85,71 +85,76 @@
- + -
- +
- +
-
-
-
+
-
-
+
- +
-
+
- +
-
+
-
-
- Simbad +
+ Simbad
- - - - + + + +
@@ -231,18 +236,18 @@
- - - -
- + @@ -499,7 +504,7 @@
- + diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 82ff751a2..68709727e 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -16,9 +16,9 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' -import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from '../../shared/types/atlas.types' +import { Angle, EquatorialCoordinateJ2000 } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' -import { DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, StarDetectionDialog } from '../../shared/types/image.types' +import { AnnotationInfoDialog, DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, StarDetectionDialog } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' @@ -92,15 +92,22 @@ export class ImageComponent implements AfterViewInit, OnDestroy { readonly annotation: ImageAnnotationDialog = { showDialog: false, + running: false, + visible: false, useStarsAndDSOs: true, useMinorPlanets: false, minorPlanetsMagLimit: 18.0, - useSimbad: false + useSimbad: false, + data: [] + } + + readonly annotationInfo: AnnotationInfoDialog = { + showDialog: false } - detecting = false readonly starDetection: StarDetectionDialog = { showDialog: false, + running: false, type: 'ASTAP', minSNR: 0, visible: false, @@ -122,7 +129,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { readonly solver: ImageSolverDialog = { showDialog: false, - solving: false, + running: false, blind: true, centerRA: '', centerDEC: '', @@ -132,11 +139,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } crossHair = false - annotations: ImageAnnotation[] = [] - annotating = false - showAnnotationInfoDialog = false - annotationInfo?: AstronomicalObject & Partial - annotationIsVisible = false readonly fitsHeaders: ImageFITSHeadersDialog = { showDialog: false, @@ -341,7 +343,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, toggle: (event) => { event.originalEvent?.stopImmediatePropagation() - this.annotationIsVisible = event.checked + this.annotation.visible = event.checked }, } @@ -752,8 +754,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private clearOverlay() { - this.annotations = [] - this.annotationIsVisible = false + this.annotation.data = [] + this.annotation.visible = false this.annotationMenuItem.toggleable = false this.starDetection.stars = [] @@ -775,15 +777,14 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } async detectStars() { - this.detecting = true - const options = this.preference.starDetectionRequest(this.starDetection.type).get() options.minSNR = this.starDetection.minSNR try { + this.starDetection.running = true this.starDetection.stars = await this.api.detectStars(this.imagePath!, options) } finally { - this.detecting = false + this.starDetection.running = false } let hfd = 0 @@ -906,21 +907,21 @@ export class ImageComponent implements AfterViewInit, OnDestroy { async annotateImage() { try { - this.annotating = true - this.annotations = await this.api.annotationsOfImage(this.imagePath!, this.annotation.useStarsAndDSOs, + this.annotation.running = true + this.annotation.data = await this.api.annotationsOfImage(this.imagePath!, this.annotation.useStarsAndDSOs, this.annotation.useMinorPlanets, this.annotation.minorPlanetsMagLimit, this.annotation.useSimbad) - this.annotationIsVisible = true - this.annotationMenuItem.toggleable = this.annotations.length > 0 - this.annotationMenuItem.toggled = this.annotationMenuItem.toggleable + this.annotation.visible = this.annotation.data.length > 0 + this.annotationMenuItem.toggleable = this.annotation.visible + this.annotationMenuItem.toggled = this.annotation.visible this.annotation.showDialog = false } finally { - this.annotating = false + this.annotation.running = false } } showAnnotationInfo(annotation: ImageAnnotation) { - this.annotationInfo = annotation.star ?? annotation.dso ?? annotation.minorPlanet - this.showAnnotationInfoDialog = true + this.annotationInfo.info = annotation.star ?? annotation.dso ?? annotation.minorPlanet + this.annotationInfo.showDialog = true } private disableAutoStretch() { @@ -1037,7 +1038,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } async solveImage() { - this.solver.solving = true + this.solver.running = true try { const solver = this.preference.plateSolverRequest(this.solver.type).get() @@ -1049,7 +1050,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } catch { this.updateImageSolved(this.imageInfo?.solved) } finally { - this.solver.solving = false + this.solver.running = false if (this.solver.solved.solved) { this.retrieveCoordinateInterpolation() diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index 1aa7d5442..4960661d5 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -209,7 +209,7 @@ export interface ImageStretchDialog { export interface ImageSolverDialog { showDialog: boolean - solving: boolean + running: boolean blind: boolean centerRA: Angle centerDEC: Angle @@ -259,10 +259,13 @@ export interface ImageTransformation { export interface ImageAnnotationDialog { showDialog: boolean + running: boolean + visible: boolean useStarsAndDSOs: boolean useMinorPlanets: boolean minorPlanetsMagLimit: number useSimbad: boolean + data: ImageAnnotation[] } export interface ROISelected { @@ -275,6 +278,7 @@ export interface ROISelected { export interface StarDetectionDialog { showDialog: boolean + running: boolean type: StarDetectorType minSNR: number visible: boolean @@ -282,3 +286,8 @@ export interface StarDetectionDialog { computed: Omit & { minFlux: number, maxFlux: number } selected: DetectedStar } + +export interface AnnotationInfoDialog { + showDialog: boolean + info?: AstronomicalObject & Partial +} diff --git a/nebulosa-pixinsight/build.gradle.kts b/nebulosa-pixinsight/build.gradle.kts index 4c7d645c6..1819f5ff6 100644 --- a/nebulosa-pixinsight/build.gradle.kts +++ b/nebulosa-pixinsight/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { api(project(":nebulosa-star-detection")) api(project(":nebulosa-livestacking")) api(libs.bundles.jackson) + api(libs.apache.codec) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt index 036a5adca..f1bed390f 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt @@ -1,12 +1,11 @@ package nebulosa.pixinsight.livestacking import nebulosa.livestacking.LiveStacker -import nebulosa.pixinsight.script.PixInsightIsRunning -import nebulosa.pixinsight.script.PixInsightScript -import nebulosa.pixinsight.script.PixInsightScriptRunner -import nebulosa.pixinsight.script.PixInsightStartup +import nebulosa.pixinsight.script.* import java.nio.file.Path import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.moveTo +import kotlin.io.path.name data class PixInsightLiveStacker( private val runner: PixInsightScriptRunner, @@ -15,42 +14,94 @@ data class PixInsightLiveStacker( private val flat: Path? = null, private val bias: Path? = null, private val use32Bits: Boolean = false, + private val slot: Int = PixInsightScript.DEFAULT_SLOT, ) : LiveStacker { private val running = AtomicBoolean() + private val stacking = AtomicBoolean() override val isRunning get() = running.get() - override val isStacking: Boolean - get() = TODO("Not yet implemented") + override val isStacking + get() = stacking.get() + + @Volatile private var stackCount = 0 + @Volatile private var referencePath: Path? = null + @Volatile private var stackedPath: Path? = null @Synchronized override fun start() { - val isPixInsightRunning = PixInsightIsRunning(PixInsightScript.DEFAULT_SLOT).runSync(runner) + if (!running.get()) { + val isPixInsightRunning = PixInsightIsRunning(slot).use { it.runSync(runner) } - if (!isPixInsightRunning) { - try { - check(PixInsightStartup(PixInsightScript.DEFAULT_SLOT).runSync(runner)) - } catch (e: Throwable) { - throw IllegalStateException("Unable to start PixInsight") - } + if (!isPixInsightRunning) { + try { + check(PixInsightStartup(slot).use { it.runSync(runner) }) + } catch (e: Throwable) { + throw IllegalStateException("unable to start PixInsight") + } - running.set(true) + running.set(true) + } } } @Synchronized override fun add(path: Path): Path? { - return null + var targetPath = path + + if (running.get()) { + stacking.set(true) + + // Calibrate. + val calibratedPath = if (dark == null && flat == null && bias == null) null else { + PixInsightCalibrate(slot, targetPath, dark, flat, if (dark == null) bias else null).use { + val outputPath = it.runSync(runner).outputImage ?: return@use null + val destinationPath = Path.of("$workingDirectory", outputPath.name) + outputPath.moveTo(destinationPath, true) + } + } + + if (calibratedPath != null) { + targetPath = calibratedPath + } + + // TODO: Debayer, Resample? + + if (stackCount > 0) { + // Align. + val alignedPath = PixInsightAlign(slot, referencePath!!, targetPath).use { + val outputPath = it.runSync(runner).outputImage ?: return@use null + val destinationPath = Path.of("$workingDirectory", outputPath.name) + outputPath.moveTo(destinationPath, true) + } + + if (alignedPath != null) { + targetPath = alignedPath + } + + // Stack. + } else { + referencePath = targetPath + } + + stackedPath = targetPath + stackCount++ + + stacking.set(false) + } + + return stackedPath } @Synchronized override fun stop() { - + running.set(false) + stackCount = 0 + referencePath = null + stackedPath = null } - override fun close() { - - } + override fun close() = Unit } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt index 76cb56794..1519e89ff 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -5,7 +5,9 @@ import com.fasterxml.jackson.module.kotlin.kotlinModule import nebulosa.common.exec.CommandLine import nebulosa.common.exec.LineReadListener import nebulosa.common.json.PathDeserializer +import nebulosa.common.json.PathSerializer import nebulosa.log.loggerFor +import org.apache.commons.codec.binary.Hex import java.nio.file.Path import java.util.concurrent.CompletableFuture @@ -48,27 +50,18 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen @JvmStatic private val KOTLIN_MODULE = kotlinModule() .addDeserializer(Path::class.java, PathDeserializer) + .addSerializer(PathSerializer) @JvmStatic internal val OBJECT_MAPPER = jsonMapper { addModule(KOTLIN_MODULE) } @JvmStatic - internal fun parameterize(slot: Int, scriptPath: Path, vararg parameters: Any?): String { + internal fun execute(slot: Int, scriptPath: Path, data: Any): String { return buildString { if (slot > 0) append("$slot:") - append("\"$scriptPath,") - - parameters.forEachIndexed { i, parameter -> - if (i > 0) append(',') - - if (parameter is Path) append("'$parameter'") - else if (parameter is CharSequence) append("'$parameter'") - else if (parameter != null) append("$parameter") - else append('0') - } - + append(Hex.encodeHexString(OBJECT_MAPPER.writeValueAsBytes(data))) append('"') } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt index 2a40a49bc..f7306ee27 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt @@ -13,9 +13,16 @@ data class PixInsightAlign( private val slot: Int, private val referencePath: Path, private val targetPath: Path, -) : AbstractPixInsightScript() { +) : AbstractPixInsightScript() { - data class Result( + private data class Input( + @JvmField val referencePath: Path, + @JvmField val targetPath: Path, + @JvmField val outputDirectory: Path, + @JvmField val statusPath: Path, + ) + + data class Output( @JvmField val outputImage: Path? = null, @JvmField val outputMaskImage: Path? = null, @JvmField val totalPairMatches: Int = 0, @@ -40,39 +47,39 @@ data class PixInsightAlign( companion object { - @JvmStatic val FAILED = Result() + @JvmStatic val FAILED = Output() } } private val outputDirectory = Files.createTempDirectory("pi-align-") private val scriptPath = Files.createTempFile("pi-", ".js") - private val outputPath = Files.createTempFile("pi-", ".txt") + private val statusPath = Files.createTempFile("pi-", ".txt") init { resource("pixinsight/Align.js")!!.transferAndClose(scriptPath.outputStream()) } - override val arguments = listOf("-x=${parameterize(slot, scriptPath, referencePath, targetPath, outputDirectory, outputPath)}") + override val arguments = listOf("-x=${execute(slot, scriptPath, Input(referencePath, targetPath, outputDirectory, statusPath))}") - override fun processOnComplete(exitCode: Int): Result { + override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { repeat(5) { - val text = outputPath.readText() + val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { - return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) } Thread.sleep(1000) } } - return Result.FAILED + return Output.FAILED } override fun close() { scriptPath.deleteIfExists() - outputPath.deleteIfExists() + statusPath.deleteIfExists() outputDirectory.deleteRecursively() } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt index b8e781707..2ed40a104 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -17,48 +17,59 @@ data class PixInsightCalibrate( private val bias: Path? = null, private val compress: Boolean = false, private val use32Bit: Boolean = false, -) : AbstractPixInsightScript() { +) : AbstractPixInsightScript() { - data class Result( + private data class Input( + @JvmField val targetPath: Path, + @JvmField val outputDirectory: Path, + @JvmField val statusPath: Path, + @JvmField val masterDark: Path? = null, + @JvmField val masterFlat: Path? = null, + @JvmField val masterBias: Path? = null, + @JvmField val compress: Boolean = false, + @JvmField val use32Bit: Boolean = false, + ) + + data class Output( @JvmField val outputImage: Path? = null, ) { companion object { - @JvmStatic val FAILED = Result() + @JvmStatic val FAILED = Output() } } private val outputDirectory = Files.createTempDirectory("pi-calibrate-") private val scriptPath = Files.createTempFile("pi-", ".js") - private val outputPath = Files.createTempFile("pi-", ".txt") + private val statusPath = Files.createTempFile("pi-", ".txt") init { resource("pixinsight/Calibrate.js")!!.transferAndClose(scriptPath.outputStream()) } override val arguments = - listOf("-x=${parameterize(slot, scriptPath, targetPath, outputDirectory, outputPath, dark, flat, bias, compress, use32Bit)}") + listOf("-x=${execute(slot, scriptPath, Input(targetPath, outputDirectory, statusPath, dark, flat, bias, compress, use32Bit))}") - override fun processOnComplete(exitCode: Int): Result { + override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { repeat(5) { - val text = outputPath.readText() + val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { - return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) } Thread.sleep(1000) } } - return Result.FAILED + return Output.FAILED } override fun close() { scriptPath.deleteIfExists() - outputPath.deleteIfExists() + statusPath.deleteIfExists() outputDirectory.deleteRecursively() } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt index 5a64f52e1..01ed54d94 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt @@ -16,14 +16,22 @@ data class PixInsightDetectStars( private val minSNR: Double = 0.0, private val invert: Boolean = false, private val timeout: Duration = Duration.ZERO, -) : AbstractPixInsightScript() { +) : AbstractPixInsightScript() { - @Suppress("ArrayInDataClass") - data class Result(@JvmField val stars: Array = emptyArray()) { + private data class Input( + @JvmField val targetPath: Path, + @JvmField val statusPath: Path, + @JvmField val minSNR: Double = 0.0, + @JvmField val invert: Boolean = false, + ) + + data class Output( + @JvmField val stars: List = emptyList(), + ) { companion object { - @JvmStatic val FAILED = Result() + @JvmStatic val FAILED = Output() } } @@ -44,25 +52,25 @@ data class PixInsightDetectStars( ) : ImageStar private val scriptPath = Files.createTempFile("pi-", ".js") - private val outputPath = Files.createTempFile("pi-", ".txt") + private val statusPath = Files.createTempFile("pi-", ".txt") init { resource("pixinsight/DetectStars.js")!!.transferAndClose(scriptPath.outputStream()) } - override val arguments = listOf("-x=${parameterize(slot, scriptPath, targetPath, outputPath, minSNR, invert)}") + override val arguments = listOf("-x=${execute(slot, scriptPath, Input(targetPath, statusPath, minSNR, invert))}") - override fun processOnComplete(exitCode: Int): Result { + override fun processOnComplete(exitCode: Int): Output { val timeoutInMillis = timeout.toMillis() if (exitCode == 0) { val startTime = System.currentTimeMillis() repeat(600) { - val text = outputPath.readText() + val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { - return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Result::class.java) + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) } if (timeoutInMillis == 0L || System.currentTimeMillis() - startTime < timeoutInMillis) { @@ -73,11 +81,11 @@ data class PixInsightDetectStars( } } - return Result.FAILED + return Output.FAILED } override fun close() { scriptPath.deleteIfExists() - outputPath.deleteIfExists() + statusPath.deleteIfExists() } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt new file mode 100644 index 000000000..cd207907e --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt @@ -0,0 +1,71 @@ +package nebulosa.pixinsight.script + +import nebulosa.io.resource +import nebulosa.io.transferAndClose +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.deleteIfExists +import kotlin.io.path.outputStream +import kotlin.io.path.readText + +data class PixInsightPixelMath( + private val slot: Int, + private val inputPaths: List, + private val outputPath: Path, + private val expressionRK: String? = null, + private val expressionG: String? = null, + private val expressionB: String? = null, +) : AbstractPixInsightScript() { + + private data class Input( + @JvmField val outputDirectory: Path, + @JvmField val statusPath: Path, + @JvmField val inputPaths: List, + @JvmField val outputPath: Path, + @JvmField val expressionRK: String? = null, + @JvmField val expressionG: String? = null, + @JvmField val expressionB: String? = null, + ) + + data class Output( + @JvmField val stackedImage: Path? = null, + ) { + + companion object { + + @JvmStatic val FAILED = Output() + } + } + + private val outputDirectory = Files.createTempDirectory("pi-pixelmath-") + private val scriptPath = Files.createTempFile("pi-", ".js") + private val statusPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/PixelMath.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = + listOf("-x=${execute(slot, scriptPath, Input(outputDirectory, statusPath, inputPaths, outputPath, expressionRK, expressionG, expressionB))}") + + override fun processOnComplete(exitCode: Int): Output? { + if (exitCode == 0) { + repeat(5) { + val text = statusPath.readText() + + if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) + } + + Thread.sleep(1000) + } + } + + return Output.FAILED + } + + override fun close() { + scriptPath.deleteIfExists() + statusPath.deleteIfExists() + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt index ef009556f..e410a4ea9 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightStartup.kt @@ -17,7 +17,7 @@ data class PixInsightStartup(private val slot: Int) : AbstractPixInsightScript 0) "-n=$slot" else "-n") + override val arguments = listOf("-r=${execute(0, scriptPath, outputPath)}", if (slot > 0) "-n=$slot" else "-n") override fun beforeRun() { var count = 0 diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js index 6638b4b23..0d28d88de 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js @@ -1,8 +1,20 @@ +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + function alignment() { - const referencePath = jsArguments[0] - const targetPath = jsArguments[1] - const outputDirectory = jsArguments[2] - const statusPath = jsArguments[3] + const input = decodeParams(jsArguments[0]) + + const referencePath = input.referencePath + const targetPath = input.targetPath + const outputDirectory = input.outputDirectory + const statusPath = input.statusPath console.writeln("referencePath=" + referencePath) console.writeln("targetPath=" + targetPath) diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js index 4ac72ab8d..c5798cf75 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -1,12 +1,24 @@ +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + function calibrate() { - const targetPath = jsArguments[0] - const outputDirectory = jsArguments[1] - const statusPath = jsArguments[2] - const masterDark = jsArguments[3] - const masterFlat = jsArguments[4] - const masterBias = jsArguments[5] - const compress = jsArguments[6].toLowerCase() === 'true' - const use32Bit = jsArguments[7].toLowerCase() === 'true' + const input = decodeParams(jsArguments[0]) + + const targetPath = input.targetPath + const outputDirectory = input.outputDirectory + const statusPath = input.statusPath + const masterDark = input.masterDark + const masterFlat = input.masterFlat + const masterBias = input.masterBias + const compress = input.compress + const use32Bit = input.use32Bit console.writeln("targetPath=" + targetPath) console.writeln("outputDirectory=" + outputDirectory) @@ -40,12 +52,12 @@ function calibrate() { [false, 0, 0, 0, 0, 0, 0, 0, 0], [false, 0, 0, 0, 0, 0, 0, 0, 0] ] - P.masterBiasEnabled = masterBias !== "0" - P.masterBiasPath = masterBias - P.masterDarkEnabled = masterDark !== "0" - P.masterDarkPath = masterDark - P.masterFlatEnabled = masterFlat !== "0" - P.masterFlatPath = masterFlat + P.masterBiasEnabled = !!masterBias + P.masterBiasPath = masterBias || "" + P.masterDarkEnabled = !!masterDark + P.masterDarkPath = masterDark || "" + P.masterFlatEnabled = !!masterFlat + P.masterFlatPath = masterFlat || "" P.calibrateBias = false P.calibrateDark = false P.calibrateFlat = false diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js index 60864248f..129407d7e 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js @@ -795,11 +795,23 @@ function computeHfr(image, s) { s.hfr = b > 0.0 ? a / b : 0.0 } +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + function detectStars() { - const targetPath = jsArguments[0] - const statusPath = jsArguments[1] - const minSNR = parseFloat(jsArguments[2]) - const invert = jsArguments[3].toLowerCase() === 'true' + const input = decodeParams(jsArguments[0]) + + const targetPath = input.targetPath + const statusPath = input.statusPath + const minSNR = input.minSNR + const invert = input.invert console.writeln("targetPath=" + targetPath) console.writeln("statusPath=" + statusPath) diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js new file mode 100644 index 000000000..c4d199db4 --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js @@ -0,0 +1,94 @@ +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + +function pixelMath() { + const input = decodeParams(jsArguments[0]) + + const outputDirectory = input.outputDirectory + const statusPath = input.statusPath + const inputPaths = input.inputPaths + const outputPath = input.outputPath + let expressionRK = input.expressionRK + let expressionG = input.expressionG + let expressionB = input.expressionB + + console.writeln("outputDirectory=" + outputDirectory) + console.writeln("statusPath=" + statusPath) + console.writeln("inputPaths=" + inputPaths) + console.writeln("outputPath=" + outputPath) + + const windows = [] + + for(let i = 0; i < input.inputPaths.length; i++) { + windows.push(ImageWindow.open(input.inputPaths[i])[0]) + } + + for(let i = 0; i < windows.length; i++) { + if (expressionRK) { + expressionRK = expressionRK.replace("{{" + i + "}}", windows[i].mainView.id) + } + if (expressionG) { + expressionG = expressionG.replace("{{" + i + "}}", windows[i].mainView.id) + } + if (expressionB) { + expressionB = expressionB.replace("{{" + i + "}}", windows[i].mainView.id) + } + } + + console.writeln("expressionRK=" + expressionRK) + console.writeln("expressionG=" + expressionG) + console.writeln("expressionB=" + expressionB) + + var P = new PixelMath + P.expression = expressionRK || "" + P.expression1 = expressionG || "" + P.expression2 = expressionB || "" + P.expression3 = "" + P.useSingleExpression = false + P.symbols = "" + P.clearImageCacheAndExit = false + P.cacheGeneratedImages = false + P.generateOutput = true + P.singleThreaded = false + P.optimization = true + P.use64BitWorkingImage = false + P.rescale = false + P.rescaleLower = 0 + P.rescaleUpper = 1 + P.truncate = true + P.truncateLower = 0 + P.truncateUpper = 1 + P.createNewImage = false + P.showNewImage = false + P.newImageId = "" + P.newImageWidth = 0 + P.newImageHeight = 0 + P.newImageAlpha = false + P.newImageColorSpace = PixelMath.prototype.SameAsTarget + P.newImageSampleFormat = PixelMath.prototype.SameAsTarget + + P.executeOn(windows[0].mainView) + + windows[0].saveAs(outputPath, false, false, false, false) + + for(let i = 0; i < windows.length; i++) { + windows[i].forceClose() + } + + console.writeln("stacking finished") + + const json = { + stackedImage: outputPath, + } + + File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") +} + +pixelMath() diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index c0b43c404..6d7a06d96 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -7,6 +7,7 @@ import io.kotest.matchers.shouldBe import nebulosa.pixinsight.script.* import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.test.NonGitHubOnlyCondition +import java.nio.file.Files import java.nio.file.Path @EnabledIf(NonGitHubOnlyCondition::class) @@ -47,5 +48,10 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { .map { it.hfd } .average() shouldBe (20.88 plusOrMinus 1e-2) } + "pixel math" { + val outputPath = Files.createTempFile("pi-stacked-", ".fits") + PixInsightPixelMath(PixInsightScript.UNSPECIFIED_SLOT, listOf(PI_01_LIGHT, PI_02_LIGHT), outputPath, "{{0}} + {{1}}") + .use { it.runSync(runner).also(::println).stackedImage.shouldNotBeNull().shouldExist() } + } } } diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt index e16408ef5..d14437843 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightStarDetectorTest.kt @@ -1,9 +1,12 @@ +import io.kotest.core.annotation.EnabledIf import io.kotest.matchers.collections.shouldHaveSize import nebulosa.pixinsight.script.PixInsightScriptRunner import nebulosa.pixinsight.star.detection.PixInsightStarDetector import nebulosa.test.AbstractFitsAndXisfTest +import nebulosa.test.NonGitHubOnlyCondition import java.nio.file.Path +@EnabledIf(NonGitHubOnlyCondition::class) class PixInsightStarDetectorTest : AbstractFitsAndXisfTest() { init { From 9ba998a4a3a86e7a18cf2bc4931b6c93c1131a8e Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 5 Jun 2024 21:34:22 -0300 Subject: [PATCH 08/26] [api][desktop]: Support PixInsight Live Stacking --- .../api/livestacking/LiveStackerType.kt | 1 + .../api/livestacking/LiveStackingRequest.kt | 18 +++++ desktop/src/app/camera/camera.component.html | 8 ++- desktop/src/app/camera/camera.component.ts | 13 ++-- .../src/app/settings/settings.component.ts | 2 + desktop/src/shared/pipes/dropdown-options.ts | 2 +- desktop/src/shared/types/camera.types.ts | 3 +- .../livestacking/PixInsightLiveStacker.kt | 68 ++++++++++++------- .../script/AbstractPixInsightScript.kt | 20 +++++- .../src/main/resources/pixinsight/Align.js | 2 +- 10 files changed, 102 insertions(+), 35 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt index 5e1d6426e..d5f5f6c09 100644 --- a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt @@ -2,4 +2,5 @@ package nebulosa.api.livestacking enum class LiveStackerType { SIRIL, + PIXINSIGHT, } diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt index 9f26e144b..629772d60 100644 --- a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt @@ -3,6 +3,11 @@ package nebulosa.api.livestacking import com.fasterxml.jackson.databind.annotation.JsonDeserialize import nebulosa.api.beans.converters.angle.DegreesDeserializer import nebulosa.livestacking.LiveStacker +import nebulosa.pixinsight.livestacking.PixInsightLiveStacker +import nebulosa.pixinsight.script.PixInsightIsRunning +import nebulosa.pixinsight.script.PixInsightScript +import nebulosa.pixinsight.script.PixInsightScriptRunner +import nebulosa.pixinsight.script.PixInsightStartup import nebulosa.siril.livestacking.SirilLiveStacker import org.jetbrains.annotations.NotNull import java.nio.file.Files @@ -15,8 +20,10 @@ data class LiveStackingRequest( @JvmField @field:NotNull val executablePath: Path? = null, @JvmField val dark: Path? = null, @JvmField val flat: Path? = null, + @JvmField val bias: Path? = null, @JvmField @field:JsonDeserialize(using = DegreesDeserializer::class) val rotate: Double = 0.0, @JvmField val use32Bits: Boolean = false, + @JvmField val slot: Int = PixInsightScript.DEFAULT_SLOT, ) : Supplier { override fun get(): LiveStacker { @@ -24,6 +31,17 @@ data class LiveStackingRequest( return when (type) { LiveStackerType.SIRIL -> SirilLiveStacker(executablePath!!, workingDirectory, dark, flat, rotate, use32Bits) + LiveStackerType.PIXINSIGHT -> { + val runner = PixInsightScriptRunner(executablePath!!) + + if (!PixInsightIsRunning(slot).use { it.runSync(runner) }) { + if (!PixInsightStartup(slot).use { it.runSync(runner) }) { + throw IllegalStateException("unable to start PixInsight") + } + } + + PixInsightLiveStacker(runner, workingDirectory, dark, flat, bias, use32Bits, slot) + } } } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 793d9b3b2..7a5d3b84f 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -255,7 +255,7 @@
- @@ -263,7 +263,7 @@
32-bits (slower) -
@@ -282,5 +282,9 @@
+
+ +
\ No newline at end of file diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 4dd5aa290..809b09a6f 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -543,10 +543,15 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { } async startCapture() { - await this.openCameraImage() - await this.api.cameraSnoop(this.camera, this.equipment) - await this.api.cameraStartCapture(this.camera, this.makeCameraStartCapture()) - this.preference.equipmentForDevice(this.camera).set(this.equipment) + try { + this.running = true + await this.openCameraImage() + await this.api.cameraSnoop(this.camera, this.equipment) + await this.api.cameraStartCapture(this.camera, this.makeCameraStartCapture()) + this.preference.equipmentForDevice(this.camera).set(this.equipment) + } catch { + this.running = false + } } abortCapture() { diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index e999409a5..25ffcd1c6 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -65,6 +65,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.starDetectors.set('PIXINSIGHT', preference.starDetectionRequest('PIXINSIGHT').get()) this.liveStackers.set('SIRIL', preference.liveStackingRequest('SIRIL').get()) + this.liveStackers.set('PIXINSIGHT', preference.liveStackingRequest('PIXINSIGHT').get()) } async ngAfterViewInit() { } @@ -134,5 +135,6 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.preference.starDetectionRequest('PIXINSIGHT').set(this.starDetectors.get('PIXINSIGHT')) this.preference.liveStackingRequest('SIRIL').set(this.liveStackers.get('SIRIL')) + this.preference.liveStackingRequest('PIXINSIGHT').set(this.liveStackers.get('PIXINSIGHT')) } } \ No newline at end of file diff --git a/desktop/src/shared/pipes/dropdown-options.ts b/desktop/src/shared/pipes/dropdown-options.ts index f0e819e55..fc5431fee 100644 --- a/desktop/src/shared/pipes/dropdown-options.ts +++ b/desktop/src/shared/pipes/dropdown-options.ts @@ -21,7 +21,7 @@ export class DropdownOptionsPipe implements PipeTransform { case 'PLATE_SOLVER': return ['ASTAP', 'ASTROMETRY_NET_ONLINE'] case 'AUTO_FOCUS_FITTING_MODE': return ['TRENDLINES', 'PARABOLIC', 'TREND_PARABOLIC', 'HYPERBOLIC', 'TREND_HYPERBOLIC'] case 'AUTO_FOCUS_BACKLASH_COMPENSATION_MODE': return ['NONE', 'ABSOLUTE', 'OVERSHOOT'] - case 'LIVE_STACKER': return ['SIRIL'] + case 'LIVE_STACKER': return ['SIRIL', 'PIXINSIGHT'] case 'SCNR_PROTECTION_METHOD': return ['MAXIMUM_MASK', 'ADDITIVE_MASK', 'AVERAGE_NEUTRAL', 'MAXIMUM_NEUTRAL', 'MINIMUM_NEUTRAL'] case 'IMAGE_FORMAT': return ['FITS', 'XISF', 'PNG', 'JPG'] case 'IMAGE_BITPIX': return ['BYTE', 'SHORT', 'INTEGER', 'FLOAT', 'DOUBLE'] diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index b7526cb8f..01e2010c7 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -13,7 +13,7 @@ export type AutoSubFolderMode = 'OFF' | 'NOON' | 'MIDNIGHT' export type ExposureMode = 'SINGLE' | 'FIXED' | 'LOOP' -export type LiveStackerType = 'SIRIL' +export type LiveStackerType = 'SIRIL' | 'PIXINSIGHT' export enum ExposureTimeUnit { MINUTE = 'm', @@ -293,6 +293,7 @@ export interface LiveStackingRequest { executablePath: string, dark?: string, flat?: string, + bias?: string, rotate: number, use32Bits: boolean, } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt index f1bed390f..b6529f84a 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt @@ -1,11 +1,13 @@ package nebulosa.pixinsight.livestacking import nebulosa.livestacking.LiveStacker +import nebulosa.log.loggerFor import nebulosa.pixinsight.script.* import java.nio.file.Path import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.copyTo +import kotlin.io.path.deleteIfExists import kotlin.io.path.moveTo -import kotlin.io.path.name data class PixInsightLiveStacker( private val runner: PixInsightScriptRunner, @@ -27,8 +29,11 @@ data class PixInsightLiveStacker( get() = stacking.get() @Volatile private var stackCount = 0 - @Volatile private var referencePath: Path? = null - @Volatile private var stackedPath: Path? = null + + private val referencePath = Path.of("$workingDirectory", "reference.fits") + private val calibratedPath = Path.of("$workingDirectory", "calibrated.fits") + private val alignedPath = Path.of("$workingDirectory", "aligned.fits") + private val stackedPath = Path.of("$workingDirectory", "stacked.fits") @Synchronized override fun start() { @@ -41,9 +46,10 @@ data class PixInsightLiveStacker( } catch (e: Throwable) { throw IllegalStateException("unable to start PixInsight") } - - running.set(true) } + + stackCount = 0 + running.set(true) } } @@ -51,15 +57,15 @@ data class PixInsightLiveStacker( override fun add(path: Path): Path? { var targetPath = path - if (running.get()) { + return if (running.get()) { stacking.set(true) // Calibrate. val calibratedPath = if (dark == null && flat == null && bias == null) null else { - PixInsightCalibrate(slot, targetPath, dark, flat, if (dark == null) bias else null).use { - val outputPath = it.runSync(runner).outputImage ?: return@use null - val destinationPath = Path.of("$workingDirectory", outputPath.name) - outputPath.moveTo(destinationPath, true) + PixInsightCalibrate(slot, targetPath, dark, flat, if (dark == null) bias else null).use { s -> + val outputPath = s.runSync(runner).outputImage ?: return@use null + LOG.info("live stacking calibrated. count={}, image={}", stackCount, outputPath) + outputPath.moveTo(calibratedPath, true) } } @@ -71,10 +77,10 @@ data class PixInsightLiveStacker( if (stackCount > 0) { // Align. - val alignedPath = PixInsightAlign(slot, referencePath!!, targetPath).use { - val outputPath = it.runSync(runner).outputImage ?: return@use null - val destinationPath = Path.of("$workingDirectory", outputPath.name) - outputPath.moveTo(destinationPath, true) + val alignedPath = PixInsightAlign(slot, referencePath, targetPath).use { s -> + val outputPath = s.runSync(runner).outputImage ?: return@use null + LOG.info("live stacking aligned. count={}, image={}", stackCount, alignedPath) + outputPath.moveTo(alignedPath, true) } if (alignedPath != null) { @@ -82,26 +88,42 @@ data class PixInsightLiveStacker( } // Stack. + val expressionRK = "({{0}} * $stackCount + {{1}}) / ${stackCount + 1}" + PixInsightPixelMath(slot, listOf(stackedPath, targetPath), stackedPath, expressionRK).use { s -> + s.runSync(runner).stackedImage?.also { + LOG.info("live stacking finished. count={}, image={}", stackCount, it) + stackCount++ + } + } } else { - referencePath = targetPath + targetPath.copyTo(referencePath, true) + targetPath.copyTo(stackedPath, true) + stackCount = 1 } - stackedPath = targetPath - stackCount++ - stacking.set(false) - } - return stackedPath + stackedPath + } else { + path + } } @Synchronized override fun stop() { running.set(false) stackCount = 0 - referencePath = null - stackedPath = null } - override fun close() = Unit + override fun close() { + referencePath.deleteIfExists() + calibratedPath.deleteIfExists() + alignedPath.deleteIfExists() + // stackedPath.deleteIfExists() + } + + companion object { + + @JvmStatic val LOG = loggerFor() + } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt index 1519e89ff..1a530b94a 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -21,6 +21,8 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen protected abstract fun processOnComplete(exitCode: Int): T? + protected open fun waitOnComplete() = Unit + final override fun run(runner: PixInsightScriptRunner) = runner.run(this) final override fun startCommandLine(commandLine: CommandLine) { @@ -28,6 +30,8 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen try { LOG.info("PixInsight script finished. done={}, exitCode={}", isDone, exitCode, exception) + waitOnComplete() + if (isDone) return@whenComplete else if (exception != null) completeExceptionally(exception) else complete(processOnComplete(exitCode)) @@ -57,11 +61,21 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen } @JvmStatic - internal fun execute(slot: Int, scriptPath: Path, data: Any): String { + internal fun execute(slot: Int, scriptPath: Path, data: Any?): String { return buildString { if (slot > 0) append("$slot:") - append("\"$scriptPath,") - append(Hex.encodeHexString(OBJECT_MAPPER.writeValueAsBytes(data))) + append("\"$scriptPath") + + if (data != null) { + append(',') + + when (data) { + is Path, is CharSequence -> append("'$data'") + is Number -> append("$data") + else -> append(Hex.encodeHexString(OBJECT_MAPPER.writeValueAsBytes(data))) + } + } + append('"') } } diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js index 0d28d88de..ffd042246 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js @@ -79,7 +79,7 @@ function alignment() { P.outputDirectory = outputDirectory P.outputExtension = ".fits" P.outputPrefix = "" - P.outputPostfix = "_sa" + P.outputPostfix = "_a" P.maskPostfix = "_m" P.distortionMapPostfix = "_dm" P.outputSampleFormat = StarAlignment.prototype.SameAsTarget From b542ffea6f4313a0869ab52c15fcff6c064b191b Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 09:25:38 -0300 Subject: [PATCH 09/26] [api][desktop]: Implement pause/unpause on Camera Capture --- .../api/cameras/CameraCaptureExecutor.kt | 8 +++++ .../api/cameras/CameraCaptureState.kt | 2 ++ .../nebulosa/api/cameras/CameraCaptureTask.kt | 33 +++++++++++++++++-- .../nebulosa/api/cameras/CameraController.kt | 10 ++++++ .../nebulosa/api/cameras/CameraService.kt | 8 +++++ .../app/alignment/alignment.component.html | 4 +-- desktop/src/app/camera/camera.component.html | 7 ++++ desktop/src/app/camera/camera.component.ts | 16 +++++++++ .../camera-exposure.component.ts | 2 ++ desktop/src/shared/services/api.service.ts | 8 +++++ desktop/src/shared/types/camera.types.ts | 2 +- 11 files changed, 94 insertions(+), 6 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index 2f7db5453..572dd53cc 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -48,6 +48,14 @@ class CameraCaptureExecutor( } } + fun pause(camera: Camera) { + jobs.find { it.task.camera === camera }?.pause() + } + + fun unpause(camera: Camera) { + jobs.find { it.task.camera === camera }?.unpause() + } + fun stop(camera: Camera) { jobs.find { it.task.camera === camera }?.stop() } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt index 4052d019a..092c6a400 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt @@ -8,6 +8,8 @@ enum class CameraCaptureState { WAITING, SETTLING, DITHERING, + PAUSING, + PAUSED, EXPOSURE_FINISHED, CAPTURE_FINISHED, } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index 4b78b7e59..cfa8bf23d 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -11,6 +11,7 @@ import nebulosa.api.tasks.SplitTask import nebulosa.api.tasks.delay.DelayEvent import nebulosa.api.tasks.delay.DelayTask import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.common.concurrency.latch.PauseListener import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent @@ -20,6 +21,7 @@ import nebulosa.log.loggerFor import java.nio.file.Path import java.time.Duration import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicBoolean data class CameraCaptureTask( @JvmField val camera: Camera, @@ -29,7 +31,7 @@ data class CameraCaptureTask( private val exposureMaxRepeat: Int = 0, private val executor: Executor? = null, private val calibrationFrameProvider: CalibrationFrameProvider? = null, -) : AbstractTask(), Consumer, CameraEventAware { +) : AbstractTask(), Consumer, PauseListener, CameraEventAware { private val delayTask = DelayTask(request.exposureDelay) private val waitForSettleTask = WaitForSettleTask(guider) @@ -54,6 +56,8 @@ data class CameraCaptureTask( @Volatile private var exposureRepeatCount = 0 @Volatile private var liveStacker: LiveStacker? = null + private val pausing = AtomicBoolean() + init { delayTask.subscribe(this) cameraExposureTask.subscribe(this) @@ -68,6 +72,14 @@ data class CameraCaptureTask( cameraExposureTask.handleCameraEvent(event) } + override fun onPause(paused: Boolean) { + pausing.set(paused) + + if (paused) { + sendEvent(CameraCaptureState.PAUSING) + } + } + private fun LiveStackingRequest.processCalibrationGroup(): LiveStackingRequest { return if (calibrationFrameProvider != null && enabled && !request.calibrationGroup.isNullOrBlank() && (dark == null || flat == null) @@ -107,6 +119,9 @@ data class CameraCaptureTask( cameraExposureTask.reset() + pausing.set(false) + cancellationToken.listenToPause(this) + if (liveStacker == null && request.liveStacking.enabled && (request.isLoop || request.exposureAmount > 1 || exposureMaxRepeat > 1) ) { @@ -126,6 +141,12 @@ data class CameraCaptureTask( ((exposureMaxRepeat > 0 && exposureRepeatCount < exposureMaxRepeat) || (exposureMaxRepeat <= 0 && (request.isLoop || exposureCount < request.exposureAmount))) ) { + if (cancellationToken.isPaused) { + pausing.set(false) + sendEvent(CameraCaptureState.PAUSED) + cancellationToken.waitForPause() + } + if (exposureCount == 0) { sendEvent(CameraCaptureState.CAPTURE_STARTED) @@ -160,6 +181,9 @@ data class CameraCaptureTask( } } + pausing.set(false) + cancellationToken.unlistenToPause(this) + sendEvent(CameraCaptureState.CAPTURE_FINISHED) liveStacker?.close() @@ -216,12 +240,14 @@ data class CameraCaptureTask( captureProgress = (estimatedCaptureTime - captureRemainingTime).toNanos().toDouble() / estimatedCaptureTime.toNanos() } + val isExposureFinished = state == CameraCaptureState.EXPOSURE_FINISHED + val event = CameraCaptureEvent( - this, camera, state, request.exposureAmount, exposureCount, + this, camera, if (pausing.get() && !isExposureFinished) CameraCaptureState.PAUSING else state, request.exposureAmount, exposureCount, captureRemainingTime, captureElapsedTime, captureProgress, stepRemainingTime, stepElapsedTime, stepProgress, savedPath, liveStackedPath, - if (state == CameraCaptureState.EXPOSURE_FINISHED) request else null + if (isExposureFinished) request else null ) onNext(event) @@ -256,6 +282,7 @@ data class CameraCaptureTask( cameraExposureTask.reset() ditherAfterExposureTask.reset() + pausing.set(false) exposureRepeatCount = 0 } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt index 58f4e460b..38dd89fc6 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt @@ -61,6 +61,16 @@ class CameraController( @RequestBody body: CameraStartCaptureRequest, ) = cameraService.startCapture(camera, body) + @PutMapping("{camera}/capture/pause") + fun pauseCapture(camera: Camera) { + cameraService.pauseCapture(camera) + } + + @PutMapping("{camera}/capture/unpause") + fun unpauseCapture(camera: Camera) { + cameraService.unpauseCapture(camera) + } + @PutMapping("{camera}/capture/abort") fun abortCapture(camera: Camera) { cameraService.abortCapture(camera) diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt index ddd92e97a..f92b075bf 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt @@ -43,6 +43,14 @@ class CameraService( cameraCaptureExecutor.execute(camera, request.copy(savePath = savePath)) } + fun pauseCapture(camera: Camera) { + cameraCaptureExecutor.pause(camera) + } + + fun unpauseCapture(camera: Camera) { + cameraCaptureExecutor.unpause(camera) + } + @Synchronized fun abortCapture(camera: Camera) { cameraCaptureExecutor.stop(camera) diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index 3acb4adf8..db8a44ae7 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -88,8 +88,8 @@ } - +
+ @if (pausingOrPaused) { + + } @else if(!running) { + } + (`cameras/${camera.id}/capture/start`, data) } + cameraPauseCapture(camera: Camera) { + return this.http.put(`cameras/${camera.id}/capture/pause`) + } + + cameraUnpauseCapture(camera: Camera) { + return this.http.put(`cameras/${camera.id}/capture/unpause`) + } + cameraAbortCapture(camera: Camera) { return this.http.put(`cameras/${camera.id}/capture/abort`) } diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index 01e2010c7..389cbfea8 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -234,7 +234,7 @@ export interface CameraCaptureEvent extends MessageEvent { capture?: CameraStartCapture } -export type CameraCaptureState = 'IDLE' | 'CAPTURE_STARTED' | 'EXPOSURE_STARTED' | 'EXPOSURING' | 'WAITING' | 'SETTLING' | 'EXPOSURE_FINISHED' | 'CAPTURE_FINISHED' +export type CameraCaptureState = 'IDLE' | 'CAPTURE_STARTED' | 'EXPOSURE_STARTED' | 'EXPOSURING' | 'WAITING' | 'SETTLING' | 'PAUSING' | 'PAUSED' | 'EXPOSURE_FINISHED' | 'CAPTURE_FINISHED' export interface CameraDialogInput { mode: CameraDialogMode From ac5fa651902e9861842bded718c95b913632ef2c Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 17:51:27 -0300 Subject: [PATCH 10/26] [api]: Implement PixInsight ABE --- .../livestacking/PixInsightLiveStacker.kt | 4 +- .../pixinsight/script/PixInsightAlign.kt | 6 +- .../PixInsightAutomaticBackgroundExtractor.kt | 60 +++++++++++++++++ .../pixinsight/script/PixInsightCalibrate.kt | 6 +- .../pixinsight/script/PixInsightPixelMath.kt | 4 +- .../src/main/resources/pixinsight/ABE.js | 64 +++++++++++++++++++ .../main/resources/pixinsight/Calibrate.js | 12 ++-- .../main/resources/pixinsight/PixelMath.js | 6 +- .../src/test/kotlin/PixInsightScriptTest.kt | 12 +++- 9 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt create mode 100644 nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt index b6529f84a..51d195db8 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt @@ -62,7 +62,7 @@ data class PixInsightLiveStacker( // Calibrate. val calibratedPath = if (dark == null && flat == null && bias == null) null else { - PixInsightCalibrate(slot, targetPath, dark, flat, if (dark == null) bias else null).use { s -> + PixInsightCalibrate(slot, workingDirectory, targetPath, dark, flat, if (dark == null) bias else null).use { s -> val outputPath = s.runSync(runner).outputImage ?: return@use null LOG.info("live stacking calibrated. count={}, image={}", stackCount, outputPath) outputPath.moveTo(calibratedPath, true) @@ -77,7 +77,7 @@ data class PixInsightLiveStacker( if (stackCount > 0) { // Align. - val alignedPath = PixInsightAlign(slot, referencePath, targetPath).use { s -> + val alignedPath = PixInsightAlign(slot, workingDirectory, referencePath, targetPath).use { s -> val outputPath = s.runSync(runner).outputImage ?: return@use null LOG.info("live stacking aligned. count={}, image={}", stackCount, alignedPath) outputPath.moveTo(alignedPath, true) 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 f7306ee27..7cdff620f 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt @@ -5,12 +5,12 @@ import nebulosa.io.transferAndClose import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.deleteIfExists -import kotlin.io.path.deleteRecursively import kotlin.io.path.outputStream import kotlin.io.path.readText data class PixInsightAlign( private val slot: Int, + private val workingDirectory: Path, private val referencePath: Path, private val targetPath: Path, ) : AbstractPixInsightScript() { @@ -51,7 +51,6 @@ data class PixInsightAlign( } } - private val outputDirectory = Files.createTempDirectory("pi-align-") private val scriptPath = Files.createTempFile("pi-", ".js") private val statusPath = Files.createTempFile("pi-", ".txt") @@ -59,7 +58,7 @@ data class PixInsightAlign( resource("pixinsight/Align.js")!!.transferAndClose(scriptPath.outputStream()) } - override val arguments = listOf("-x=${execute(slot, scriptPath, Input(referencePath, targetPath, outputDirectory, statusPath))}") + override val arguments = listOf("-x=${execute(slot, scriptPath, Input(referencePath, targetPath, workingDirectory, statusPath))}") override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { @@ -80,6 +79,5 @@ data class PixInsightAlign( override fun close() { scriptPath.deleteIfExists() statusPath.deleteIfExists() - outputDirectory.deleteRecursively() } } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt new file mode 100644 index 000000000..1a7555d7d --- /dev/null +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt @@ -0,0 +1,60 @@ +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 PixInsightAutomaticBackgroundExtractor( + private val slot: Int, + private val targetPath: Path, + private val outputPath: Path, +) : AbstractPixInsightScript() { + + private data class Input( + @JvmField val targetPath: Path, + @JvmField val outputPath: Path, + @JvmField val statusPath: Path, + ) + + data class Output(@JvmField val outputImage: Path? = null) { + + companion object { + + @JvmField val FAILED = Output() + } + } + + private val scriptPath = Files.createTempFile("pi-", ".js") + private val statusPath = Files.createTempFile("pi-", ".txt") + + init { + resource("pixinsight/ABE.js")!!.transferAndClose(scriptPath.outputStream()) + } + + override val arguments = listOf("-x=${execute(slot, scriptPath, Input(targetPath, outputPath, statusPath))}") + + override fun processOnComplete(exitCode: Int): Output { + if (exitCode == 0) { + repeat(5) { + val text = statusPath.readText() + + if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { + return OBJECT_MAPPER.readValue(text.substring(1, text.length - 1), Output::class.java) + } + + Thread.sleep(1000) + } + } + + return Output.FAILED + } + + override fun close() { + scriptPath.deleteIfExists() + statusPath.deleteIfExists() + } +} diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt index 2ed40a104..b5f0249be 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -5,12 +5,12 @@ import nebulosa.io.transferAndClose import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.deleteIfExists -import kotlin.io.path.deleteRecursively import kotlin.io.path.outputStream import kotlin.io.path.readText 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, @@ -40,7 +40,6 @@ data class PixInsightCalibrate( } } - private val outputDirectory = Files.createTempDirectory("pi-calibrate-") private val scriptPath = Files.createTempFile("pi-", ".js") private val statusPath = Files.createTempFile("pi-", ".txt") @@ -49,7 +48,7 @@ data class PixInsightCalibrate( } override val arguments = - listOf("-x=${execute(slot, scriptPath, Input(targetPath, outputDirectory, statusPath, dark, flat, bias, compress, use32Bit))}") + listOf("-x=${execute(slot, scriptPath, Input(targetPath, workingDirectory, statusPath, dark, flat, bias, compress, use32Bit))}") override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { @@ -70,6 +69,5 @@ data class PixInsightCalibrate( override fun close() { scriptPath.deleteIfExists() statusPath.deleteIfExists() - outputDirectory.deleteRecursively() } } 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 cd207907e..d5618bcfe 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt @@ -18,7 +18,6 @@ data class PixInsightPixelMath( ) : AbstractPixInsightScript() { private data class Input( - @JvmField val outputDirectory: Path, @JvmField val statusPath: Path, @JvmField val inputPaths: List, @JvmField val outputPath: Path, @@ -37,7 +36,6 @@ data class PixInsightPixelMath( } } - private val outputDirectory = Files.createTempDirectory("pi-pixelmath-") private val scriptPath = Files.createTempFile("pi-", ".js") private val statusPath = Files.createTempFile("pi-", ".txt") @@ -46,7 +44,7 @@ data class PixInsightPixelMath( } override val arguments = - listOf("-x=${execute(slot, scriptPath, Input(outputDirectory, statusPath, inputPaths, outputPath, expressionRK, expressionG, expressionB))}") + listOf("-x=${execute(slot, scriptPath, Input(statusPath, inputPaths, outputPath, expressionRK, expressionG, expressionB))}") override fun processOnComplete(exitCode: Int): Output? { if (exitCode == 0) { diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js b/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js new file mode 100644 index 000000000..c1e9632bc --- /dev/null +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js @@ -0,0 +1,64 @@ +function decodeParams(hex) { + let decoded = '' + + for (let i = 0; i < hex.length; i += 2) { + decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + } + + return JSON.parse(decoded) +} + +function abe() { + const input = decodeParams(jsArguments[0]) + + const targetPath = input.targetPath + const outputPath = input.outputPath + const statusPath = input.statusPath + + console.writeln("targetPath=" + targetPath) + console.writeln("outputPath=" + outputPath) + console.writeln("statusPath=" + statusPath) + + const window = ImageWindow.open(targetPath)[0] + + const P = new AutomaticBackgroundExtractor + P.tolerance = 1.000 + P.deviation = 0.800 + P.unbalance = 1.800 + P.minBoxFraction = 0.050 + P.maxBackground = 1.0000 + P.minBackground = 0.0000 + P.useBrightnessLimits = false + P.polyDegree = 4 + P.boxSize = 5 + P.boxSeparation = 5 + P.modelImageSampleFormat = AutomaticBackgroundExtractor.prototype.f32 + P.abeDownsample = 2.00 + P.writeSampleBoxes = false + P.justTrySamples = false + P.targetCorrection = AutomaticBackgroundExtractor.prototype.Subtract + P.normalize = true + P.discardModel = true + P.replaceTarget = true + P.correctedImageId = "" + P.correctedImageSampleFormat = AutomaticBackgroundExtractor.prototype.SameAsTarget + P.verboseCoefficients = false + P.compareModel = false + P.compareFactor = 10.00 + + P.executeOn(window.mainView) + + window.saveAs(outputPath, false, false, false, false) + + window.forceClose() + + console.writeln("abe finished") + + const json = { + outputImage: outputPath, + } + + File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") +} + +abe() diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js index c5798cf75..b17130c5c 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -14,9 +14,9 @@ function calibrate() { const targetPath = input.targetPath const outputDirectory = input.outputDirectory const statusPath = input.statusPath - const masterDark = input.masterDark - const masterFlat = input.masterFlat - const masterBias = input.masterBias + const masterDark = input.masterDark || "" + const masterFlat = input.masterFlat || "" + const masterBias = input.masterBias || "" const compress = input.compress const use32Bit = input.use32Bit @@ -53,11 +53,11 @@ function calibrate() { [false, 0, 0, 0, 0, 0, 0, 0, 0] ] P.masterBiasEnabled = !!masterBias - P.masterBiasPath = masterBias || "" + P.masterBiasPath = masterBias P.masterDarkEnabled = !!masterDark - P.masterDarkPath = masterDark || "" + P.masterDarkPath = masterDark P.masterFlatEnabled = !!masterFlat - P.masterFlatPath = masterFlat || "" + P.masterFlatPath = masterFlat P.calibrateBias = false P.calibrateDark = false P.calibrateFlat = false diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js index c4d199db4..54cfc0a07 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js @@ -11,7 +11,6 @@ function decodeParams(hex) { function pixelMath() { const input = decodeParams(jsArguments[0]) - const outputDirectory = input.outputDirectory const statusPath = input.statusPath const inputPaths = input.inputPaths const outputPath = input.outputPath @@ -19,15 +18,14 @@ function pixelMath() { let expressionG = input.expressionG let expressionB = input.expressionB - console.writeln("outputDirectory=" + outputDirectory) console.writeln("statusPath=" + statusPath) console.writeln("inputPaths=" + inputPaths) console.writeln("outputPath=" + outputPath) const windows = [] - for(let i = 0; i < input.inputPaths.length; i++) { - windows.push(ImageWindow.open(input.inputPaths[i])[0]) + for(let i = 0; i < inputPaths.length; i++) { + windows.push(ImageWindow.open(inputPaths[i])[0]) } for(let i = 0; i < windows.length; i++) { diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index 6d7a06d96..8ed867538 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -1,4 +1,6 @@ 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.doubles.plusOrMinus import io.kotest.matchers.nulls.shouldNotBeNull @@ -15,6 +17,7 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { init { val runner = PixInsightScriptRunner(Path.of("PixInsight")) + val workingDirectory = tempdir("pi-").toPath() "startup" { PixInsightStartup(PixInsightScript.DEFAULT_SLOT) @@ -25,11 +28,11 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { .use { it.runSync(runner).shouldBeTrue() } } "calibrate" { - PixInsightCalibrate(PixInsightScript.UNSPECIFIED_SLOT, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) + PixInsightCalibrate(PixInsightScript.UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_DARK, PI_FLAT, PI_BIAS) .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().shouldExist() } } "align" { - PixInsightAlign(PixInsightScript.UNSPECIFIED_SLOT, PI_01_LIGHT, PI_02_LIGHT) + PixInsightAlign(PixInsightScript.UNSPECIFIED_SLOT, workingDirectory, PI_01_LIGHT, PI_02_LIGHT) .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull().shouldExist() } } "detect stars" { @@ -53,5 +56,10 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { PixInsightPixelMath(PixInsightScript.UNSPECIFIED_SLOT, listOf(PI_01_LIGHT, PI_02_LIGHT), outputPath, "{{0}} + {{1}}") .use { it.runSync(runner).also(::println).stackedImage.shouldNotBeNull().shouldExist() } } + "abe" { + val outputPath = tempfile("pi-", ".fits").toPath() + PixInsightAutomaticBackgroundExtractor(PixInsightScript.UNSPECIFIED_SLOT, PI_01_LIGHT, outputPath) + .use { it.runSync(runner).also(::println).outputImage.shouldNotBeNull() } + } } } From bbb3986cb332c6e3e09bc3e325807e50f5f96976 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 18:11:08 -0300 Subject: [PATCH 11/26] [api]: Improve PixInsight HFD --- .../src/main/resources/pixinsight/DetectStars.js | 13 +++++++++---- .../src/test/kotlin/PixInsightScriptTest.kt | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js index 129407d7e..8d5998f90 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js @@ -780,14 +780,19 @@ function computeHfr(image, s) { let a = 0 let b = 0 + const r = Math.min(s.rect.y1 - s.rect.y0, s.rect.x1 - s.rect.x0) / 2 + for(let y = s.rect.y0; y <= s.rect.y1; y++) { for(let x = s.rect.x0; x <= s.rect.x1; x++) { if(x >= 0 && x < image.width && y >= 0 && y < image.height) { - const p = image.sample(x, y) - const v = p - s.bkg const d = Math.sqrt((x - s.pos.x) * (x - s.pos.x) + (y - s.pos.y) * (y - s.pos.y)) - a += v * d - b += v + + if(d <= r) { + const p = image.sample(x, y) + const v = p - s.bkg + a += v * d + b += v + } } } } diff --git a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt index 8ed867538..2bc11d4d2 100644 --- a/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt +++ b/nebulosa-pixinsight/src/test/kotlin/PixInsightScriptTest.kt @@ -39,7 +39,7 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { PixInsightDetectStars(PixInsightScript.UNSPECIFIED_SLOT, PI_FOCUS_0) .use { it.runSync(runner).also(::println).stars } .map { it.hfd } - .average() shouldBe (9.03 plusOrMinus 1e-2) + .average() shouldBe (8.43 plusOrMinus 1e-2) PixInsightDetectStars(PixInsightScript.UNSPECIFIED_SLOT, PI_FOCUS_30000) .use { it.runSync(runner).also(::println).stars } @@ -49,7 +49,7 @@ class PixInsightScriptTest : AbstractFitsAndXisfTest() { PixInsightDetectStars(PixInsightScript.UNSPECIFIED_SLOT, PI_FOCUS_100000) .use { it.runSync(runner).also(::println).stars } .map { it.hfd } - .average() shouldBe (20.88 plusOrMinus 1e-2) + .average() shouldBe (18.35 plusOrMinus 1e-2) } "pixel math" { val outputPath = Files.createTempFile("pi-stacked-", ".fits") From 7de80a2c3ac2f13b816ac5312d213432a6bcb0a7 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 19:56:50 -0300 Subject: [PATCH 12/26] [desktop]: Fix show Astronomical Object dialog --- desktop/src/app/image/image.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index e9acad941..c372fd401 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -89,7 +89,7 @@ -
From 9561e0795e569ac101d3b2cca35ec408ff9d0122 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 19:57:12 -0300 Subject: [PATCH 13/26] [desktop]: Use toggle button for Camera calibration menu on Image --- desktop/src/app/image/image.component.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 68709727e..809a722ce 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -597,7 +597,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { for (let i = 3; i < this.calibrationMenuItem.items!.length; i++) { const item = this.calibrationMenuItem.items![i] item.checked = item.label === (name ?? 'None') - item.disabled = this.calibrationViaCamera } } @@ -621,9 +620,11 @@ export class ImageComponent implements AfterViewInit, OnDestroy { checked: this.transformation.calibrationGroup === name, disabled: this.calibrationViaCamera, command: async () => { - this.transformation.calibrationGroup = name - this.markCalibrationGroupItem(label) - await this.loadImage() + if (!this.calibrationViaCamera) { + this.transformation.calibrationGroup = name + this.markCalibrationGroupItem(label) + await this.loadImage() + } }, } } @@ -639,9 +640,10 @@ export class ImageComponent implements AfterViewInit, OnDestroy { menu.push({ label: 'Camera', icon: 'mdi mdi-camera-iris', - checked: this.calibrationViaCamera, - command: () => { - this.calibrationViaCamera = !this.calibrationViaCamera + toggleable: true, + toggled: this.calibrationViaCamera, + toggle: (e) => { + this.calibrationViaCamera = !!e.checked this.markCalibrationGroupItem(this.transformation.calibrationGroup) } }) From 63831f40e19ba46fda4854741ba58b69bd8d8ec5 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 20:05:24 -0300 Subject: [PATCH 14/26] [desktop]: Show exposure count on Image's subtitle --- desktop/src/app/image/image.component.ts | 9 +++++++++ desktop/src/shared/types/image.types.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 809a722ce..1be6be08f 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -543,6 +543,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.imageData.path = event.savedPath this.imageData.liveStackedPath = event.liveStackedPath this.imageData.capture = event.capture + this.imageData.exposureCount = event.exposureCount this.clearOverlay() this.loadImage(true) @@ -831,6 +832,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { await this.loadImageFromPath(path) } + let extraInfo = '' + + if (this.imageData.exposureCount) { + extraInfo += ` · ${this.imageData.exposureCount}` + } + if (this.imageData.title) { this.app.subTitle = this.imageData.title } else if (this.imageData.camera) { @@ -840,6 +847,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } else { this.app.subTitle = '' } + + this.app.subTitle += extraInfo } private async loadImageFromPath(path: string) { diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index 4960661d5..d4533e86d 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -132,6 +132,7 @@ export interface ImageData { source?: ImageSource title?: string capture?: CameraStartCapture + exposureCount?: number } export interface FOV { From 4c21edad767df23aea6de278c697439e09e92b22 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 20:05:43 -0300 Subject: [PATCH 15/26] [desktop]: Disable Image calibration on live stacking --- desktop/src/app/image/image.component.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 1be6be08f..102cd4bcf 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -478,6 +478,11 @@ export class ImageComponent implements AfterViewInit, OnDestroy { toggle: (event) => { if (event.originalEvent) { this.showLiveStackedImage = !!event.checked + + if (this.showLiveStackedImage) { + this.disableCalibration(true) + } + this.loadImage(true) } }, @@ -533,6 +538,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.showLiveStackedImage = true this.app.topMenu[0].toggled = true this.app.topMenu[0].visible = true + this.disableCalibration(true) } } else if (!event.liveStackedPath) { this.showLiveStackedImage = undefined From a58d6f8a98cb523712e686865823ab5e6bf4c64f Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 20:15:09 -0300 Subject: [PATCH 16/26] [desktop]: Add links for Mount remote controls --- desktop/src/app/mount/mount.component.html | 8 +++++++- desktop/src/shared/pipes/dropdown-options.ts | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html index 471ac72fc..a21d943c1 100644 --- a/desktop/src/app/mount/mount.component.html +++ b/desktop/src/app/mount/mount.component.html @@ -226,7 +226,7 @@
- @@ -244,6 +244,12 @@
+
+ Used with the mobile app + Used with the desktop app +
diff --git a/desktop/src/shared/pipes/dropdown-options.ts b/desktop/src/shared/pipes/dropdown-options.ts index fc5431fee..915938d42 100644 --- a/desktop/src/shared/pipes/dropdown-options.ts +++ b/desktop/src/shared/pipes/dropdown-options.ts @@ -3,14 +3,15 @@ import { AutoFocusFittingMode, BacklashCompensationMode } from '../types/autofoc import { LiveStackerType } from '../types/camera.types' import { Bitpix, ImageChannel, ImageFormat, SCNRProtectionMethod } from '../types/image.types' import { PlateSolverType, StarDetectorType } from '../types/settings.types' +import { MountRemoteControlType } from '../types/mount.types' export type DropdownOptionType = 'STAR_DETECTOR' | 'PLATE_SOLVER' | 'LIVE_STACKER' | 'AUTO_FOCUS_FITTING_MODE' | 'AUTO_FOCUS_BACKLASH_COMPENSATION_MODE' | 'SCNR_PROTECTION_METHOD' - | 'IMAGE_FORMAT' | 'IMAGE_BITPIX' | 'IMAGE_CHANNEL' + | 'IMAGE_FORMAT' | 'IMAGE_BITPIX' | 'IMAGE_CHANNEL' | 'MOUNT_REMOTE_CONTROL_TYPE' export type DropdownOptionReturnType = StarDetectorType[] | PlateSolverType[] | LiveStackerType[] | AutoFocusFittingMode[] | BacklashCompensationMode[] | SCNRProtectionMethod[] - | ImageFormat[] | Bitpix[] | ImageChannel[] + | ImageFormat[] | Bitpix[] | ImageChannel[] | MountRemoteControlType[] @Pipe({ name: 'dropdownOptions' }) export class DropdownOptionsPipe implements PipeTransform { @@ -26,6 +27,7 @@ export class DropdownOptionsPipe implements PipeTransform { case 'IMAGE_FORMAT': return ['FITS', 'XISF', 'PNG', 'JPG'] case 'IMAGE_BITPIX': return ['BYTE', 'SHORT', 'INTEGER', 'FLOAT', 'DOUBLE'] case 'IMAGE_CHANNEL': return ['RED', 'GREEN', 'BLUE', 'GRAY'] + case 'MOUNT_REMOTE_CONTROL_TYPE': return ['LX200', 'STELLARIUM'] } } } From 816241b894a70c703c8b0f2c4def6e0407d95135 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 20:33:10 -0300 Subject: [PATCH 17/26] [api][desktop]: Add slot option for PixInsight on Settings --- .../api/livestacking/LiveStackingRequest.kt | 3 +- .../api/stardetection/StarDetectionRequest.kt | 8 ++--- desktop/src/app/camera/camera.component.ts | 4 ++- .../src/app/sequencer/sequencer.component.ts | 3 +- .../src/app/settings/settings.component.html | 30 +++++++++++++++---- desktop/src/shared/types/camera.types.ts | 21 +++++++------ desktop/src/shared/types/settings.types.ts | 2 ++ 7 files changed, 48 insertions(+), 23 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt index 629772d60..62dd939e9 100644 --- a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt @@ -5,7 +5,6 @@ import nebulosa.api.beans.converters.angle.DegreesDeserializer import nebulosa.livestacking.LiveStacker import nebulosa.pixinsight.livestacking.PixInsightLiveStacker import nebulosa.pixinsight.script.PixInsightIsRunning -import nebulosa.pixinsight.script.PixInsightScript import nebulosa.pixinsight.script.PixInsightScriptRunner import nebulosa.pixinsight.script.PixInsightStartup import nebulosa.siril.livestacking.SirilLiveStacker @@ -23,7 +22,7 @@ data class LiveStackingRequest( @JvmField val bias: Path? = null, @JvmField @field:JsonDeserialize(using = DegreesDeserializer::class) val rotate: Double = 0.0, @JvmField val use32Bits: Boolean = false, - @JvmField val slot: Int = PixInsightScript.DEFAULT_SLOT, + @JvmField val slot: Int = 1, ) : Supplier { override fun get(): LiveStacker { diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt index d71e1f10a..489d4feec 100644 --- a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt @@ -2,7 +2,6 @@ package nebulosa.api.stardetection import nebulosa.astap.star.detection.AstapStarDetector import nebulosa.pixinsight.script.PixInsightIsRunning -import nebulosa.pixinsight.script.PixInsightScript import nebulosa.pixinsight.script.PixInsightScriptRunner import nebulosa.pixinsight.script.PixInsightStartup import nebulosa.pixinsight.star.detection.PixInsightStarDetector @@ -16,6 +15,7 @@ data class StarDetectionRequest( @JvmField val executablePath: Path? = null, @JvmField val timeout: Duration = Duration.ZERO, @JvmField val minSNR: Double = 0.0, + @JvmField val slot: Int = 1, ) : Supplier> { override fun get() = when (type) { @@ -23,13 +23,13 @@ data class StarDetectionRequest( StarDetectorType.PIXINSIGHT -> { val runner = PixInsightScriptRunner(executablePath!!) - if (!PixInsightIsRunning(PixInsightScript.DEFAULT_SLOT).use { it.runSync(runner) }) { - if (!PixInsightStartup(PixInsightScript.DEFAULT_SLOT).use { it.runSync(runner) }) { + if (!PixInsightIsRunning(slot).use { it.runSync(runner) }) { + if (!PixInsightStartup(slot).use { it.runSync(runner) }) { throw IllegalStateException("unable to start PixInsight") } } - PixInsightStarDetector(runner, PixInsightScript.DEFAULT_SLOT, minSNR, timeout) + PixInsightStarDetector(runner, slot, minSNR, timeout) } } diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 58d6d04b8..4aa9b489f 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -540,7 +540,9 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { const exposureAmount = this.exposureMode === 'LOOP' ? 0 : (this.exposureMode === 'FIXED' ? this.request.exposureAmount : 1) const savePath = this.mode !== 'CAPTURE' ? this.request.savePath : this.savePath - this.request.liveStacking.executablePath = this.preference.liveStackingRequest(this.request.liveStacking.type).get().executablePath + const liveStackingRequest = this.preference.liveStackingRequest(this.request.liveStacking.type).get() + this.request.liveStacking.executablePath = liveStackingRequest.executablePath + this.request.liveStacking.slot = liveStackingRequest.slot || 1 return { ...this.request, diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index c17bd92d8..8577473d3 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -301,7 +301,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable type: 'SIRIL', executablePath: '', rotate: 0, - use32Bits: false + use32Bits: false, + slot: 1, }, }) diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 14c632fce..38c8a9dd8 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -83,7 +83,12 @@
-
+
+ +
+
Min SNR
-
+
Timeout (s)
-
- +
+ + + +
@@ -121,6 +130,15 @@ [path]="liveStackers.get(liveStackerType)!.executablePath" label="Executable path" (pathChange)="liveStackers.get(liveStackerType)!.executablePath = $event; save()" />
+
+ + + + +
\ No newline at end of file diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index 389cbfea8..2da17ce42 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -199,6 +199,7 @@ export const EMPTY_CAMERA_START_CAPTURE: CameraStartCapture = { executablePath: "", rotate: 0, use32Bits: false, + slot: 1, } } @@ -288,14 +289,15 @@ export const EMPTY_CAMERA_CAPTURE_INFO: CameraCaptureInfo = { } export interface LiveStackingRequest { - enabled: boolean, - type: LiveStackerType, - executablePath: string, - dark?: string, - flat?: string, - bias?: string, - rotate: number, - use32Bits: boolean, + enabled: boolean + type: LiveStackerType + executablePath: string + dark?: string + flat?: string + bias?: string + rotate: number + use32Bits: boolean + slot: number } export const EMPTY_LIVE_STACKING_REQUEST: LiveStackingRequest = { @@ -303,5 +305,6 @@ export const EMPTY_LIVE_STACKING_REQUEST: LiveStackingRequest = { type: 'SIRIL', executablePath: '', rotate: 0, - use32Bits: false + use32Bits: false, + slot: 1, } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index 7e6b3fc4b..c048dda1d 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -25,6 +25,7 @@ export interface StarDetectionOptions { executablePath: string timeout: number minSNR: number + slot: number } export const EMPTY_STAR_DETECTION_OPTIONS: StarDetectionOptions = { @@ -32,4 +33,5 @@ export const EMPTY_STAR_DETECTION_OPTIONS: StarDetectionOptions = { executablePath: '', timeout: 600, minSNR: 0, + slot: 1, } From e589397616707e252fa831c09ed45dc1e3a9e913 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 21:18:22 -0300 Subject: [PATCH 18/26] [desktop]: Fix Guider bar chart --- desktop/src/app/guider/guider.component.html | 6 +++--- desktop/src/app/guider/guider.component.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index afdf8c25b..5eac1413e 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -127,11 +127,11 @@
+ icon="mdi mdi-play" severity="success" size="small" [text]="true" /> --> + severity="info" size="small" [text]="true" /> + severity="danger" size="small" [text]="true" />
diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index 8b05c6572..7eb97a0d9 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -70,7 +70,7 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { } readonly chartData: ChartData = { - labels: Array.from({ length: 100 }), + labels: Array.from({ length: 100 }, (_, i) => `${i}`), datasets: [ // RA. { @@ -188,7 +188,8 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { } }, x: { - stacked: false, + type: 'linear', + stacked: true, min: 0, max: 100, border: { From 8df1cee9cc5723c43eab1bfc61a8a811c8e73667 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 21:31:30 -0300 Subject: [PATCH 19/26] [desktop]: Improve Guider layout --- desktop/src/app/guider/guider.component.html | 233 +++++++++--------- .../shared/services/browser-window.service.ts | 2 +- 2 files changed, 118 insertions(+), 117 deletions(-) diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index 5eac1413e..ad692a1fe 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -1,8 +1,8 @@ -
+
-
+
- + + size="small" severity="info" [text]="true" />
-
-
{{ guideState | enum | lowercase }} {{ message }}
-
- - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
-
-
- -
- North - East -
-
- -
-
- - - - -
-
- - - - -
-
- - - - -
-
-
-
-
- - - -
+
+ + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ +
+ North + East +
+
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+
+ + + +
+
+
- +
-
- -
-
+
+
+ +
diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 215f407de..1d10e9b81 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -115,7 +115,7 @@ export class BrowserWindowService { } openSettings(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'settings', width: 340, height: 440 }) + Object.assign(options, { icon: 'settings', width: 400, height: 450 }) this.openWindow({ ...options, id: 'settings', path: 'settings', data: undefined, autoResizable: false }) } From 9ff238004e94f228c27e4e1a10147e34a2e4b744 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 21:37:45 -0300 Subject: [PATCH 20/26] [desktop]: Fix Camera calibration menu --- desktop/src/app/image/image.component.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 102cd4bcf..58075f525 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -599,8 +599,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private markCalibrationGroupItem(name?: string) { - this.calibrationMenuItem.items![1].checked = this.calibrationViaCamera - for (let i = 3; i < this.calibrationMenuItem.items!.length; i++) { const item = this.calibrationMenuItem.items![i] item.checked = item.label === (name ?? 'None') @@ -625,8 +623,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { return { label, icon, checked: this.transformation.calibrationGroup === name, - disabled: this.calibrationViaCamera, - command: async () => { + command: async (e) => { if (!this.calibrationViaCamera) { this.transformation.calibrationGroup = name this.markCalibrationGroupItem(label) @@ -650,6 +647,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { toggleable: true, toggled: this.calibrationViaCamera, toggle: (e) => { + e.originalEvent?.stopImmediatePropagation() this.calibrationViaCamera = !!e.checked this.markCalibrationGroupItem(this.transformation.calibrationGroup) } From f84e15dbbd6d60a56f1351e7946461173e77a756 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 6 Jun 2024 21:46:44 -0300 Subject: [PATCH 21/26] [desktop]: Improve layout for buttons --- .../src/app/alignment/alignment.component.html | 16 ++++++++-------- .../src/app/autofocus/autofocus.component.html | 4 ++-- desktop/src/app/camera/camera.component.html | 12 ++++++------ .../app/filterwheel/filterwheel.component.html | 2 +- .../app/flat-wizard/flat-wizard.component.html | 4 ++-- .../indi/property/indi-property.component.html | 11 ++++++----- .../src/app/sequencer/sequencer.component.html | 4 ++-- .../shared/services/browser-window.service.ts | 2 +- 8 files changed, 28 insertions(+), 27 deletions(-) diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index db8a44ae7..fcdc2d66b 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -83,15 +83,15 @@
@if (pausingOrPaused) { + severity="success" size="small" [text]="true" /> } @else if(!running) { + label="Start" (onClick)="tppaStart()" icon="mdi mdi-play" severity="success" size="small" [text]="true" /> } + size="small" [text]="true" /> + icon="mdi mdi-stop" severity="danger" size="small" [text]="true" />
@@ -124,15 +124,15 @@
+ icon="mdi mdi-play" severity="success" size="small" [text]="true" /> + icon="mdi mdi-stop" severity="danger" size="small" [text]="true" /> + styleClass="ml-4" pTooltip="View image" tooltipPosition="bottom" size="small" [text]="true" />
-
+
1. Locate a star near the south meridian and close to declination 0. 2. Start DARV and wait for routine to complete. 3. If you see V shaped track, adjust the Azimuth and repeat the step 2 till you get a line. diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 7162a69c9..bbad649d8 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -120,9 +120,9 @@
+ icon="mdi mdi-play" severity="success" size="small" [text]="true" /> + icon="mdi mdi-stop" severity="danger" size="small" [text]="true" />
diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 17d9d74de..3accc3f0e 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -156,7 +156,7 @@
+ severity="info" size="small" pTooltip="Full size" tooltipPosition="bottom" [text]="true" />
@@ -205,18 +205,18 @@
@if (pausingOrPaused) { + severity="success" size="small" [text]="true" /> } @else if(!running) { + tooltipStyleClass="min-w-22rem flex justify-content-center" [text]="true" /> } + severity="info" size="small" [text]="true" /> + severity="danger" size="small" [text]="true" /> + severity="info" size="small" [text]="true" />
diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index 9ae270e7d..eb8b6f42c 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -47,7 +47,7 @@
+ (onClick)="moveToSelectedFilter()" size="small" [text]="true" />
diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.html b/desktop/src/app/flat-wizard/flat-wizard.component.html index 9c6bf6135..8fb92158c 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.html +++ b/desktop/src/app/flat-wizard/flat-wizard.component.html @@ -67,9 +67,9 @@
+ severity="success" size="small" [text]="true" /> + severity="danger" size="small" [text]="true" />
\ No newline at end of file diff --git a/desktop/src/app/indi/property/indi-property.component.html b/desktop/src/app/indi/property/indi-property.component.html index 7b5c9c3f6..84098b176 100644 --- a/desktop/src/app/indi/property/indi-property.component.html +++ b/desktop/src/app/indi/property/indi-property.component.html @@ -6,17 +6,17 @@
+ (onClick)="sendSwitch(item)" icon="pi" [severity]="item.value ? 'success' : 'danger'" size="small" [text]="true">
+ (onClick)="sendSwitch(item)" icon="mdi mdi-check" size="small" [text]="true" />
+ (onClick)="sendSwitch(item)" icon="pi" [severity]="item.value ? 'success' : 'danger'" size="small" [text]="true">
@@ -39,7 +39,7 @@
+ size="small" [text]="true" />
@@ -60,7 +60,8 @@
- +
diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index 52c823755..b8c5b9344 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -226,9 +226,9 @@
+ severity="success" size="small" [text]="true" /> + severity="danger" size="small" [text]="true" />
diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 1d10e9b81..4a7e38b4e 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -95,7 +95,7 @@ export class BrowserWindowService { } openAlignment(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'star', width: 415, height: 365 }) + Object.assign(options, { icon: 'star', width: 425, height: 365 }) this.openWindow({ ...options, id: 'alignment', path: 'alignment', data: undefined }) } From 9ec66ec2ecaf06a5c857bb4f76755184dc9277ac Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 7 Jun 2024 11:47:31 -0300 Subject: [PATCH 22/26] [api]: Fix invalid parameter encoding; Handle error on PixInsight scripts --- .../script/AbstractPixInsightScript.kt | 4 +- .../pixinsight/script/PixInsightAlign.kt | 2 + .../PixInsightAutomaticBackgroundExtractor.kt | 6 +- .../pixinsight/script/PixInsightCalibrate.kt | 2 + .../script/PixInsightDetectStars.kt | 2 + .../pixinsight/script/PixInsightPixelMath.kt | 2 + .../src/main/resources/pixinsight/ABE.js | 98 +- .../src/main/resources/pixinsight/Align.js | 247 +-- .../main/resources/pixinsight/Calibrate.js | 196 ++- .../main/resources/pixinsight/DetectStars.js | 1496 ++++++++--------- .../main/resources/pixinsight/PixelMath.js | 152 +- 11 files changed, 1125 insertions(+), 1082 deletions(-) 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 1a530b94a..f93800306 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, LineReadListen if (isDone) return@whenComplete else if (exception != null) completeExceptionally(exception) - else complete(processOnComplete(exitCode)) + else complete(processOnComplete(exitCode).also { LOG.info("script processed. output={}", it) }) } finally { commandLine.unregisterLineReadListener(this) } @@ -72,7 +72,7 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen when (data) { is Path, is CharSequence -> append("'$data'") is Number -> append("$data") - else -> append(Hex.encodeHexString(OBJECT_MAPPER.writeValueAsBytes(data))) + else -> append(Hex.encodeHexString(OBJECT_MAPPER.writeValueAsString(data).toByteArray(Charsets.UTF_16BE))) } } 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 7cdff620f..82d4ae4eb 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt @@ -23,6 +23,8 @@ data class PixInsightAlign( ) data class Output( + @JvmField val success: Boolean = false, + @JvmField val errorMessage: String? = null, @JvmField val outputImage: Path? = null, @JvmField val outputMaskImage: Path? = null, @JvmField val totalPairMatches: Int = 0, 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 1a7555d7d..b06e3d6d8 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt @@ -20,7 +20,11 @@ data class PixInsightAutomaticBackgroundExtractor( @JvmField val statusPath: Path, ) - data class Output(@JvmField val outputImage: Path? = null) { + data class Output( + @JvmField val success: Boolean = false, + @JvmField val errorMessage: String? = null, + @JvmField val outputImage: Path? = null, + ) { 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 b5f0249be..9dd204749 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -31,6 +31,8 @@ data class PixInsightCalibrate( ) data class Output( + @JvmField val success: Boolean = false, + @JvmField val errorMessage: String? = null, @JvmField val outputImage: Path? = null, ) { 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 01ed54d94..4c3e58a40 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt @@ -26,6 +26,8 @@ data class PixInsightDetectStars( ) data class Output( + @JvmField val success: Boolean = false, + @JvmField val errorMessage: String? = null, @JvmField val stars: List = emptyList(), ) { 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 d5618bcfe..30eb3e1aa 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt @@ -27,6 +27,8 @@ data class PixInsightPixelMath( ) data class Output( + @JvmField val success: Boolean = false, + @JvmField val errorMessage: String? = null, @JvmField val stackedImage: Path? = null, ) { diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js b/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js index c1e9632bc..9ffb6e97c 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/ABE.js @@ -1,64 +1,74 @@ function decodeParams(hex) { - let decoded = '' + const buffer = new Uint8Array(hex.length / 4) - for (let i = 0; i < hex.length; i += 2) { - decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + for (let i = 0; i < hex.length; i += 4) { + buffer[i / 4] = parseInt(hex.substr(i, 4), 16) } - return JSON.parse(decoded) + return JSON.parse(String.fromCharCode.apply(null, buffer)) } function abe() { - const input = decodeParams(jsArguments[0]) + const data = { + success: true, + errorMessage: null, + outputImage: null, + } - const targetPath = input.targetPath - const outputPath = input.outputPath - const statusPath = input.statusPath + try { + const input = decodeParams(jsArguments[0]) - console.writeln("targetPath=" + targetPath) - console.writeln("outputPath=" + outputPath) - console.writeln("statusPath=" + statusPath) + const targetPath = input.targetPath + const outputPath = input.outputPath + const statusPath = input.statusPath - const window = ImageWindow.open(targetPath)[0] + console.writeln("targetPath=" + targetPath) + console.writeln("outputPath=" + outputPath) + console.writeln("statusPath=" + statusPath) - const P = new AutomaticBackgroundExtractor - P.tolerance = 1.000 - P.deviation = 0.800 - P.unbalance = 1.800 - P.minBoxFraction = 0.050 - P.maxBackground = 1.0000 - P.minBackground = 0.0000 - P.useBrightnessLimits = false - P.polyDegree = 4 - P.boxSize = 5 - P.boxSeparation = 5 - P.modelImageSampleFormat = AutomaticBackgroundExtractor.prototype.f32 - P.abeDownsample = 2.00 - P.writeSampleBoxes = false - P.justTrySamples = false - P.targetCorrection = AutomaticBackgroundExtractor.prototype.Subtract - P.normalize = true - P.discardModel = true - P.replaceTarget = true - P.correctedImageId = "" - P.correctedImageSampleFormat = AutomaticBackgroundExtractor.prototype.SameAsTarget - P.verboseCoefficients = false - P.compareModel = false - P.compareFactor = 10.00 + const window = ImageWindow.open(targetPath)[0] - P.executeOn(window.mainView) + const P = new AutomaticBackgroundExtractor + P.tolerance = 1.000 + P.deviation = 0.800 + P.unbalance = 1.800 + P.minBoxFraction = 0.050 + P.maxBackground = 1.0000 + P.minBackground = 0.0000 + P.useBrightnessLimits = false + P.polyDegree = 4 + P.boxSize = 5 + P.boxSeparation = 5 + P.modelImageSampleFormat = AutomaticBackgroundExtractor.prototype.f32 + P.abeDownsample = 2.00 + P.writeSampleBoxes = false + P.justTrySamples = false + P.targetCorrection = AutomaticBackgroundExtractor.prototype.Subtract + P.normalize = true + P.discardModel = true + P.replaceTarget = true + P.correctedImageId = "" + P.correctedImageSampleFormat = AutomaticBackgroundExtractor.prototype.SameAsTarget + P.verboseCoefficients = false + P.compareModel = false + P.compareFactor = 10.00 - window.saveAs(outputPath, false, false, false, false) + P.executeOn(window.mainView) - window.forceClose() + window.saveAs(outputPath, false, false, false, false) - console.writeln("abe finished") + window.forceClose() - const json = { - outputImage: outputPath, - } + data.outputImage = outputPath - File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") + console.writeln("abe finished") + } catch (e) { + data.success = false + data.errorMessage = e.message + console.writeln(data.errorMessage) + } finally { + File.writeTextFile(statusPath, "@" + JSON.stringify(data) + "#") + } } abe() diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js index ffd042246..f8f93f38c 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Align.js @@ -1,126 +1,155 @@ function decodeParams(hex) { - let decoded = '' + const buffer = new Uint8Array(hex.length / 4) - for (let i = 0; i < hex.length; i += 2) { - decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + for (let i = 0; i < hex.length; i += 4) { + buffer[i / 4] = parseInt(hex.substr(i, 4), 16) } - return JSON.parse(decoded) + return JSON.parse(String.fromCharCode.apply(null, buffer)) } function alignment() { - const input = decodeParams(jsArguments[0]) + const data = { + success: true, + errorMessage: null, + outputImage: null, + outputMaskImage: null, + totalPairMatches: 0, + inliers: 0, + overlapping: 0, + regularity: 0, + quality: 0, + rmsError: 0, + rmsErrorDev: 0, + peakErrorX: 0, + peakErrorY: 0, + h11: 0, + h12: 0, + h13: 0, + h21: 0, + h22: 0, + h23: 0, + h31: 0, + h32: 0, + h33: 0, + } - const referencePath = input.referencePath - const targetPath = input.targetPath - const outputDirectory = input.outputDirectory - const statusPath = input.statusPath + try { + const input = decodeParams(jsArguments[0]) - console.writeln("referencePath=" + referencePath) - console.writeln("targetPath=" + targetPath) - console.writeln("outputDirectory=" + outputDirectory) - console.writeln("statusPath=" + statusPath) + const referencePath = input.referencePath + const targetPath = input.targetPath + const outputDirectory = input.outputDirectory + const statusPath = input.statusPath - var P = new StarAlignment - P.structureLayers = 5 - P.noiseLayers = 0 - P.hotPixelFilterRadius = 1 - P.noiseReductionFilterRadius = 0 - P.minStructureSize = 0 - P.sensitivity = 0.50 - P.peakResponse = 0.50 - P.brightThreshold = 3.00 - P.maxStarDistortion = 0.60 - P.allowClusteredSources = false - P.localMaximaDetectionLimit = 0.75 - P.upperLimit = 1.000 - P.invert = false - P.distortionModel = "" - P.undistortedReference = false - P.distortionCorrection = false - P.distortionMaxIterations = 20 - P.distortionMatcherExpansion = 1.00 - P.splineOrder = 2 - P.splineSmoothness = 0.005 - P.matcherTolerance = 0.0500 - P.ransacMaxIterations = 2000 - P.ransacMaximizeInliers = 1.00 - P.ransacMaximizeOverlapping = 1.00 - P.ransacMaximizeRegularity = 1.00 - P.ransacMinimizeError = 1.00 - P.maxStars = 0 - P.fitPSF = StarAlignment.prototype.FitPSF_DistortionOnly - P.psfTolerance = 0.50 - P.useTriangles = false - P.polygonSides = 5 - P.descriptorsPerStar = 20 - P.restrictToPreviews = true - P.intersection = StarAlignment.prototype.MosaicOnly - P.useBrightnessRelations = false - P.useScaleDifferences = false - P.scaleTolerance = 0.100 - P.referenceImage = referencePath - P.referenceIsFile = true - P.targets = [ // enabled, isFile, image - [true, true, targetPath] - ] - P.inputHints = "" - P.outputHints = "" - P.mode = StarAlignment.prototype.RegisterMatch - P.writeKeywords = true - P.generateMasks = false - P.generateDrizzleData = false - P.generateDistortionMaps = false - P.inheritAstrometricSolution = false - P.frameAdaptation = false - P.randomizeMosaic = false - P.pixelInterpolation = StarAlignment.prototype.Auto - P.clampingThreshold = 0.30 - P.outputDirectory = outputDirectory - P.outputExtension = ".fits" - P.outputPrefix = "" - P.outputPostfix = "_a" - P.maskPostfix = "_m" - P.distortionMapPostfix = "_dm" - P.outputSampleFormat = StarAlignment.prototype.SameAsTarget - P.overwriteExistingFiles = true - P.onError = StarAlignment.prototype.Continue - P.useFileThreads = true - P.noGUIMessages = true - P.fileThreadOverload = 1.00 - P.maxFileReadThreads = 0 - P.maxFileWriteThreads = 0 - P.memoryLoadControl = true - P.memoryLoadLimit = 0.85 + console.writeln("referencePath=" + referencePath) + console.writeln("targetPath=" + targetPath) + console.writeln("outputDirectory=" + outputDirectory) + console.writeln("statusPath=" + statusPath) - P.executeGlobal() + var P = new StarAlignment + P.structureLayers = 5 + P.noiseLayers = 0 + P.hotPixelFilterRadius = 1 + P.noiseReductionFilterRadius = 0 + P.minStructureSize = 0 + P.sensitivity = 0.50 + P.peakResponse = 0.50 + P.brightThreshold = 3.00 + P.maxStarDistortion = 0.60 + P.allowClusteredSources = false + P.localMaximaDetectionLimit = 0.75 + P.upperLimit = 1.000 + P.invert = false + P.distortionModel = "" + P.undistortedReference = false + P.distortionCorrection = false + P.distortionMaxIterations = 20 + P.distortionMatcherExpansion = 1.00 + P.splineOrder = 2 + P.splineSmoothness = 0.005 + P.matcherTolerance = 0.0500 + P.ransacMaxIterations = 2000 + P.ransacMaximizeInliers = 1.00 + P.ransacMaximizeOverlapping = 1.00 + P.ransacMaximizeRegularity = 1.00 + P.ransacMinimizeError = 1.00 + P.maxStars = 0 + P.fitPSF = StarAlignment.prototype.FitPSF_DistortionOnly + P.psfTolerance = 0.50 + P.useTriangles = false + P.polygonSides = 5 + P.descriptorsPerStar = 20 + P.restrictToPreviews = true + P.intersection = StarAlignment.prototype.MosaicOnly + P.useBrightnessRelations = false + P.useScaleDifferences = false + P.scaleTolerance = 0.100 + P.referenceImage = referencePath + P.referenceIsFile = true + P.targets = [ // enabled, isFile, image + [true, true, targetPath] + ] + P.inputHints = "" + P.outputHints = "" + P.mode = StarAlignment.prototype.RegisterMatch + P.writeKeywords = true + P.generateMasks = false + P.generateDrizzleData = false + P.generateDistortionMaps = false + P.inheritAstrometricSolution = false + P.frameAdaptation = false + P.randomizeMosaic = false + P.pixelInterpolation = StarAlignment.prototype.Auto + P.clampingThreshold = 0.30 + P.outputDirectory = outputDirectory + P.outputExtension = ".fits" + P.outputPrefix = "" + P.outputPostfix = "_a" + P.maskPostfix = "_m" + P.distortionMapPostfix = "_dm" + P.outputSampleFormat = StarAlignment.prototype.SameAsTarget + P.overwriteExistingFiles = true + P.onError = StarAlignment.prototype.Continue + P.useFileThreads = true + P.noGUIMessages = true + P.fileThreadOverload = 1.00 + P.maxFileReadThreads = 0 + P.maxFileWriteThreads = 0 + P.memoryLoadControl = true + P.memoryLoadLimit = 0.85 - console.writeln("alignment finished") + P.executeGlobal() - const json = { - outputImage: P.outputData[0][0] || null, - outputMaskImage: P.outputData[0][1] || null, - totalPairMatches: P.outputData[0][2], - inliers: P.outputData[0][3], - overlapping: P.outputData[0][4], - regularity: P.outputData[0][5], - quality: P.outputData[0][6], - rmsError: P.outputData[0][7], - rmsErrorDev: P.outputData[0][8], - peakErrorX: P.outputData[0][9], - peakErrorY: P.outputData[0][10], - h11: P.outputData[0][11], - h12: P.outputData[0][12], - h13: P.outputData[0][13], - h21: P.outputData[0][14], - h22: P.outputData[0][15], - h23: P.outputData[0][16], - h31: P.outputData[0][17], - h32: P.outputData[0][18], - h33: P.outputData[0][19], - } + data.outputImage = P.outputData[0][0] || null + data.outputMaskImage = P.outputData[0][1] || null + data.totalPairMatches = P.outputData[0][2] + data.inliers = P.outputData[0][3] + data.overlapping = P.outputData[0][4] + data.regularity = P.outputData[0][5] + data.quality = P.outputData[0][6] + data.rmsError = P.outputData[0][7] + data.rmsErrorDev = P.outputData[0][8] + data.peakErrorX = P.outputData[0][9] + data.peakErrorY = P.outputData[0][10] + data.h11 = P.outputData[0][11] + data.h12 = P.outputData[0][12] + data.h13 = P.outputData[0][13] + data.h21 = P.outputData[0][14] + data.h22 = P.outputData[0][15] + data.h23 = P.outputData[0][16] + data.h31 = P.outputData[0][17] + data.h32 = P.outputData[0][18] + data.h33 = P.outputData[0][19] - File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") + console.writeln("alignment finished") + } catch (e) { + data.success = false + data.errorMessage = e.message + console.writeln(data.errorMessage) + } finally { + File.writeTextFile(statusPath, "@" + JSON.stringify(data) + "#") + } } alignment() diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js index b17130c5c..814586b86 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/Calibrate.js @@ -1,111 +1,121 @@ function decodeParams(hex) { - let decoded = '' + const buffer = new Uint8Array(hex.length / 4) - for (let i = 0; i < hex.length; i += 2) { - decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + for (let i = 0; i < hex.length; i += 4) { + buffer[i / 4] = parseInt(hex.substr(i, 4), 16) } - return JSON.parse(decoded) + return JSON.parse(String.fromCharCode.apply(null, buffer)) } function calibrate() { - const input = decodeParams(jsArguments[0]) + const data = { + success: true, + errorMessage: null, + outputImage: null, + } - const targetPath = input.targetPath - const outputDirectory = input.outputDirectory - const statusPath = input.statusPath - const masterDark = input.masterDark || "" - const masterFlat = input.masterFlat || "" - const masterBias = input.masterBias || "" - const compress = input.compress - const use32Bit = input.use32Bit + try { + const input = decodeParams(jsArguments[0]) - console.writeln("targetPath=" + targetPath) - console.writeln("outputDirectory=" + outputDirectory) - console.writeln("statusPath=" + statusPath) - console.writeln("masterDark=" + masterDark) - console.writeln("masterFlat=" + masterFlat) - console.writeln("masterBias=" + masterBias) - console.writeln("compress=" + compress) - console.writeln("use32Bit=" + use32Bit) + const targetPath = input.targetPath + const outputDirectory = input.outputDirectory + const statusPath = input.statusPath + const masterDark = input.masterDark || "" + const masterFlat = input.masterFlat || "" + const masterBias = input.masterBias || "" + const compress = input.compress + const use32Bit = input.use32Bit - const P = new ImageCalibration + console.writeln("targetPath=" + targetPath) + console.writeln("outputDirectory=" + outputDirectory) + console.writeln("statusPath=" + statusPath) + console.writeln("masterDark=" + masterDark) + console.writeln("masterFlat=" + masterFlat) + console.writeln("masterBias=" + masterBias) + console.writeln("compress=" + compress) + console.writeln("use32Bit=" + use32Bit) - P.targetFrames = [ // enabled, path - [true, targetPath] - ] - P.enableCFA = true - P.cfaPattern = ImageCalibration.prototype.Auto - P.inputHints = "fits-keywords normalize raw cfa signed-is-physical" - P.outputHints = "properties fits-keywords no-compress-data no-embedded-data no-resolution" - P.pedestal = 0 - P.pedestalMode = ImageCalibration.prototype.Keyword - P.pedestalKeyword = "" - P.overscanEnabled = false - P.overscanImageX0 = 0 - P.overscanImageY0 = 0 - P.overscanImageX1 = 0 - P.overscanImageY1 = 0 - P.overscanRegions = [ // enabled, sourceX0, sourceY0, sourceX1, sourceY1, targetX0, targetY0, targetX1, targetY1 - [false, 0, 0, 0, 0, 0, 0, 0, 0], - [false, 0, 0, 0, 0, 0, 0, 0, 0], - [false, 0, 0, 0, 0, 0, 0, 0, 0], - [false, 0, 0, 0, 0, 0, 0, 0, 0] - ] - P.masterBiasEnabled = !!masterBias - P.masterBiasPath = masterBias - P.masterDarkEnabled = !!masterDark - P.masterDarkPath = masterDark - P.masterFlatEnabled = !!masterFlat - P.masterFlatPath = masterFlat - P.calibrateBias = false - P.calibrateDark = false - P.calibrateFlat = false - P.optimizeDarks = false - P.darkOptimizationThreshold = 0.00000 - P.darkOptimizationLow = 3.0000 - P.darkOptimizationWindow = 0 - P.darkCFADetectionMode = ImageCalibration.prototype.DetectCFA - P.separateCFAFlatScalingFactors = true - P.flatScaleClippingFactor = 0.05 - P.evaluateNoise = false - P.noiseEvaluationAlgorithm = ImageCalibration.prototype.NoiseEvaluation_MRS - P.evaluateSignal = false - P.structureLayers = 5 - P.saturationThreshold = 1.00 - P.saturationRelative = false - P.noiseLayers = 1 - P.hotPixelFilterRadius = 1 - P.noiseReductionFilterRadius = 0 - P.minStructureSize = 0 - P.psfType = ImageCalibration.prototype.PSFType_Moffat4 - P.psfGrowth = 1.00 - P.maxStars = 24576 - P.outputDirectory = outputDirectory - P.outputExtension = ".fits" - P.outputPrefix = "" - P.outputPostfix = "_c" - P.outputSampleFormat = use32Bit ? ImageCalibration.prototype.f32 : ImageCalibration.prototype.i16 - P.outputPedestal = 0 - P.outputPedestalMode = ImageCalibration.prototype.OutputPedestal_Literal - P.autoPedestalLimit = 0.00010 - P.overwriteExistingFiles = true - P.onError = ImageCalibration.prototype.Continue - P.noGUIMessages = true - P.useFileThreads = true - P.fileThreadOverload = 1.00 - P.maxFileReadThreads = 0 - P.maxFileWriteThreads = 0 + const P = new ImageCalibration - P.executeGlobal() + P.targetFrames = [ // enabled, path + [true, targetPath] + ] + P.enableCFA = true + P.cfaPattern = ImageCalibration.prototype.Auto + P.inputHints = "fits-keywords normalize raw cfa signed-is-physical" + P.outputHints = "properties fits-keywords no-compress-data no-embedded-data no-resolution" + P.pedestal = 0 + P.pedestalMode = ImageCalibration.prototype.Keyword + P.pedestalKeyword = "" + P.overscanEnabled = false + P.overscanImageX0 = 0 + P.overscanImageY0 = 0 + P.overscanImageX1 = 0 + P.overscanImageY1 = 0 + P.overscanRegions = [ // enabled, sourceX0, sourceY0, sourceX1, sourceY1, targetX0, targetY0, targetX1, targetY1 + [false, 0, 0, 0, 0, 0, 0, 0, 0], + [false, 0, 0, 0, 0, 0, 0, 0, 0], + [false, 0, 0, 0, 0, 0, 0, 0, 0], + [false, 0, 0, 0, 0, 0, 0, 0, 0] + ] + P.masterBiasEnabled = !!masterBias + P.masterBiasPath = masterBias + P.masterDarkEnabled = !!masterDark + P.masterDarkPath = masterDark + P.masterFlatEnabled = !!masterFlat + P.masterFlatPath = masterFlat + P.calibrateBias = false + P.calibrateDark = false + P.calibrateFlat = false + P.optimizeDarks = false + P.darkOptimizationThreshold = 0.00000 + P.darkOptimizationLow = 3.0000 + P.darkOptimizationWindow = 0 + P.darkCFADetectionMode = ImageCalibration.prototype.DetectCFA + P.separateCFAFlatScalingFactors = true + P.flatScaleClippingFactor = 0.05 + P.evaluateNoise = false + P.noiseEvaluationAlgorithm = ImageCalibration.prototype.NoiseEvaluation_MRS + P.evaluateSignal = false + P.structureLayers = 5 + P.saturationThreshold = 1.00 + P.saturationRelative = false + P.noiseLayers = 1 + P.hotPixelFilterRadius = 1 + P.noiseReductionFilterRadius = 0 + P.minStructureSize = 0 + P.psfType = ImageCalibration.prototype.PSFType_Moffat4 + P.psfGrowth = 1.00 + P.maxStars = 24576 + P.outputDirectory = outputDirectory + P.outputExtension = ".fits" + P.outputPrefix = "" + P.outputPostfix = "_c" + P.outputSampleFormat = use32Bit ? ImageCalibration.prototype.f32 : ImageCalibration.prototype.i16 + P.outputPedestal = 0 + P.outputPedestalMode = ImageCalibration.prototype.OutputPedestal_Literal + P.autoPedestalLimit = 0.00010 + P.overwriteExistingFiles = true + P.onError = ImageCalibration.prototype.Continue + P.noGUIMessages = true + P.useFileThreads = true + P.fileThreadOverload = 1.00 + P.maxFileReadThreads = 0 + P.maxFileWriteThreads = 0 - console.writeln("calibration finished") + P.executeGlobal() - const json = { - outputImage: P.outputData[0][0] || null, - } + data.outputImage = P.outputData[0][0] || null - File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") + console.writeln("calibration finished") + } catch (e) { + data.success = false + data.errorMessage = e.message + console.writeln(data.errorMessage) + } finally { + File.writeTextFile(statusPath, "@" + JSON.stringify(data) + "#") + } } calibrate() diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js index 8d5998f90..45b04f3e6 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/DetectStars.js @@ -53,725 +53,691 @@ #include #include -function Star( pos, flux, bkg, rect, size, nmax, snr, peak ) -{ - // Centroid position in pixels, image coordinates. This property is an - // object with x and y Number properties. - this.pos = pos - // Total flux, normalized intensity units. - this.flux = flux - // Mean local background, normalized intensity units. - this.bkg = bkg - // Detection region, image coordinates. - this.rect = rect - // Area of detected star structure in square pixels. - this.size = size - // Number of local maxima in the detection structure. A value greater than - // one denotes a double/multiple star or a crowded source. A value of zero - // signals that detection of local maxima has been disabled, either globally - // or for this particular structure. - this.nmax = nmax - - this.snr = snr - this.peak = peak +function Star(pos, flux, bkg, rect, size, nmax, snr, peak) { + // Centroid position in pixels, image coordinates. This property is an + // object with x and y Number properties. + this.pos = pos + // Total flux, normalized intensity units. + this.flux = flux + // Mean local background, normalized intensity units. + this.bkg = bkg + // Detection region, image coordinates. + this.rect = rect + // Area of detected star structure in square pixels. + this.size = size + // Number of local maxima in the detection structure. A value greater than + // one denotes a double/multiple star or a crowded source. A value of zero + // signals that detection of local maxima has been disabled, either globally + // or for this particular structure. + this.nmax = nmax + + this.snr = snr + this.peak = peak } -function StarDetector() -{ - this.__base__ = Object - this.__base__() - - /* - * Number of wavelet layers for structure detection. (default=5) - */ - this.structureLayers = 5 - - /* - * Half size in pixels of a morphological median filter, for hot pixel - * removal. (default=1) - */ - this.hotPixelFilterRadius = 1 - - /* - * Whether the hot pixel filter removal should be applied to the image used - * for star detection, or only to the working image used to build the - * structure map. (default=false) - * - * By setting this parameter to true, the detection algorithm is completely - * robust to hot pixels (of sizes not larger than hotPixelFilterRadius), but - * it is also less sensitive, so less stars will in general be detected. - * With the default value of false, some hot pixels may be wrongly detected - * as stars but the number of true stars detected will generally be larger. - */ - this.applyHotPixelFilterToDetectionImage = false - - /* - * Half size in pixels of a Gaussian convolution filter applied for noise - * reduction. Useful for star detection in low-SNR images. (default=0) - * - * N.B. Setting the value of this parameter > 0 implies - * applyHotPixelFilterToDetectionImage=true. - */ - this.noiseReductionFilterRadius = 0 - - /* - * Sensitivity of the star detection device. - * - * Internally, the sensitivity of the star detection algorithm is expressed - * in signal-to-noise ratio units with respect to the evaluated dispersion - * of local background pixels for each detected structure. Given a source - * with estimated brightness s, local background b and local background - * dispersion n, sensitivity is the minimum value of (s - b)/n necessary to - * trigger star detection. - * - * To isolate the public interface of this class from its internal - * implementation, this parameter is normalized to the [0,1] range, where 0 - * and 1 represent minimum and maximum sensitivity, respectively. This - * abstraction allows us to change the star detection engine without - * breaking dependent tools and processes. - * - * Increase this value to favor detection of fainter stars. Decrease it to - * restrict detection to brighter stars. (default=0.5). - */ - this.sensitivity = 0.5 - - /*! - * Peak sensitivity of the star detection device. - * - * Internally, the peak response property of the star detection algorithm is - * expressed in kurtosis units. For each detected structure, kurtosis is - * evaluated from all significant pixels with values greater than the - * estimated mean local background. Peak response is the minimum value of - * kurtosis necessary to trigger star detection. - * - * To isolate the public interface of this class from its internal - * implementation, this parameter is normalized to the [0,1] range, where 0 - * and 1 represent minimum and maximum peak response, respectively. This - * abstraction allows us to change the star detection engine without - * breaking dependent tools and processes. - * - * If you decrease this parameter, stars will need to have stronger (or more - * prominent) peaks to be detected. This is useful to prevent detection of - * saturated stars, as well as small nonstellar features. By increasing this - * parameter, the star detection algorithm will be more sensitive to - * 'peakedness', and hence more tolerant with relatively flat image - * features. (default=0.5). - */ - this.peakResponse = 0.5 - - /*! - * If this parameter is false, a local maxima map will be generated to - * identify and prevent detection of multiple sources that are too close to - * be separated as individual structures, such as double and multiple stars. - * In general, barycenter positions cannot be accurately determined for - * sources with several local maxima. If this parameter is true, - * non-separable multiple sources will be detectable as single objects. - * (default=false) - */ - this.allowClusteredSources = false - - /* - * Half size in pixels of the local maxima detection filter. (default=2) - */ - this.localDetectionFilterRadius = 2 - - /*! - * This parameter is a normalized pixel value in the [0,1] range. Structures - * with pixels above this value will be excluded for local maxima detection. - * (default=0.75) - */ - this.localMaximaDetectionLimit = 0.75 - - /* - * Set this flag true to avoid detection of local maxima. (default=false) - * Setting this parameter to true implies allowClusteredSources = true. - */ - this.noLocalMaximaDetection = false - - /*! - * Maximum star distortion. - * - * Internally, star distortion is evaluated in units of coverage of a square - * region circumscribed to each detected structure. The coverage of a - * perfectly circular star is pi/4 (about 0.8). Lower values denote - * elongated or irregular sources. - * - * To isolate the public interface of this class from its internal - * implementation, this parameter is normalized to the [0,1] range, where 0 - * and 1 represent minimum and maximum distortion, respectively. This - * abstraction allows us to change the star detection engine without - * breaking dependent tools and processes. - * - * Use this parameter, if necessary, to control inclusion of elongated - * stars, complex clusters of stars, and nonstellar image features. - * (default=0.6) - */ - this.maxDistortion = 0.6 - - /*! - * Stars with measured SNR above this parameter in units of the minimum - * detection level (as defined by the sensitivity parameter) will always be - * detected, even if their profiles are too flat for the current peak - * response. This allows us to force inclusion of bright stars. (default=3) - */ - this.brightThreshold = 3 - - /* - * Minimum signal-to-noise ratio of a detectable star. - * - * Given a source with estimated brightness s, local background b and local - * background dispersion n, SNR is evaluated as (s - b)/n. Stars with - * measured SNR below this parameter won't be detected. (default=0) - * - * The value of this parameter can be increased to limit star detection to a - * subset of the brightest sources in the image adaptively, instead of - * imposing an arbitrary limit on the number of detected stars. - */ - this.minSNR = 0 - - /*! - * Minimum size of a detectable star structure in square pixels. - * - * This parameter can be used to prevent detection of small and bright image - * artifacts as stars. This can be useful to work with uncalibrated or - * wrongly calibrated data, especially demosaiced CFA frames where hot - * pixels have generated large bright artifacts that cannot be removed with - * a median filter, poorly focused images, and images with poor tracking. - * (default=1) - */ - this.minStructureSize = 1 - - /* - * Stars with peak values greater than this value won't be detected. - * (default=1) - */ - this.upperLimit = 1.0 - - /* - * Detect dark structures over a bright background, instead of bright - * structures over a dark background. (default=false) - */ - this.invert = false - - /* - * Optional callback progress function with the following signature: - * - * Boolean progressCallback( int count, int total ) - * - * If defined, this function will be called by the stars() method for each - * row of its target image. The count argument is the current number of - * processed pixel rows, and total is the height of the target image. If the - * function returns false, the star detection task will be aborted. If the - * function returns true, the task will continue. (default=undefined) - */ - this.progressCallback = undefined - - /* - * Optional mask image. If defined, star detection will be restricted to - * nonzero mask pixels. (default=undefined) - */ - this.mask = undefined - - /* - * Stretch factor for the barycenter search algorithm, in sigma units. - * Increase it to make the algorithm more robust to nearby structures, such - * as multiple/crowded stars and small nebular features. However, too large - * of a stretch factor will make the algorithm less accurate. (default=1.5) - */ - this.xyStretch = 1.5 - - /* - * Square structuring element - */ - function BoxStructure( size ) - { - let B = new Array( size*size ) - for ( let i = 0; i < B.length; ++i ) - B[i] = 1 - let S = new Array - S.push( B ) - return S - } - - /* - * Circular structuring element - */ - function CircularStructure( size ) - { - size |= 1 - let C = new Array( size*size ) - let s2 = size >> 1 - let n2 = size/2 - let n22 = n2*n2 - for ( let i = 0; i < s2; ++i ) - { - let di = i+0.5 - n2 - let di2 = di*di - let i2 = i*size - let i1 = i2 + size - 1 - let i3 = (size - i - 1)*size - let i4 = i3 + size - 1 - for ( let j = 0; j < s2; ++j ) - { - let dj = j+0.5 - n2 - C[i1-j] = C[i2+j] = C[i3+j] = C[i4-j] = (di2 + dj*dj <= n22) ? 1 : 0 - } - } - for ( let i = 0; i < size; ++i ) - C[i*size + s2] = C[s2*size + i] = 1 - let S = new Array - S.push( C ) - return S - } - - /* - * Hot pixel removal with a median filter - */ - this.hotPixelFilter = function( image ) - { - if ( this.hotPixelFilterRadius > 0 ) - if ( this.hotPixelFilterRadius > 1 ) - image.morphologicalTransformation( MorphOp_Median, CircularStructure( 2*this.hotPixelFilterRadius + 1 ) ) - else - image.morphologicalTransformation( MorphOp_Median, BoxStructure( 3 ) ) - } - - /* - * Isolate star detection structures in an image. Replaces the specified map - * image with its binary star detection map. - */ - this.getStructureMap = function( map ) - { - // Flatten the image with a high-pass filter - let s = new Image( map ) - let G = Matrix.gaussianFilterBySize( 1 + (1 << this.structureLayers) ) - s.convolveSeparable( G.rowVector( G.rows >> 1 ), G.rowVector( G.rows >> 1 ) ) - map.apply( s, ImageOp_Sub ) - s.free() - map.truncate() - map.rescale() - - // Strength the smallest structures with a dilation filter - map.morphologicalTransformation( MorphOp_Dilation, BoxStructure( 3 ) ) - - // Adaptive binarization - let m = map.median() - if ( 1 + m == 1 ) - { - // Black background - probably a noiseless synthetic star field - let wasRangeClippingEnabled = map.rangeClippingEnabled - let wasRangeClipLow = map.rangeClipLow - let wasRangeClipHigh = map.rangeClipHigh - map.rangeClippingEnabled = true - map.rangeClipLow = 0 - map.rangeClipHigh = 1 - if ( !wasRangeClippingEnabled || wasRangeClipLow != 0 || wasRangeClipHigh != 1 ) - m = map.median() - map.binarize( m + map.MAD( m ) ) - map.rangeClippingEnabled = wasRangeClippingEnabled - map.rangeClipLow = wasRangeClipLow - map.rangeClipHigh = wasRangeClipHigh - } - else - { - // A "natural" image - binarize at 3*noise_stdDev - let n = map.noiseKSigma( 1 )[0] - map.binarize( m + 3*n ) - } - - // Remove noise residuals with an erosion filter - map.morphologicalTransformation( MorphOp_Erosion, BoxStructure( 3 ) ) - - // Optional star detection mask - if ( this.mask != undefined ) - map.apply( this.mask, ImageOp_Mul ) - } - - /* - * Local maxima detection. - */ - this.getLocalMaximaMap = function( map ) - { - // We apply a dilation filter with a flat structuring element without its - // central element. Local maxima are those pixels in the input image with - // values greater than the dilated image. - // The localMaximaDetectionLimit parameter allows us to prevent detection of - // false multiple maxima on saturated or close to saturated structures. - let Bh = BoxStructure( (this.localDetectionFilterRadius << 1)|1 ) - Bh[0][Bh[0].length >> 1] = 0 - let l = new Image( map ) - l.binarize( this.localMaximaDetectionLimit ) - l.invert() - let t = new Image( map ) - t.morphologicalTransformation( MorphOp_Dilation, Bh ) - map.apply( t, ImageOp_Sub ) - t.free() - map.binarize( 0 ) - map.apply( l, ImageOp_Mul ) - l.free() - } - - /* - * Compute star parameters - */ - this.starParameters = function( image, rect, starPoints, lmMap ) - { - let params = { pos: { x:0, y:0 }, // barycenter image coordinates - rect: { x0:0, y0:0, x1:0, y1:0 }, // detection rectangle - bkg: 0, // local background - sigma: 0, // local background dispersion - flux: 0, // total flux - max: 0, // maximum pixel value - nmax: 0, // number of local maxima in structure - peak: 0, // robust peak value - kurt: 0, // kurtosis - count: 0, // sample length - size: 0 } // structure size in square pixels - - // Mean local background and local background dispersion - for ( let delta = 4, it = 0, m0 = 1;; ++delta, ++it ) - { - let r = rect.inflatedBy( delta ) - let a = [], b = [], c = [], d = [] - image.getSamples( a, new Rect( r.x0, r.y0, r.x1, rect.y0 ) ) - image.getSamples( b, new Rect( r.x0, rect.y0, rect.x0, rect.y1 ) ) - image.getSamples( c, new Rect( r.x0, rect.y1, r.x1, r.y1 ) ) - image.getSamples( d, new Rect( rect.x1, rect.y0, r.x1, rect.y1 ) ) - let B = a.concat( b, c, d ) - let m = Math.median( B ) - if ( m > m0 || (m0 - m)/m0 < 0.01 ) - { - params.bkg = m - params.sigma = Math.max( 1.4826*Math.MAD( B ), Math.EPSILON32 ) - break - } - // Guard us against rare ill-posed conditions - if ( it == 200 ) - return null - m0 = m - } - - // Detection region - params.rect = rect.inflatedBy( 2 ).intersection( image.bounds ) - - // Structure size - params.size = starPoints.length - - // Significant subset - let v = [] - for ( let i = 0; i < starPoints.length; ++i ) - { - let p = starPoints[i] - let f = image.sample( p.x, p.y ) - if ( f > params.bkg ) - { - // Local maxima - if ( !this.noLocalMaximaDetection ) - if ( lmMap.sample( p.x, p.y ) != 0 ) - ++params.nmax - v.push( f ) - // Total flux above local background - params.flux += f - } - } - - // Fail if no significant data - if ( v.length == 0 ) - return null - - // Fail if we have multiple maxima and those are not allowed - if ( params.nmax > 1 ) - if ( !this.allowClusteredSources ) +function StarDetector() { + this.__base__ = Object + this.__base__() + + /* + * Number of wavelet layers for structure detection. (default=5) + */ + this.structureLayers = 5 + + /* + * Half size in pixels of a morphological median filter, for hot pixel + * removal. (default=1) + */ + this.hotPixelFilterRadius = 1 + + /* + * Whether the hot pixel filter removal should be applied to the image used + * for star detection, or only to the working image used to build the + * structure map. (default=false) + * + * By setting this parameter to true, the detection algorithm is completely + * robust to hot pixels (of sizes not larger than hotPixelFilterRadius), but + * it is also less sensitive, so less stars will in general be detected. + * With the default value of false, some hot pixels may be wrongly detected + * as stars but the number of true stars detected will generally be larger. + */ + this.applyHotPixelFilterToDetectionImage = false + + /* + * Half size in pixels of a Gaussian convolution filter applied for noise + * reduction. Useful for star detection in low-SNR images. (default=0) + * + * N.B. Setting the value of this parameter > 0 implies + * applyHotPixelFilterToDetectionImage=true. + */ + this.noiseReductionFilterRadius = 0 + + /* + * Sensitivity of the star detection device. + * + * Internally, the sensitivity of the star detection algorithm is expressed + * in signal-to-noise ratio units with respect to the evaluated dispersion + * of local background pixels for each detected structure. Given a source + * with estimated brightness s, local background b and local background + * dispersion n, sensitivity is the minimum value of (s - b)/n necessary to + * trigger star detection. + * + * To isolate the public interface of this class from its internal + * implementation, this parameter is normalized to the [0,1] range, where 0 + * and 1 represent minimum and maximum sensitivity, respectively. This + * abstraction allows us to change the star detection engine without + * breaking dependent tools and processes. + * + * Increase this value to favor detection of fainter stars. Decrease it to + * restrict detection to brighter stars. (default=0.5). + */ + this.sensitivity = 0.5 + + /*! + * Peak sensitivity of the star detection device. + * + * Internally, the peak response property of the star detection algorithm is + * expressed in kurtosis units. For each detected structure, kurtosis is + * evaluated from all significant pixels with values greater than the + * estimated mean local background. Peak response is the minimum value of + * kurtosis necessary to trigger star detection. + * + * To isolate the public interface of this class from its internal + * implementation, this parameter is normalized to the [0,1] range, where 0 + * and 1 represent minimum and maximum peak response, respectively. This + * abstraction allows us to change the star detection engine without + * breaking dependent tools and processes. + * + * If you decrease this parameter, stars will need to have stronger (or more + * prominent) peaks to be detected. This is useful to prevent detection of + * saturated stars, as well as small nonstellar features. By increasing this + * parameter, the star detection algorithm will be more sensitive to + * 'peakedness', and hence more tolerant with relatively flat image + * features. (default=0.5). + */ + this.peakResponse = 0.5 + + /*! + * If this parameter is false, a local maxima map will be generated to + * identify and prevent detection of multiple sources that are too close to + * be separated as individual structures, such as double and multiple stars. + * In general, barycenter positions cannot be accurately determined for + * sources with several local maxima. If this parameter is true, + * non-separable multiple sources will be detectable as single objects. + * (default=false) + */ + this.allowClusteredSources = false + + /* + * Half size in pixels of the local maxima detection filter. (default=2) + */ + this.localDetectionFilterRadius = 2 + + /*! + * This parameter is a normalized pixel value in the [0,1] range. Structures + * with pixels above this value will be excluded for local maxima detection. + * (default=0.75) + */ + this.localMaximaDetectionLimit = 0.75 + + /* + * Set this flag true to avoid detection of local maxima. (default=false) + * Setting this parameter to true implies allowClusteredSources = true. + */ + this.noLocalMaximaDetection = false + + /*! + * Maximum star distortion. + * + * Internally, star distortion is evaluated in units of coverage of a square + * region circumscribed to each detected structure. The coverage of a + * perfectly circular star is pi/4 (about 0.8). Lower values denote + * elongated or irregular sources. + * + * To isolate the public interface of this class from its internal + * implementation, this parameter is normalized to the [0,1] range, where 0 + * and 1 represent minimum and maximum distortion, respectively. This + * abstraction allows us to change the star detection engine without + * breaking dependent tools and processes. + * + * Use this parameter, if necessary, to control inclusion of elongated + * stars, complex clusters of stars, and nonstellar image features. + * (default=0.6) + */ + this.maxDistortion = 0.6 + + /*! + * Stars with measured SNR above this parameter in units of the minimum + * detection level (as defined by the sensitivity parameter) will always be + * detected, even if their profiles are too flat for the current peak + * response. This allows us to force inclusion of bright stars. (default=3) + */ + this.brightThreshold = 3 + + /* + * Minimum signal-to-noise ratio of a detectable star. + * + * Given a source with estimated brightness s, local background b and local + * background dispersion n, SNR is evaluated as (s - b)/n. Stars with + * measured SNR below this parameter won't be detected. (default=0) + * + * The value of this parameter can be increased to limit star detection to a + * subset of the brightest sources in the image adaptively, instead of + * imposing an arbitrary limit on the number of detected stars. + */ + this.minSNR = 0 + + /*! + * Minimum size of a detectable star structure in square pixels. + * + * This parameter can be used to prevent detection of small and bright image + * artifacts as stars. This can be useful to work with uncalibrated or + * wrongly calibrated data, especially demosaiced CFA frames where hot + * pixels have generated large bright artifacts that cannot be removed with + * a median filter, poorly focused images, and images with poor tracking. + * (default=1) + */ + this.minStructureSize = 1 + + /* + * Stars with peak values greater than this value won't be detected. + * (default=1) + */ + this.upperLimit = 1.0 + + /* + * Detect dark structures over a bright background, instead of bright + * structures over a dark background. (default=false) + */ + this.invert = false + + /* + * Optional callback progress function with the following signature: + * + * Boolean progressCallback( int count, int total ) + * + * If defined, this function will be called by the stars() method for each + * row of its target image. The count argument is the current number of + * processed pixel rows, and total is the height of the target image. If the + * function returns false, the star detection task will be aborted. If the + * function returns true, the task will continue. (default=undefined) + */ + this.progressCallback = undefined + + /* + * Optional mask image. If defined, star detection will be restricted to + * nonzero mask pixels. (default=undefined) + */ + this.mask = undefined + + /* + * Stretch factor for the barycenter search algorithm, in sigma units. + * Increase it to make the algorithm more robust to nearby structures, such + * as multiple/crowded stars and small nebular features. However, too large + * of a stretch factor will make the algorithm less accurate. (default=1.5) + */ + this.xyStretch = 1.5 + + /* + * Square structuring element + */ + function BoxStructure(size) { + let B = new Array(size * size) + for (let i = 0; i < B.length; ++i) + B[i] = 1 + let S = new Array + S.push(B) + return S + } + + /* + * Circular structuring element + */ + function CircularStructure(size) { + size |= 1 + let C = new Array(size * size) + let s2 = size >> 1 + let n2 = size / 2 + let n22 = n2 * n2 + for (let i = 0; i < s2; ++i) { + let di = i + 0.5 - n2 + let di2 = di * di + let i2 = i * size + let i1 = i2 + size - 1 + let i3 = (size - i - 1) * size + let i4 = i3 + size - 1 + for (let j = 0; j < s2; ++j) { + let dj = j + 0.5 - n2 + C[i1 - j] = C[i2 + j] = C[i3 + j] = C[i4 - j] = (di2 + dj * dj <= n22) ? 1 : 0 + } + } + for (let i = 0; i < size; ++i) + C[i * size + s2] = C[s2 * size + i] = 1 + let S = new Array + S.push(C) + return S + } + + /* + * Hot pixel removal with a median filter + */ + this.hotPixelFilter = function (image) { + if (this.hotPixelFilterRadius > 0) + if (this.hotPixelFilterRadius > 1) + image.morphologicalTransformation(MorphOp_Median, CircularStructure(2 * this.hotPixelFilterRadius + 1)) + else + image.morphologicalTransformation(MorphOp_Median, BoxStructure(3)) + } + + /* + * Isolate star detection structures in an image. Replaces the specified map + * image with its binary star detection map. + */ + this.getStructureMap = function (map) { + // Flatten the image with a high-pass filter + let s = new Image(map) + let G = Matrix.gaussianFilterBySize(1 + (1 << this.structureLayers)) + s.convolveSeparable(G.rowVector(G.rows >> 1), G.rowVector(G.rows >> 1)) + map.apply(s, ImageOp_Sub) + s.free() + map.truncate() + map.rescale() + + // Strength the smallest structures with a dilation filter + map.morphologicalTransformation(MorphOp_Dilation, BoxStructure(3)) + + // Adaptive binarization + let m = map.median() + if (1 + m == 1) { + // Black background - probably a noiseless synthetic star field + let wasRangeClippingEnabled = map.rangeClippingEnabled + let wasRangeClipLow = map.rangeClipLow + let wasRangeClipHigh = map.rangeClipHigh + map.rangeClippingEnabled = true + map.rangeClipLow = 0 + map.rangeClipHigh = 1 + if (!wasRangeClippingEnabled || wasRangeClipLow != 0 || wasRangeClipHigh != 1) + m = map.median() + map.binarize(m + map.MAD(m)) + map.rangeClippingEnabled = wasRangeClippingEnabled + map.rangeClipLow = wasRangeClipLow + map.rangeClipHigh = wasRangeClipHigh + } + else { + // A "natural" image - binarize at 3*noise_stdDev + let n = map.noiseKSigma(1)[0] + map.binarize(m + 3 * n) + } + + // Remove noise residuals with an erosion filter + map.morphologicalTransformation(MorphOp_Erosion, BoxStructure(3)) + + // Optional star detection mask + if (this.mask != undefined) + map.apply(this.mask, ImageOp_Mul) + } + + /* + * Local maxima detection. + */ + this.getLocalMaximaMap = function (map) { + // We apply a dilation filter with a flat structuring element without its + // central element. Local maxima are those pixels in the input image with + // values greater than the dilated image. + // The localMaximaDetectionLimit parameter allows us to prevent detection of + // false multiple maxima on saturated or close to saturated structures. + let Bh = BoxStructure((this.localDetectionFilterRadius << 1) | 1) + Bh[0][Bh[0].length >> 1] = 0 + let l = new Image(map) + l.binarize(this.localMaximaDetectionLimit) + l.invert() + let t = new Image(map) + t.morphologicalTransformation(MorphOp_Dilation, Bh) + map.apply(t, ImageOp_Sub) + t.free() + map.binarize(0) + map.apply(l, ImageOp_Mul) + l.free() + } + + /* + * Compute star parameters + */ + this.starParameters = function (image, rect, starPoints, lmMap) { + let params = { + pos: { x: 0, y: 0 }, // barycenter image coordinates + rect: { x0: 0, y0: 0, x1: 0, y1: 0 }, // detection rectangle + bkg: 0, // local background + sigma: 0, // local background dispersion + flux: 0, // total flux + max: 0, // maximum pixel value + nmax: 0, // number of local maxima in structure + peak: 0, // robust peak value + kurt: 0, // kurtosis + count: 0, // sample length + size: 0 + } // structure size in square pixels + + // Mean local background and local background dispersion + for (let delta = 4, it = 0, m0 = 1; ; ++delta, ++it) { + let r = rect.inflatedBy(delta) + let a = [], b = [], c = [], d = [] + image.getSamples(a, new Rect(r.x0, r.y0, r.x1, rect.y0)) + image.getSamples(b, new Rect(r.x0, rect.y0, rect.x0, rect.y1)) + image.getSamples(c, new Rect(r.x0, rect.y1, r.x1, r.y1)) + image.getSamples(d, new Rect(rect.x1, rect.y0, r.x1, rect.y1)) + let B = a.concat(b, c, d) + let m = Math.median(B) + if (m > m0 || (m0 - m) / m0 < 0.01) { + params.bkg = m + params.sigma = Math.max(1.4826 * Math.MAD(B), Math.EPSILON32) + break + } + // Guard us against rare ill-posed conditions + if (it == 200) + return null + m0 = m + } + + // Detection region + params.rect = rect.inflatedBy(2).intersection(image.bounds) + + // Structure size + params.size = starPoints.length + + // Significant subset + let v = [] + for (let i = 0; i < starPoints.length; ++i) { + let p = starPoints[i] + let f = image.sample(p.x, p.y) + if (f > params.bkg) { + // Local maxima + if (!this.noLocalMaximaDetection) + if (lmMap.sample(p.x, p.y) != 0) + ++params.nmax + v.push(f) + // Total flux above local background + params.flux += f + } + } + + // Fail if no significant data + if (v.length == 0) return null - // Barycenter coordinates - let M = Matrix.fromImage( image, rect ) - M.truncate( Math.range( M.median() + this.xyStretch*M.stdDev(), 0.0, 1.0 ), 1.0 ) - M.rescale() - let sx = 0, sy = 0, sz = 0 - for ( let y = rect.y0, i = 0; i < M.rows; ++y, ++i ) - for ( let x = rect.x0, j = 0; j < M.cols; ++x, ++j ) - { - let z = M.at( i, j ) - if ( z > 0 ) - { - sx += z*x - sy += z*y - sz += z + // Fail if we have multiple maxima and those are not allowed + if (params.nmax > 1) + if (!this.allowClusteredSources) + return null + + // Barycenter coordinates + let M = Matrix.fromImage(image, rect) + M.truncate(Math.range(M.median() + this.xyStretch * M.stdDev(), 0.0, 1.0), 1.0) + M.rescale() + let sx = 0, sy = 0, sz = 0 + for (let y = rect.y0, i = 0; i < M.rows; ++y, ++i) + for (let x = rect.x0, j = 0; j < M.cols; ++x, ++j) { + let z = M.at(i, j) + if (z > 0) { + sx += z * x + sy += z * y + sz += z + } } - } - params.pos.x = sx/sz + 0.5 - params.pos.y = sy/sz + 0.5 - - // Sort significant pixels in decreasing flux order - v.sort( (a, b) => (a < b) ? +1 : ((b < a) ? -1 : 0) ) - // Maximum pixel value - params.max = v[0] - - // Find subset of significant high pixel values - let mn = 0 - for ( let i = 0; i < v.length && (mn < 5 || v[i] == v[i-1]); ++i, ++mn ) {} - for ( let i = 0; i < mn; ++i ) - params.peak += v[i] - // Significant peak value - params.peak /= mn - // Significant sample length - params.count = v.length - - // Kurtosis - let s = Math.stdDev( v ) - if ( 1 + s != 1 ) - { - let m = params.flux/v.length - let k = 0 - for ( let i = 0; i < v.length; ++i ) - { - let d = (v[i] - m)/s - d *= d - k += d*d - } - params.kurt = k/params.count - } - - return params - } - - /* - * Finds all the stars in an image. Returns an array of Star objects. - */ - this.stars = function( image ) - { - // We work on a duplicate of the source grayscale image, or on its HSI - // intensity component if it is a color image. - let wrk = Image.newFloatImage() - image.getIntensity( wrk ) - - // Hot pixel removal, if applied to the image where we are going to find - // stars, not just to the image used to build the structure map. - // When noise reduction is enabled, always remove hot pixels first, or - // hot pixels would be promoted to "stars". - let alreadyFixedHotPixels = false - if ( this.applyHotPixelFilterToDetectionImage || this.noiseReductionFilterRadius > 0 ) - { - this.hotPixelFilter( wrk ) - alreadyFixedHotPixels = true - } - - // If the invert flag is set, then we are looking for dark structures on - // a bright background. - if ( this.invert ) - wrk.invert() - - // Optional noise reduction - if ( this.noiseReductionFilterRadius > 0 ) - { - let G = Matrix.gaussianFilterBySize( (this.noiseReductionFilterRadius << 1)|1 ) - wrk.convolveSeparable( G.rowVector( G.rows >> 1 ), G.rowVector( G.rows >> 1 ) ) - } - - // Structure map - let map = Image.newFloatImage() - map.assign( wrk ) - // Hot pixel removal, if applied just to the image used to build the - // structure map. - if ( !alreadyFixedHotPixels ) - this.hotPixelFilter( map ) - this.getStructureMap( map ) - - // Use matrices instead of images for faster access - let M = map.toMatrix() - map.free() - - // Local maxima map - let lmMap - if ( !this.noLocalMaximaDetection ) - { - lmMap = Image.newFloatImage() - lmMap.assign( wrk ) - this.getLocalMaximaMap( lmMap ) - } - - /* - * Internal detection parameters - */ - // Signal detection threshold in local sigma units. - let snrThreshold = 0.1 + 4.8*(1 - Math.range( this.sensitivity, 0, 1 )) - // Peak detection threshold in kurtosis units. - let peakThreshold = 0.1 + 9.8*(1 - Math.range( this.peakResponse, 0, 1 )) - // Maximum distortion in coverage units. - let minCoverage = Math.PI4*(1 - Math.range( this.maxDistortion, 0, 1 )) - - // The detected stars - let S = new Array - - // Structure scanner - for ( let y0 = 0, x1 = M.cols-1, y1 = M.rows-1; y0 < y1; ++y0 ) - { - if ( this.progressCallback != undefined ) - if ( !this.progressCallback( y0, M.rows ) ) - return null - - for ( let x0 = 0; x0 < x1; ++x0 ) - { - // Exclude background pixels and already visited pixels - if ( M.at( y0, x0 ) == 0 ) - continue - - // Star pixel coordinates - let starPoints = new Array - - // Star bounding rectangle - let r = new Rect( x0, y0, x0+1, y0+1 ) - - // Grow star region downward - for ( let y = y0, x = x0, xa, xb; ; ) - { - // Add this pixel to the current star - starPoints.push( { x:x, y:y } ) - - // Explore the left segment of this row - for ( xa = x; xa > 0; ) - { - if ( M.at( y, xa-1 ) == 0 ) - break - --xa - starPoints.push( { x:xa, y:y } ) - } - - // Explore the right segment of this row - for ( xb = x; xb < x1; ) - { - if ( M.at( y, xb+1 ) == 0 ) - break - ++xb - starPoints.push( { x:xb, y:y } ) - } - - // xa and xb are now the left and right boundary limits, - // respectively, of this row in the current star. - - if ( xa < r.x0 ) // update left boundary - r.x0 = xa - - if ( xb >= r.x1 ) // update right boundary - r.x1 = xb + 1 // bottom-right corner excluded (PCL-specific) - - // Prepare for next row - ++y - - // Decide whether we are done with this star now, or if - // there is at least one more row that must be explored. - - let nextRow = false - - // Explore the next row from left to right. We'll continue - // gathering pixels if we find at least one nonzero map pixel. - for ( x = xa; x <= xb; ++x ) - if ( M.at( y, x ) != 0 ) - { - nextRow = true - break - } - - if ( !nextRow ) - break - - // Update bottom boundary - r.y1 = y + 1 // Rect *excludes* the bottom-right corner - - // Terminate if we reach the last row of the image - if ( y == y1 ) - break + params.pos.x = sx / sz + 0.5 + params.pos.y = sy / sz + 0.5 + + // Sort significant pixels in decreasing flux order + v.sort((a, b) => (a < b) ? +1 : ((b < a) ? -1 : 0)) + // Maximum pixel value + params.max = v[0] + + // Find subset of significant high pixel values + let mn = 0 + for (let i = 0; i < v.length && (mn < 5 || v[i] == v[i - 1]); ++i, ++mn) { } + for (let i = 0; i < mn; ++i) + params.peak += v[i] + // Significant peak value + params.peak /= mn + // Significant sample length + params.count = v.length + + // Kurtosis + let s = Math.stdDev(v) + if (1 + s != 1) { + let m = params.flux / v.length + let k = 0 + for (let i = 0; i < v.length; ++i) { + let d = (v[i] - m) / s + d *= d + k += d * d } + params.kurt = k / params.count + } + + return params + } + + /* + * Finds all the stars in an image. Returns an array of Star objects. + */ + this.stars = function (image) { + // We work on a duplicate of the source grayscale image, or on its HSI + // intensity component if it is a color image. + let wrk = Image.newFloatImage() + image.getIntensity(wrk) + + // Hot pixel removal, if applied to the image where we are going to find + // stars, not just to the image used to build the structure map. + // When noise reduction is enabled, always remove hot pixels first, or + // hot pixels would be promoted to "stars". + let alreadyFixedHotPixels = false + if (this.applyHotPixelFilterToDetectionImage || this.noiseReductionFilterRadius > 0) { + this.hotPixelFilter(wrk) + alreadyFixedHotPixels = true + } + + // If the invert flag is set, then we are looking for dark structures on + // a bright background. + if (this.invert) + wrk.invert() - /* - * If this is a reliable star, compute its barycenter coordinates - * and add it to the star list. - * - * Rejection criteria: - * - * * Stars whose peak values are greater than the upperLimit - * parameter are rejected. - * - * * If this structure is touching a border of the image, reject - * it. We cannot compute an accurate position for a clipped star. - * - * * Too small structures are rejected. This mainly prevents - * inclusion of hot (or cold) pixels. This condition is enforced - * by the hot pixel removal and noise reduction steps performed - * during the structure detection phase, and optionally by - * increasing the minStructureSize parameter. - * - * * Too large structures are rejected. This prevents inclusion of - * extended nonstellar objects and saturated bright stars. This - * is also part of the structure detection algorithm. - * - * * Too elongated stars are rejected. The minCoverage parameter - * determines the maximum distortion allowed. A perfect square - * has coverage = 1. The coverage of a perfect circle is pi/4. - * - * * Too sparse sources are rejected. This prevents detection of - * multiple stars where centroids cannot be well determined. - * - * * Too dim structures are rejected. The sensitivity parameter - * defines the sensitivity of the star detection algorithm in - * local sigma units. The minSNR parameter can be used to limit - * star detection to a subset of the brightest stars adaptively. - * - * * Too flat structures are rejected. The peakThreshold parameter - * defines the peak sensitivity of the star detection algorithm - * in kurtosis units. - */ - if ( r.width > 1 && r.height > 1 ) - if ( r.y0 > 0 && r.y1 <= y1 && r.x0 > 0 && r.x1 <= x1 ) - if ( starPoints.length >= this.minStructureSize ) - { - let p = this.starParameters( wrk, r, starPoints, lmMap ) - if ( p != null ) - if ( p.max <= this.upperLimit ) - { - let d = Math.max( r.width, r.height ) - if ( p.count/d/d >= minCoverage ) - { - let ix = Math.trunc( p.pos.x )|0 - let iy = Math.trunc( p.pos.y )|0 - if ( this.mask == undefined || this.mask.sample( ix, iy ) != 0 ) - { - let snr = (p.peak - p.bkg)/p.sigma - if ( snr >= this.minSNR ) - { - let s1 = snr/snrThreshold - if ( s1 >= 1 ) - if ( s1 >= this.brightThreshold || p.kurt == 0 || p.kurt/peakThreshold >= 1 ) - S.push( new Star( p.pos, p.flux, p.bkg, p.rect, p.size, p.nmax, snr, p.peak ) ) - } - } - } + // Optional noise reduction + if (this.noiseReductionFilterRadius > 0) { + let G = Matrix.gaussianFilterBySize((this.noiseReductionFilterRadius << 1) | 1) + wrk.convolveSeparable(G.rowVector(G.rows >> 1), G.rowVector(G.rows >> 1)) + } + + // Structure map + let map = Image.newFloatImage() + map.assign(wrk) + // Hot pixel removal, if applied just to the image used to build the + // structure map. + if (!alreadyFixedHotPixels) + this.hotPixelFilter(map) + this.getStructureMap(map) + + // Use matrices instead of images for faster access + let M = map.toMatrix() + map.free() + + // Local maxima map + let lmMap + if (!this.noLocalMaximaDetection) { + lmMap = Image.newFloatImage() + lmMap.assign(wrk) + this.getLocalMaximaMap(lmMap) + } + + /* + * Internal detection parameters + */ + // Signal detection threshold in local sigma units. + let snrThreshold = 0.1 + 4.8 * (1 - Math.range(this.sensitivity, 0, 1)) + // Peak detection threshold in kurtosis units. + let peakThreshold = 0.1 + 9.8 * (1 - Math.range(this.peakResponse, 0, 1)) + // Maximum distortion in coverage units. + let minCoverage = Math.PI4 * (1 - Math.range(this.maxDistortion, 0, 1)) + + // The detected stars + let S = new Array + + // Structure scanner + for (let y0 = 0, x1 = M.cols - 1, y1 = M.rows - 1; y0 < y1; ++y0) { + if (this.progressCallback != undefined) + if (!this.progressCallback(y0, M.rows)) + return null + + for (let x0 = 0; x0 < x1; ++x0) { + // Exclude background pixels and already visited pixels + if (M.at(y0, x0) == 0) + continue + + // Star pixel coordinates + let starPoints = new Array + + // Star bounding rectangle + let r = new Rect(x0, y0, x0 + 1, y0 + 1) + + // Grow star region downward + for (let y = y0, x = x0, xa, xb; ;) { + // Add this pixel to the current star + starPoints.push({ x: x, y: y }) + + // Explore the left segment of this row + for (xa = x; xa > 0;) { + if (M.at(y, xa - 1) == 0) + break + --xa + starPoints.push({ x: xa, y: y }) + } + + // Explore the right segment of this row + for (xb = x; xb < x1;) { + if (M.at(y, xb + 1) == 0) + break + ++xb + starPoints.push({ x: xb, y: y }) + } + + // xa and xb are now the left and right boundary limits, + // respectively, of this row in the current star. + + if (xa < r.x0) // update left boundary + r.x0 = xa + + if (xb >= r.x1) // update right boundary + r.x1 = xb + 1 // bottom-right corner excluded (PCL-specific) + + // Prepare for next row + ++y + + // Decide whether we are done with this star now, or if + // there is at least one more row that must be explored. + + let nextRow = false + + // Explore the next row from left to right. We'll continue + // gathering pixels if we find at least one nonzero map pixel. + for (x = xa; x <= xb; ++x) + if (M.at(y, x) != 0) { + nextRow = true + break } - } - // Erase this structure. - for ( let i = 0; i < starPoints.length; ++i ) - { - let p = starPoints[i] - M.at( p.y, p.x, 0 ) + if (!nextRow) + break + + // Update bottom boundary + r.y1 = y + 1 // Rect *excludes* the bottom-right corner + + // Terminate if we reach the last row of the image + if (y == y1) + break + } + + /* + * If this is a reliable star, compute its barycenter coordinates + * and add it to the star list. + * + * Rejection criteria: + * + * * Stars whose peak values are greater than the upperLimit + * parameter are rejected. + * + * * If this structure is touching a border of the image, reject + * it. We cannot compute an accurate position for a clipped star. + * + * * Too small structures are rejected. This mainly prevents + * inclusion of hot (or cold) pixels. This condition is enforced + * by the hot pixel removal and noise reduction steps performed + * during the structure detection phase, and optionally by + * increasing the minStructureSize parameter. + * + * * Too large structures are rejected. This prevents inclusion of + * extended nonstellar objects and saturated bright stars. This + * is also part of the structure detection algorithm. + * + * * Too elongated stars are rejected. The minCoverage parameter + * determines the maximum distortion allowed. A perfect square + * has coverage = 1. The coverage of a perfect circle is pi/4. + * + * * Too sparse sources are rejected. This prevents detection of + * multiple stars where centroids cannot be well determined. + * + * * Too dim structures are rejected. The sensitivity parameter + * defines the sensitivity of the star detection algorithm in + * local sigma units. The minSNR parameter can be used to limit + * star detection to a subset of the brightest stars adaptively. + * + * * Too flat structures are rejected. The peakThreshold parameter + * defines the peak sensitivity of the star detection algorithm + * in kurtosis units. + */ + if (r.width > 1 && r.height > 1) + if (r.y0 > 0 && r.y1 <= y1 && r.x0 > 0 && r.x1 <= x1) + if (starPoints.length >= this.minStructureSize) { + let p = this.starParameters(wrk, r, starPoints, lmMap) + if (p != null) + if (p.max <= this.upperLimit) { + let d = Math.max(r.width, r.height) + if (p.count / d / d >= minCoverage) { + let ix = Math.trunc(p.pos.x) | 0 + let iy = Math.trunc(p.pos.y) | 0 + if (this.mask == undefined || this.mask.sample(ix, iy) != 0) { + let snr = (p.peak - p.bkg) / p.sigma + if (snr >= this.minSNR) { + let s1 = snr / snrThreshold + if (s1 >= 1) + if (s1 >= this.brightThreshold || p.kurt == 0 || p.kurt / peakThreshold >= 1) + S.push(new Star(p.pos, p.flux, p.bkg, p.rect, p.size, p.nmax, snr, p.peak)) + } + } + } + } + } + + // Erase this structure. + for (let i = 0; i < starPoints.length; ++i) { + let p = starPoints[i] + M.at(p.y, p.x, 0) + } } - } - } + } - // Sort the list of detected sources in descending brightness order. - S.sort( (a, b) => (a.flux < b.flux) ? +1 : ((b.flux < a.flux) ? -1 : 0) ) + // Sort the list of detected sources in descending brightness order. + S.sort((a, b) => (a.flux < b.flux) ? +1 : ((b.flux < a.flux) ? -1 : 0)) - // Perform a soft garbage collection. This eases integration with very - // long batch tasks and has no measurable performance penalty. - gc( false/*hard*/ ) + // Perform a soft garbage collection. This eases integration with very + // long batch tasks and has no measurable performance penalty. + gc(false/*hard*/) - if ( this.progressCallback != undefined ) - if ( !this.progressCallback( M.rows, M.rows ) ) - return null + if (this.progressCallback != undefined) + if (!this.progressCallback(M.rows, M.rows)) + return null - return S - } + return S + } } StarDetector.prototype = new Object @@ -782,12 +748,12 @@ function computeHfr(image, s) { const r = Math.min(s.rect.y1 - s.rect.y0, s.rect.x1 - s.rect.x0) / 2 - for(let y = s.rect.y0; y <= s.rect.y1; y++) { - for(let x = s.rect.x0; x <= s.rect.x1; x++) { - if(x >= 0 && x < image.width && y >= 0 && y < image.height) { + for (let y = s.rect.y0; y <= s.rect.y1; y++) { + for (let x = s.rect.x0; x <= s.rect.x1; x++) { + if (x >= 0 && x < image.width && y >= 0 && y < image.height) { const d = Math.sqrt((x - s.pos.x) * (x - s.pos.x) + (y - s.pos.y) * (y - s.pos.y)) - if(d <= r) { + if (d <= r) { const p = image.sample(x, y) const v = p - s.bkg a += v * d @@ -801,69 +767,75 @@ function computeHfr(image, s) { } function decodeParams(hex) { - let decoded = '' + const buffer = new Uint8Array(hex.length / 4) - for (let i = 0; i < hex.length; i += 2) { - decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + for (let i = 0; i < hex.length; i += 4) { + buffer[i / 4] = parseInt(hex.substr(i, 4), 16) } - return JSON.parse(decoded) + return JSON.parse(String.fromCharCode.apply(null, buffer)) } function detectStars() { - const input = decodeParams(jsArguments[0]) - - const targetPath = input.targetPath - const statusPath = input.statusPath - const minSNR = input.minSNR - const invert = input.invert - - console.writeln("targetPath=" + targetPath) - console.writeln("statusPath=" + statusPath) - console.writeln("minSNR=" + minSNR) - console.writeln("invert=" + invert) - - const P = new StarDetector - P.structureLayers = 5 - P.hotPixelFilterRadius = 1 - P.applyHotPixelFilterToDetectionImage = false - P.noiseReductionFilterRadius = 0 - P.sensitivity = 0.5 - P.peakResponse = 0.5 - P.allowClusteredSources = false - P.localDetectionFilterRadius = 2 - P.localMaximaDetectionLimit = 0.75 - P.noLocalMaximaDetection = false - P.maxDistortion = 0.6 - P.brightThreshold = 3 - P.minSNR = minSNR - P.minStructureSize = 1 - P.upperLimit = 1.0 - P.invert = invert - P.xyStretch = 1.5 - - const window = ImageWindow.open(targetPath) - const image = window[0].mainView.image - - const sl = P.stars(image) - - console.writeln("star detection finished. stars=", sl.length) - - const stars = [] - - for(let i = 0; i < sl.length; i++) { - const s = sl[i] - computeHfr(image, s) - stars.push({ x: s.pos.x, y: s.pos.y, flux: s.flux * 65536, size: s.size, nmax: s.nmax, bkg: s.bkg, x0: s.rect.x0, y0: s.rect.y0, x1: s.rect.x1, y1: s.rect.y1, snr: s.snr, peak: s.peak, hfd: 2 * s.hfr }) + const data = { + success: true, + errorMessage: null, + stars: [], } - window[0].forceClose() + try { + const input = decodeParams(jsArguments[0]) + + const targetPath = input.targetPath + const statusPath = input.statusPath + const minSNR = input.minSNR + const invert = input.invert + + console.writeln("targetPath=" + targetPath) + console.writeln("statusPath=" + statusPath) + console.writeln("minSNR=" + minSNR) + console.writeln("invert=" + invert) + + const P = new StarDetector + P.structureLayers = 5 + P.hotPixelFilterRadius = 1 + P.applyHotPixelFilterToDetectionImage = false + P.noiseReductionFilterRadius = 0 + P.sensitivity = 0.5 + P.peakResponse = 0.5 + P.allowClusteredSources = false + P.localDetectionFilterRadius = 2 + P.localMaximaDetectionLimit = 0.75 + P.noLocalMaximaDetection = false + P.maxDistortion = 0.6 + P.brightThreshold = 3 + P.minSNR = minSNR + P.minStructureSize = 1 + P.upperLimit = 1.0 + P.invert = invert + P.xyStretch = 1.5 + + const window = ImageWindow.open(targetPath) + const image = window[0].mainView.image + + const sl = P.stars(image) + + for (let i = 0; i < sl.length; i++) { + const s = sl[i] + computeHfr(image, s) + data.stars.push({ x: s.pos.x, y: s.pos.y, flux: s.flux * 65536, size: s.size, nmax: s.nmax, bkg: s.bkg, x0: s.rect.x0, y0: s.rect.y0, x1: s.rect.x1, y1: s.rect.y1, snr: s.snr, peak: s.peak, hfd: 2 * s.hfr }) + } - const json = { - stars: stars - } + window[0].forceClose() - File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") + console.writeln("star detection finished. stars=", sl.length) + } catch (e) { + data.success = false + data.errorMessage = e.message + console.writeln(data.errorMessage) + } finally { + File.writeTextFile(statusPath, "@" + JSON.stringify(data) + "#") + } } detectStars() diff --git a/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js index 54cfc0a07..7c335ff77 100644 --- a/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js +++ b/nebulosa-pixinsight/src/main/resources/pixinsight/PixelMath.js @@ -1,92 +1,102 @@ function decodeParams(hex) { - let decoded = '' + const buffer = new Uint8Array(hex.length / 4) - for (let i = 0; i < hex.length; i += 2) { - decoded += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) + for (let i = 0; i < hex.length; i += 4) { + buffer[i / 4] = parseInt(hex.substr(i, 4), 16) } - return JSON.parse(decoded) + return JSON.parse(String.fromCharCode.apply(null, buffer)) } function pixelMath() { - const input = decodeParams(jsArguments[0]) + const data = { + success: true, + errorMessage: null, + stackedImage: null, + } - const statusPath = input.statusPath - const inputPaths = input.inputPaths - const outputPath = input.outputPath - let expressionRK = input.expressionRK - let expressionG = input.expressionG - let expressionB = input.expressionB + try { + const input = decodeParams(jsArguments[0]) - console.writeln("statusPath=" + statusPath) - console.writeln("inputPaths=" + inputPaths) - console.writeln("outputPath=" + outputPath) + const statusPath = input.statusPath + const inputPaths = input.inputPaths + const outputPath = input.outputPath + let expressionRK = input.expressionRK + let expressionG = input.expressionG + let expressionB = input.expressionB - const windows = [] + console.writeln("statusPath=" + statusPath) + console.writeln("inputPaths=" + inputPaths) + console.writeln("outputPath=" + outputPath) - for(let i = 0; i < inputPaths.length; i++) { - windows.push(ImageWindow.open(inputPaths[i])[0]) - } + const windows = [] - for(let i = 0; i < windows.length; i++) { - if (expressionRK) { - expressionRK = expressionRK.replace("{{" + i + "}}", windows[i].mainView.id) + for (let i = 0; i < inputPaths.length; i++) { + windows.push(ImageWindow.open(inputPaths[i])[0]) } - if (expressionG) { - expressionG = expressionG.replace("{{" + i + "}}", windows[i].mainView.id) - } - if (expressionB) { - expressionB = expressionB.replace("{{" + i + "}}", windows[i].mainView.id) + + for (let i = 0; i < windows.length; i++) { + if (expressionRK) { + expressionRK = expressionRK.replace("{{" + i + "}}", windows[i].mainView.id) + } + if (expressionG) { + expressionG = expressionG.replace("{{" + i + "}}", windows[i].mainView.id) + } + if (expressionB) { + expressionB = expressionB.replace("{{" + i + "}}", windows[i].mainView.id) + } } - } - console.writeln("expressionRK=" + expressionRK) - console.writeln("expressionG=" + expressionG) - console.writeln("expressionB=" + expressionB) - - var P = new PixelMath - P.expression = expressionRK || "" - P.expression1 = expressionG || "" - P.expression2 = expressionB || "" - P.expression3 = "" - P.useSingleExpression = false - P.symbols = "" - P.clearImageCacheAndExit = false - P.cacheGeneratedImages = false - P.generateOutput = true - P.singleThreaded = false - P.optimization = true - P.use64BitWorkingImage = false - P.rescale = false - P.rescaleLower = 0 - P.rescaleUpper = 1 - P.truncate = true - P.truncateLower = 0 - P.truncateUpper = 1 - P.createNewImage = false - P.showNewImage = false - P.newImageId = "" - P.newImageWidth = 0 - P.newImageHeight = 0 - P.newImageAlpha = false - P.newImageColorSpace = PixelMath.prototype.SameAsTarget - P.newImageSampleFormat = PixelMath.prototype.SameAsTarget - - P.executeOn(windows[0].mainView) - - windows[0].saveAs(outputPath, false, false, false, false) - - for(let i = 0; i < windows.length; i++) { - windows[i].forceClose() - } + console.writeln("expressionRK=" + expressionRK) + console.writeln("expressionG=" + expressionG) + console.writeln("expressionB=" + expressionB) + + var P = new PixelMath + P.expression = expressionRK || "" + P.expression1 = expressionG || "" + P.expression2 = expressionB || "" + P.expression3 = "" + P.useSingleExpression = false + P.symbols = "" + P.clearImageCacheAndExit = false + P.cacheGeneratedImages = false + P.generateOutput = true + P.singleThreaded = false + P.optimization = true + P.use64BitWorkingImage = false + P.rescale = false + P.rescaleLower = 0 + P.rescaleUpper = 1 + P.truncate = true + P.truncateLower = 0 + P.truncateUpper = 1 + P.createNewImage = false + P.showNewImage = false + P.newImageId = "" + P.newImageWidth = 0 + P.newImageHeight = 0 + P.newImageAlpha = false + P.newImageColorSpace = PixelMath.prototype.SameAsTarget + P.newImageSampleFormat = PixelMath.prototype.SameAsTarget + + P.executeOn(windows[0].mainView) + + windows[0].saveAs(outputPath, false, false, false, false) + + for (let i = 0; i < windows.length; i++) { + windows[i].forceClose() + } - console.writeln("stacking finished") + data.stackedImage = outputPath - const json = { - stackedImage: outputPath, + console.writeln("stacking finished") + } catch (e) { + data.success = false + data.errorMessage = e.message + console.writeln(data.errorMessage) + } finally { + File.writeTextFile(statusPath, "@" + JSON.stringify(data) + "#") } - - File.writeTextFile(statusPath, "@" + JSON.stringify(json) + "#") } pixelMath() From 302c7ab92eb875ea7e265962fdaa80786c184897 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 7 Jun 2024 13:55:07 -0300 Subject: [PATCH 23/26] [api][desktop]: Add Rotator on Sequencer --- .../api/alignment/polar/darv/DARVTask.kt | 3 +- .../api/alignment/polar/tppa/TPPATask.kt | 2 +- .../nebulosa/api/autofocus/AutoFocusTask.kt | 2 +- .../api/sequencer/SequencerController.kt | 5 +- .../api/sequencer/SequencerExecutor.kt | 9 ++- .../api/sequencer/SequencerService.kt | 5 +- .../nebulosa/api/sequencer/SequencerTask.kt | 4 +- .../app/sequencer/sequencer.component.html | 11 +-- .../src/app/sequencer/sequencer.component.ts | 74 +++++++++++-------- desktop/src/shared/types/sequencer.types.ts | 2 + .../script/PixInsightDetectStars.kt | 2 + 11 files changed, 71 insertions(+), 48 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt index 65e4ea05c..73842019a 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt @@ -17,6 +17,7 @@ import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent import nebulosa.indi.device.camera.FrameType import nebulosa.indi.device.guide.GuideOutput +import nebulosa.indi.device.mount.Mount import nebulosa.log.loggerFor import java.nio.file.Files import java.time.Duration @@ -64,7 +65,7 @@ data class DARVTask( override fun execute(cancellationToken: CancellationToken) { LOG.info("DARV started. camera={}, guideOutput={}, request={}", camera, guideOutput, request) - camera.snoop(listOf(guideOutput)) + if (guideOutput is Mount) camera.snoop(camera.snoopedDevices.filter { it !is Mount } + guideOutput) val task = SplitTask(listOf(cameraCaptureTask, Task.of(delayTask, forwardGuidePulseTask, backwardGuidePulseTask)), executor) task.execute(cancellationToken) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt index b3d8e926c..192faa152 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt @@ -110,7 +110,7 @@ data class TPPATask( rightAscension = mount?.rightAscension ?: 0.0 declination = mount?.declination ?: 0.0 - camera.snoop(listOf(mount)) + camera.snoop(camera.snoopedDevices.filter { it !is Mount } + mount) cancellationToken.listenToPause(this) diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index d1231501b..17492d104 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -85,7 +85,7 @@ data class AutoFocusTask( var numberOfAttempts = 0 val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10 - // camera.snoop(listOf(focuser)) + camera.snoop(camera.snoopedDevices.filter { it !is Focuser } + focuser) while (!exited && !cancellationToken.isCancelled) { numberOfAttempts++ diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt index fd36445da..17f9d715a 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt @@ -5,6 +5,7 @@ import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.rotator.Rotator import org.springframework.web.bind.annotation.* @RestController @@ -16,9 +17,9 @@ class SequencerController( @PutMapping("{camera}/start") fun start( camera: Camera, - mount: Mount?, wheel: FilterWheel?, focuser: Focuser?, + mount: Mount?, wheel: FilterWheel?, focuser: Focuser?, rotator: Rotator?, @RequestBody @Valid body: SequencePlanRequest, - ) = sequencerService.start(camera, body, mount, wheel, focuser) + ) = sequencerService.start(camera, body, mount, wheel, focuser, rotator) @PutMapping("{camera}/stop") fun stop(camera: Camera) { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt index 48b10b2af..4f3bd05eb 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt @@ -16,6 +16,7 @@ import nebulosa.indi.device.filterwheel.FilterWheelEvent import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserEvent import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.rotator.Rotator import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor @@ -54,7 +55,7 @@ class SequencerExecutor( fun execute( camera: Camera, request: SequencePlanRequest, - mount: Mount? = null, wheel: FilterWheel? = null, focuser: Focuser? = null, + mount: Mount? = null, wheel: FilterWheel? = null, focuser: Focuser? = null, rotator: Rotator? = null, ) { check(camera.connected) { "${camera.name} Camera is not connected" } check(jobs.none { it.task.camera === camera }) { "${camera.name} Sequencer Job is already in progress" } @@ -67,7 +68,11 @@ class SequencerExecutor( check(jobs.none { it.task.focuser === focuser }) { "${camera.name} Sequencer Job is already in progress" } } - val task = SequencerTask(camera, request, guider, mount, wheel, focuser, threadPoolTaskExecutor, calibrationFrameService) + if (rotator != null && rotator.connected) { + check(jobs.none { it.task.rotator === rotator }) { "${camera.name} Sequencer Job is already in progress" } + } + + val task = SequencerTask(camera, request, guider, mount, wheel, focuser, rotator, threadPoolTaskExecutor, calibrationFrameService) task.subscribe(this) with(SequencerJob(task)) { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt index fe26e8e24..871fcd1f3 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt @@ -4,6 +4,7 @@ import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.rotator.Rotator import org.springframework.stereotype.Service import java.nio.file.Path import kotlin.io.path.exists @@ -18,13 +19,13 @@ class SequencerService( @Synchronized fun start( camera: Camera, request: SequencePlanRequest, - mount: Mount?, wheel: FilterWheel?, focuser: Focuser?, + mount: Mount?, wheel: FilterWheel?, focuser: Focuser?, rotator: Rotator?, ) { val savePath = request.savePath ?.takeIf { "$it".isNotBlank() && it.exists() && it.isDirectory() } ?: Path.of("$sequencesPath", (System.currentTimeMillis() / 1000).toString()) - sequencerExecutor.execute(camera, request.copy(savePath = savePath), mount, wheel, focuser) + sequencerExecutor.execute(camera, request.copy(savePath = savePath), mount, wheel, focuser, rotator) } @Synchronized diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt index ecd8aca07..9b624c38d 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt @@ -19,6 +19,7 @@ import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.filterwheel.FilterWheelEvent import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.rotator.Rotator import nebulosa.log.loggerFor import java.time.Duration import java.util.* @@ -37,6 +38,7 @@ data class SequencerTask( @JvmField val mount: Mount? = null, @JvmField val wheel: FilterWheel? = null, @JvmField val focuser: Focuser? = null, + @JvmField val rotator: Rotator? = null, private val executor: Executor? = null, private val calibrationFrameProvider: CalibrationFrameProvider? = null, ) : AbstractTask(), Consumer, CameraEventAware, WheelEventAware { @@ -131,7 +133,7 @@ data class SequencerTask( override fun execute(cancellationToken: CancellationToken) { LOG.info("Sequencer started. camera={}, mount={}, wheel={}, focuser={}, plan={}", camera, mount, wheel, focuser, plan) - camera.snoop(listOf(mount, wheel, focuser)) + camera.snoop(listOf(mount, wheel, focuser, rotator)) for (task in tasks) { if (cancellationToken.isCancelled) break diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index b8c5b9344..998f0830e 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -27,13 +27,8 @@ - - - - - - +
@@ -144,6 +139,8 @@ (deviceChange)="wheelChanged()" /> +
diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index 8577473d3..43b06746c 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -13,6 +13,7 @@ import { JsonFile } from '../../shared/types/app.types' import { Camera, CameraCaptureEvent, CameraStartCapture } from '../../shared/types/camera.types' import { Focuser } from '../../shared/types/focuser.types' import { Mount } from '../../shared/types/mount.types' +import { Rotator } from '../../shared/types/rotator.types' import { EMPTY_SEQUENCE_PLAN, SEQUENCE_ENTRY_PROPERTIES, SequenceCaptureMode, SequenceEntryProperty, SequencePlan, SequencerEvent } from '../../shared/types/sequencer.types' import { FilterWheel } from '../../shared/types/wheel.types' import { deviceComparator } from '../../shared/utils/comparators' @@ -34,11 +35,13 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable mounts: Mount[] = [] wheels: FilterWheel[] = [] focusers: Focuser[] = [] + rotators: Rotator[] = [] camera?: Camera mount?: Mount wheel?: FilterWheel focuser?: Focuser + rotator?: Rotator readonly captureModes: SequenceCaptureMode[] = ['FULLY', 'INTERLEAVED'] readonly plan = structuredClone(EMPTY_SEQUENCE_PLAN) @@ -177,43 +180,53 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable }) electron.on('CAMERA.UPDATED', event => { - ngZone.run(() => { - const camera = this.cameras.find(e => e.id === event.device.id) + const camera = this.cameras.find(e => e.id === event.device.id) - if (camera) { + if (camera) { + ngZone.run(() => { Object.assign(camera, event.device) - } - }) + }) + } }) electron.on('MOUNT.UPDATED', event => { - ngZone.run(() => { - const mount = this.mounts.find(e => e.id === event.device.id) + const mount = this.mounts.find(e => e.id === event.device.id) - if (mount) { + if (mount) { + ngZone.run(() => { Object.assign(mount, event.device) - } - }) + }) + } }) electron.on('WHEEL.UPDATED', event => { - ngZone.run(() => { - const wheel = this.wheels.find(e => e.id === event.device.id) + const wheel = this.wheels.find(e => e.id === event.device.id) - if (wheel) { + if (wheel) { + ngZone.run(() => { Object.assign(wheel, event.device) - } - }) + }) + } }) electron.on('FOCUSER.UPDATED', event => { - ngZone.run(() => { - const focuser = this.focusers.find(e => e.id === event.device.id) + const focuser = this.focusers.find(e => e.id === event.device.id) - if (focuser) { + if (focuser) { + ngZone.run(() => { Object.assign(focuser, event.device) - } - }) + }) + } + }) + + electron.on('ROTATOR.UPDATED', event => { + const rotator = this.rotators.find(e => e.id === event.device.id) + + if (rotator) { + ngZone.run(() => { + Object.assign(rotator, event.device) + }) + } }) electron.on('SEQUENCER.ELAPSED', event => { @@ -246,6 +259,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable this.mounts = (await this.api.mounts()).sort(deviceComparator) this.wheels = (await this.api.wheels()).sort(deviceComparator) this.focusers = (await this.api.focusers()).sort(deviceComparator) + this.rotators = (await this.api.rotators()).sort(deviceComparator) this.loadSavedJsonFileFromPathOrAddDefault() @@ -262,6 +276,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable if (this.mount) this.api.mountListen(this.mount) if (this.focuser) this.api.focuserListen(this.focuser) if (this.wheel) this.api.wheelListen(this.wheel) + if (this.rotator) this.api.rotatorListen(this.rotator) } private enableOrDisableTopbarMenu(enable: boolean) { @@ -272,6 +287,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable const camera = this.camera ?? this.cameras[0] // const wheel = this.wheel ?? this.wheels[0] // const focuser = this.focuser ?? this.focusers[0] + // const rotator = this.rotator ?? this.rotators[0] this.plan.entries.push({ enabled: true, @@ -356,8 +372,9 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable this.camera = this.cameras.find(e => e.name === this.plan.camera?.name) ?? this.cameras[0] this.mount = this.mounts.find(e => e.name === this.plan.mount?.name) ?? this.mounts[0] - this.focuser = this.focusers.find(e => e.name === this.plan.focuser?.name) ?? this.focusers[0] this.wheel = this.wheels.find(e => e.name === this.plan.wheel?.name) ?? this.wheels[0] + this.focuser = this.focusers.find(e => e.name === this.plan.focuser?.name) ?? this.focusers[0] + this.rotator = this.rotators.find(e => e.name === this.plan.rotator?.name) ?? this.rotators[0] return plan.entries.length } @@ -377,16 +394,6 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } } - async chooseSavePath() { - const defaultPath = this.plan.savePath - const path = await this.electron.openDirectory({ defaultPath }) - - if (path) { - this.plan.savePath = path - this.savePlan() - } - } - async showCameraDialog(entry: CameraStartCapture) { if (await CameraComponent.showAsDialog(this.browserWindow, 'SEQUENCER', this.camera!, entry)) { this.savePlan() @@ -415,11 +422,16 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable this.ping() } + rotatorChanged() { + this.ping() + } + savePlan() { this.plan.camera = this.camera this.plan.mount = this.mount this.plan.wheel = this.wheel this.plan.focuser = this.focuser + this.plan.rotator = this.rotator this.storage.set(SEQUENCER_PLAN_KEY, this.plan) this.savedPathWasModified = !!this.savedPath } diff --git a/desktop/src/shared/types/sequencer.types.ts b/desktop/src/shared/types/sequencer.types.ts index 58996386d..844a540b6 100644 --- a/desktop/src/shared/types/sequencer.types.ts +++ b/desktop/src/shared/types/sequencer.types.ts @@ -1,6 +1,7 @@ import { AutoSubFolderMode, Camera, CameraCaptureEvent, CameraStartCapture, Dither } from './camera.types' import { Focuser } from './focuser.types' import { Mount } from './mount.types' +import { Rotator } from './rotator.types' import { FilterWheel } from './wheel.types' export type SequenceCaptureMode = 'FULLY' | 'INTERLEAVED' @@ -39,6 +40,7 @@ export interface SequencePlan { mount?: Mount wheel?: FilterWheel focuser?: Focuser + rotator?: Rotator } export const EMPTY_SEQUENCE_PLAN: SequencePlan = { 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 4c3e58a40..db7b9a684 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightDetectStars.kt @@ -31,6 +31,8 @@ data class PixInsightDetectStars( @JvmField val stars: List = emptyList(), ) { + override fun toString() = "Output(success=$success, errorMessage=$errorMessage, stars=${stars.size})" + companion object { @JvmStatic val FAILED = Output() From 44066a0f54f910718e2fec17ef2fcfdd2d723d17 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 7 Jun 2024 20:08:20 -0300 Subject: [PATCH 24/26] [api]: Fix PixInsight Live Stacking --- .../nebulosa/api/cameras/CameraCaptureTask.kt | 1 + .../livestacking/PixInsightLiveStacker.kt | 32 ++++++++++--------- .../script/AbstractPixInsightScript.kt | 2 +- .../pixinsight/script/PixInsightAlign.kt | 2 +- .../PixInsightAutomaticBackgroundExtractor.kt | 2 +- .../pixinsight/script/PixInsightCalibrate.kt | 2 +- .../pixinsight/script/PixInsightPixelMath.kt | 2 +- .../script/PixInsightScriptRunner.kt | 2 +- 8 files changed, 24 insertions(+), 21 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index cfa8bf23d..d27ae0fdd 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -263,6 +263,7 @@ data class CameraCaptureTask( delayAndWaitForSettleSplitTask.close() cameraExposureTask.close() ditherAfterExposureTask.close() + liveStacker?.close() super.close() } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt index 51d195db8..6c4d52115 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/livestacking/PixInsightLiveStacker.kt @@ -61,15 +61,16 @@ data class PixInsightLiveStacker( stacking.set(true) // Calibrate. - val calibratedPath = if (dark == null && flat == null && bias == null) null else { + 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 null - LOG.info("live stacking calibrated. count={}, image={}", stackCount, outputPath) + val outputPath = s.runSync(runner).outputImage ?: return@use false + LOG.info("live stacking calibrated. count={}, output={}", stackCount, outputPath) outputPath.moveTo(calibratedPath, true) + true } } - if (calibratedPath != null) { + if (calibrated) { targetPath = calibratedPath } @@ -77,27 +78,28 @@ data class PixInsightLiveStacker( if (stackCount > 0) { // Align. - val alignedPath = PixInsightAlign(slot, workingDirectory, referencePath, targetPath).use { s -> - val outputPath = s.runSync(runner).outputImage ?: return@use null - LOG.info("live stacking aligned. count={}, image={}", stackCount, alignedPath) + 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 (alignedPath != null) { + if (aligned) { 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={}, image={}", stackCount, it) - stackCount++ + // 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) + } } } } else { targetPath.copyTo(referencePath, true) targetPath.copyTo(stackedPath, true) + LOG.info("live stacking started. target={}, reference={}, stacked={}", targetPath, referencePath, stackedPath) stackCount = 1 } 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 f93800306..57468b766 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -28,7 +28,7 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen final override fun startCommandLine(commandLine: CommandLine) { commandLine.whenComplete { exitCode, exception -> try { - LOG.info("PixInsight script finished. done={}, exitCode={}", isDone, exitCode, exception) + LOG.info("{} script finished. done={}, exitCode={}", this::class.simpleName, isDone, exitCode, exception) waitOnComplete() 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 82d4ae4eb..2958d15b0 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAlign.kt @@ -64,7 +64,7 @@ data class PixInsightAlign( override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { - repeat(5) { + repeat(30) { val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { 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 b06e3d6d8..a68dd5049 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightAutomaticBackgroundExtractor.kt @@ -43,7 +43,7 @@ data class PixInsightAutomaticBackgroundExtractor( override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { - repeat(5) { + repeat(30) { val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { 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 9dd204749..53cd83351 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightCalibrate.kt @@ -54,7 +54,7 @@ data class PixInsightCalibrate( override fun processOnComplete(exitCode: Int): Output { if (exitCode == 0) { - repeat(5) { + repeat(30) { val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { 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 30eb3e1aa..a4cd13ad8 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightPixelMath.kt @@ -50,7 +50,7 @@ data class PixInsightPixelMath( override fun processOnComplete(exitCode: Int): Output? { if (exitCode == 0) { - repeat(5) { + repeat(30) { val text = statusPath.readText() if (text.startsWith(START_FILE) && text.endsWith(END_FILE)) { diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt index 74be7d3cb..6becf6d2b 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightScriptRunner.kt @@ -13,7 +13,7 @@ data class PixInsightScriptRunner(private val executablePath: Path) { DEFAULT_ARGS.forEach(::putArg) } - LOG.info("running PixInsight script: {}", commandLine.command) + LOG.info("running {} script: {}", script::class.simpleName, commandLine.command) script.startCommandLine(commandLine) } From 82219513d736ccb89c0ba3c3b93079069c72ff7a Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 7 Jun 2024 21:08:10 -0300 Subject: [PATCH 25/26] [desktop]: Fix Guider chart zoom --- desktop/src/app/guider/guider.component.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index 7eb97a0d9..318fb1a39 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -129,9 +129,8 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { const scale = barType ? this.phdDurationScale : 1.0 const y = context.parsed.y * scale const prefix = raType ? 'RA: ' : 'DEC: ' - const barSuffix = ' ms' const lineSuffix = this.yAxisUnit === 'ARCSEC' ? '"' : 'px' - const formattedY = prefix + (barType ? y.toFixed(0) + barSuffix : y.toFixed(2) + lineSuffix) + const formattedY = prefix + (barType ? y.toFixed(0) + ' ms' : y.toFixed(2) + lineSuffix) return formattedY } } @@ -197,13 +196,20 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { dash: [2, 4], }, ticks: { - autoSkip: true, + autoSkip: false, count: 11, maxRotation: 0, minRotation: 0, - callback: (value) => { + callback: (value, i, ticks) => { const a = value as number - return (a - Math.trunc(a) > 0) ? undefined : a.toFixed(0) + + if (i === 0) { + return a.toFixed(0) + } else if (ticks[i - 1]) { + if (Math.abs(Math.trunc(ticks[i - 1].value) - Math.trunc(a)) >= 1) { + return a.toFixed(0) + } + } } }, grid: { From d2736c0b153386888e2b960dd1544e5371f0b156 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 7 Jun 2024 21:08:41 -0300 Subject: [PATCH 26/26] [desktop]: Update screenshots --- desktop/camera.png | Bin 41677 -> 41516 bytes desktop/guider.png | Bin 27628 -> 46248 bytes desktop/indi.png | Bin 25855 -> 32368 bytes desktop/sequencer.png | Bin 64819 -> 67173 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/desktop/camera.png b/desktop/camera.png index 8487d530ddb57e219528f1b13f2ac860057d66c5..c25ce32522db1c75c836e3d1a5445939eca54088 100644 GIT binary patch literal 41516 zcmb@tbC4)a@GUsDZQC~Q*tTukwr$(y9o(^P+qP$RzW3s{vH$Md?TGH|?5L=y>Z+*9 zbMjP#ysQ{36c!W!0069nxUeDs0AT#@y#N9B`vuZT7Vg)8IQ@}OhWNd_A&kR+eN1N& zb!R0zQ)f2=M-u=uTRR&QS|=k%6BAn}b35l7;9kDpLS+9H5^^*#aJI0sB~Z4oF#&LJ zu^?bzBd~O_C17G;Vj*B+2qKW+F#>`hkO1OOUikl??mhJ$&$TI=?N{hzQV0qj|6uMEJhLA6 zVh0k)o+TO3XkEaIcdp+TrE1J#!@&@&_3MYAcza&jw=4hLe8F@!0fZVgqJS@`H~Ln; z_YWFe&piP4_AC02=bZvumrp#76~bH(Lh?O|R`2AYu^=J&0>AOWe1bpoO0Rcf|71?r z(=r~DTrSB&R9d^=mDDAVb7KEN*z0Wq2oIokQOBDf-{Gu-*OlqXgd5WwvbWuqq=5k$ zx6(_zha)c ziafelH4QiY-XzeBwLK2lSiYwhVyVLON`I@pit@B1_>f_3rh8a{A_w7%S)$wS(LHT?`Px_Xtf?`%8`y z-#+z^lrRevuK@0co=5VDYn#6vP|-=X*%C2*gXFn&UH9wz{$$0(k>YC$wBC?X)(v^5 zRtntN)?BeghNk+jm=NlM@n|+{|A>nyP!ME`ZJty?h3;?qI! zJk0#}z6h_uKY`w;XSy&FY733A+Hhxr$iwBqfyU69<}A};b? z7`w;rOV%jm+Hk4bb5UG-#z~%bh*=8Yt;oT~#-J9*YrjwR2Lt#C=f&FVT~14Gn{E&0iB6p3NM78dl~aR~xw*L(e+&V|r{_90fkjRSvO7-Z4)e4~Bcx~ld?YUN`? zOe3u5;X>y7p}W>Q^f(*JbXB^2CqHC?m93rrv4ykS_Trw_;yc`fZDP3e^hX6o;1OUk zfDE)ph0wg?fuDJ4-qGp(M5~(S>pM4#L->15su}_>7^9nD>%RSvW=BsFHdrA7rESzPYabNyi*`mqHl6Jgz*>rS+y3XK2TCNMO zgIA)9FrJc%LvS>v_lU8j@&0rmL%A&C@Mews?q#LPWzWMTbjju@Et+FIwA%0ZNZ4s` z+}1{uT`-&+FYa2486}0k8?~XF%;8USnAi5Dxj!71@!Px3ND@y^nnbB%Vv@sZaGofA z9+vr(OH9}bo3H@2#~wKs!!vy9@1V(MhO_94%f@O*m6_05b85$Fn)$<^6Sn0TbDD@f5>p&=qtQ}Hg?WmZT7UeHW|Z~^d-c2Y!P=qQ&uM5b>TIxb=7;- zW=#Ckz7ZGnQ7PfEC!anY{C}%2}7h z5{>HDKFYHOW>ZbKAq25%;wo$(9lLYvH=g z5$Vqwx=yN0bT=Z)HF_hRv`drSi~$dvw}zXC1XwOhqo~i6_YqMVt6eE0dFms~n3}1R z_X9TXl0*->##_j7O*#$XAy-0kwD;nPWG;t-Qv1TR$Fbq2j72$#;d&o=_YxAqc7lBZZC21#fY6+d_t6W$rC0E%2Jb9NJYPY^l9%3zIdWUB zwrV$G8;Zk##WPz};){$>{pHPek6hJ`5_`XPj=AO)QbbolRkoIwR>xDZ&1vHMgNZS@& zOrX7_X#3Ies6@F*P!NtuNcvb3d_2eK7X=ekpo_`1o!xZr$*3qm+{CdWfI{JzY*S8j zs{~_r<4hfX5V0{*!0Qz4w5$e1x5Jr`xA8`KziFiS1)-zfV zq7HXFn5T(ZWC1=W@#Pj7i3N@sJ<_-3FQ7He?`{qJCRjTMLE04H<_$d;>QLvhjVnfL zoUe+=s0D2ULF#JaRNo%B(V>C`56Sz&z@JuNw9>(FCld}KjQKJ%<(xq zxRKSMJ$Y#Bm*DckJGmn(;_#v)p-mS`u+;e4({cLq1mBp|X9y_bMRufkS}r)C;oo4&yV6;i=hV1fcXU%IdV`1XK>Psl-kF802IzHpZr(F6IzVYP7tdKVxS86N4 z`Xip{OXQ|^GaPr8g6{nKLIo>`02b{YZ(k2WV|-_fkMT?)M-Rl1^wQU4Jw+bk*S=jqkq2u_UVUPkl%ld>(yb%bkO|tnaplI6B@6ax#X{ zb??tq@duvS!JqgEY>xXG^ooRF9dB-8RmT?`*qE~GlQVY)^qc)7x3;9a(JyOnIEszF zs4{>U9|x(omrwP-2sWN*Xh@x;ri9ep2pys$FGs0S%ieT3p#%`XB0S*#8)TUGBmJU} zcVQ3&K?INjIbi_A|6`YHCqzX{i#z%4KRi>5M3n-C!1}*xjr;#nt8toM@sBtO5(*S3 z(^~$wb)9;R>ZB=iO-+qZ4yqtRa8-Z)k^l)3WNmG&n(-|_{FrfjXD3lYxMapJc(Q}h zw5XM*pko>dy0uDm`dYMSaYQCv^Ag|64cO*V=e&?Cke}V$je4&D})D**YW$l}xh9rT&DiobBrY4-eJ(?c+H}%a=IsW<-`Y$$&g!KsO z4U0n3lwNEDVo`E@1OR^^5Uwh|yioJF9zgQmPfX7Til6glN}19_J3z#g-a5&Wma2?Xceoa<282YxeT&B}>xrU_aWXivY5B z&Z^HEza87o#HyvAjsy?6*ftIko$EjBjMY6SyqS!xPOLJzz(ctvR@l@;^Q+0Dc8&dx!T9N)nQRhUVO1IfsT{O1~)Yfkk~h86`x8oB5* zWUn)@B5=oRcoj6O*$t737jRTs4QZ_BxT9Zp=D@69fOw+rD3KK=A5|Ap$L%7RpJg zE#BVVmIG>TZhq7%?Dl{oIjb#eH(TR5p3EJ{UW0D89fMIWi;QT0feX#!5Kn+SJn)@p zvJ#DVV#o_+it=;yXAVuEyLa@>kwo3X>HkTa0YZjI)JdGB`Yh2J2|uR;93)2X9TX9wk$L& zt=|tN(T+2d--IC`cfRKRyk15*p3D)9ZVapjB33m4p?s+|d1`SI&qWd!vSl+NW9{&OBkW&v`(chr$Vs6RqB_fUUaa>&V;8jE(3P zmqVY*iJK}SBCpb;UE5*wwcv>jeP=u`vy zX=r00K4g;9AF~|guZP|e07Dq#*x&yuUp{Ndlp5pA4f8s*{U*3Tjo?!8llTy>xBCCxb8yZXO)c4Mp}QWF}&7|bsTcj zUI}M7j;-;#;mLX(+R67XfFIsHrp8BNd1?hE1LA5378x= zXOC*QBbXg237lGceb1nn9@i8^o163zz;FBq07E@C(QY+vAQnCI^R1^l_rp zG$!{$LF91ZUG(J`V+1U7Ir7O3V6euRhd~g z?b5>hVCL*U8Mb@!JMEmDXlvnz1 z_E}$5Y|a7`nassldMsGoPZFByv2&O!1rPZ0tb&YZO*^ zf^dJh%>A*Jm8WZuQ`i0ZWfC&gErT8=zZB>C=R_9jPihW4(N*7pZkqJ9--%RDH<6V; z-Ya;FQTm4b$v{90=q-WsbJXWa$t*7b(flMP0d!d%D<-=1}&XX68NfXx&+^IXny}&SYy_`?wehV}Nul zrT}FSnUYt)&SJ1ct-zY^;Iv7eKjgw?--Htra@WJyB4;tB1<_A>!s!^|v-5~7nRJ@O zo`@x6n#@dq)qsJnhb1re!XXK1@UtOs$oIS>T@W{S;OMy3>w=7%eH~HKN86>b+QQKa z`g)EOxfp!DK$bnd#8zM!s2Sb*4FBn3aBSAhYx%6y3odK`AuPfDE+|b5)!~8M!x_zr zbBJ1O|KXGZWtDYo5mfR_o%FU$lLdk!D z4V92Bc|tS6O;|0(a798~rRQXay16HE01>IdCQcA3TIE#@`Ht1KufqwA1n>PxB1pl# zop45Jdl;7c!&aDRZs>BnPbj@|_alCn#?45i1r5lvlINp8zT{fEn}g^zPU+Z76Z2BB1^bVsOCwzy-b0lYd$zU zK44pSK8KGx)X*P8@j0&NgDTQiuYqx~uKXs^C@EtRcp7550iX!iJ48a7aJL7*#uA)$ug^ZUJoddl53;Hn>97Xp0U~8f;2dy~Jr= zg^n$=#%D4z;u3?2S{d4{DQ&$M;i_noQ5UnZ4Qg6TMnv|s)aOji_1C8c=fm_yr6frM zCcIy<5k{C?L}@evIbP|X6UImPXOKd&h?7O^Tb4Hu?iV-3)@RG)j;!XBy9YIO1yyop z8+vvI7YfAsr9?KkT5u__mZ~%-&kbH1LDrskU~IO&@iKLMg2NrL9ctnT7Sd)XR_yh6 zdRjAW(%OWfmz?ny3&gjNk7ipP-<+vqw2Qg2Mi<_C3Jap+ZJ|M<3H)NJ3Hg& zV!1{wi+_|Hu(Wo@zzYSD`Wi>_i~!LxBj?xNXPW6kTDe?l#y8_A&NpeE)`yBx)xt2#XA}wH^X3&^IvyoisY9@>3mP;bGhDpbrOWy7 z;d_{C^$;IGLEy#yhe-|eTTGL_ZNS(EYmKMNqv%biX#Y9_K{!Ja9nj|89%I+R`rZQk zlNPxqN5@6wOcwrSs9}KaMEa}Jo3KizPH&4l2{P+{0ikEn5C==niCRFF$!^|nbZ;N> zUyKeSc$x$VNuXL~PJq80R8>aq9oe)ZiEZFZjOnXz|^s zqu;Kx(NA$X6r7-{YIH+hu8cBOio3fzK|(kN6+}P1l9d(p)6-MBH1Gr89v~C}T|20> z@wPiE_l{gx1+_$k3h`BI^3^6H-*#)<{)A1Iu#EA(Vkt}6-E}N6MhVrYG9Al`vf@Tq zn=nPn`1H%#OQv$b-*Ma!rH>sOQ_Cu>`me{lUlfahf)r8QRE-@@K=kzd?2D9NQo_&w zXEU&%KGwf@jU;&Bj-HSFKpL{lQOL1*t8gTp^l( z&z7i&l}t8|39)lmG$|08Zu!EvC?jdNPoo>(sTNnz#dnRs+f5*nZ4awIOIC?+35jd@pHa|Q};XhM%Fl~kp<1C;)H<&kuJ4q@;79*0F~ zqqhf-ZD&$4rIALXBUj~kQnt%$Yr3U2RBy&r7-Ly*=P#m?kaNknXxVtGmZvbczAQT? z_%}2JiZKc=*|J{Ws$t=PH#4ttd*}5*alMI71+)R}li>$MT~}q`WQmrx&O>iUZ|rY< z$hz5nDz*J}sMG!}s=CSEuyLc=kU8zR8}#zpH|wOy-?2762Am@H@}7jEGDwpu=A30+ zLwZt`W2~&*gW-z57uRtJVA}7i>FYbgPIdV|S^zi=fWB)m$4Y>oJ>eLJcrE3q8loT_ z6}H;wm2XN2$W+JYS(SIxUIT;I<<$5k{3WH|pH> z@&JZ$ixjN!uS9bL`rK6D0^N-b01E-bCnpxe?(=abQc@wk{4$4ck>wlIPMTbN&%xn1 zuotZ`lE8z;%quHori4gu3_m+~vgWt`$IBJLv7+tjrz{?{7?$oaHVB&Lln+=wZn$#7 z$s)dm$!bIwVf5U1BK+bc6v;LS-82|fZy;|1U{7voiPCDsy{2j@S{ZAiUr_^3D%#e6b?&i?8i83 zD#S_zlV>DWalRU6pqb1TR%EJF2n$P4RM_>UUI~F+(7$Zib=$5@*FB+Aw{;A!tn>xo zmoPiLR%D?*Lf*8`&c^3WA7iNe>hZC6Esw`t)ny+b1%@DI8!Ko$7DEEgotHMkFSa3b4tsGZUj)H3kd}%j6rdbZ&5>GX=Tzbi(q^iKMiBApM6475cWTpe3l1#jQ|Ki-JF1vJ56KBCEM&7mC*cuiEhf;sBTH!VO zltP^wP6whu_sH0oG6HiC8Vr~~gb>)N>C3Yj)9Lj(9E*8Y!>~EU>2bo*KF(Ajf8X1a zRm52N{L-VYrf_TwydnuoZw8CX`I&Y;WeK53Rgh7CPRVu|sgzGL1u-5B;ap9~BW2|6 zQY}U423^v>87Bu;KCFN>ttjn9o=2OG_ow~&Vo6+DZe^&xefq+KAZrbr!=D40@gnp2 zhrHepwX2OcZf`6q{q*BO@fwC`!wZ54xIc8tdYIP`PGYY%O=n%(Gkl9bRqu1xBV>0+ zSvVHe3m_qA?^gDi-BLmuMUVkm<#Nr_(`R7W*{(%o#S^@Vj53=(s(@`?`4Xy;aM=U#59 zacuP;BaXN5wlnESEom`bb*)O9yRwh{9ntrcBon-;>N7yDWyiC-hy#TlRR3}m?&j>R z$ty|_7)dUL|V;znvJWmYa{Ri@a+v z^ChJspyvi%2SXME2EQpQqyz5CDt}T)hq)zc5|Yw*@HG$);kcX|Y!L}4&Qvr5qin1{ zXg7CXFha$2nQzBjok@I^J-71@hp^k5kEkVk2{T)HLtK+~uL5@l5CJduh=Wu_!+3>ZZeJtY-7bQ(iD~kQh~2{X0C5MOp_AM}cRj ze2Tpah8@D00?P)ncdH%Qm9JpPn`*?T&$Go|mzV0E5a%hDKeBhA3$q1_zmN0k5 z=#d43v1vKPTVVE6g{s^KlFgyZZ!g9HsGG=!Ci5ZeeE#_>7CqSVND<0rCMq$NzL7<0 zV~VXP;?+US=+rtZs+>-bDGEIQPo8BM2`YKcIaJUJl=P_x0+&ui&2WHST52H!$lBTF<49L|0OZ5PoUqI=-Bw+D{0%282n3|Sz>_9duC~1P5 z45Zq}N3&FZurj0?&ohYX25BXd(fVC5uC# z{sk}6V=Q$5!Pxjq2lYnfBBwlmDY@&Tb6KaC@YX=5%PeaB&M!CzKB!J*Si0doZ+Xz? zdVWo3Wfn2SEQF}a_V`CT%d3#i{r-nOUQ4PXX^l%NUOgbzY-S?d8bRWR!qPu-DK%hq z0xjCi509&piqp#@2*==|Pz))>P2KZh@wW=m(%OHd$M{@v zJH$6!;fs43NJ5qp8%6-k=^F^rrJ~9^$f5x(P$T78-xtIhVq*Au!$HQQ9);sz1Ra)z z)%AHrMy5bUobB*#bUbrB+v&inXgv{t#i7moc!ZB`E2_Kj_*TE^ELL{J=T%UIi^l;z zyS*CpwNOz_(oPdx?g=OB0unE!g41K8id(`g(gD)8)4|ynsyI` zCYu?s;4aC!f5VKl-Hn8}>%10wpw6(dDAtx|*U3OJi0@P+xX0F+RY*&@OOq9cuo^{C z#%Tk@GdYXztDYDnh8)g+vfcFS@eGD1B1Rnb&_aXC-qg>H)V+X5=^m3R0OF7k3I?N9 z=bXG%n9PdJNnV<5Re8x~V!T)I`JWFuDBz(8NV$WB^*FA6O5wEa8*jT@krhQY##fY= znjhR|`NPcV;%R8pcx?t&%(E!S67_|liB79_NYZ%E*j3NR-W6rs<77cXTx{pqHWXXQ zvy4l;!K?DvoNG@k&F%ZwaJW~Bk^jsKQHOA9im2W2c{H)g-8So>XbZlt%Y9PoyI|!h z3Ml=BPBLdLOHOB$6}6>qh3m0|EVQs0hXazvw=cU>H}B& z37Fap>=u-UIyQ`~G=hMI6Dpu0FY6CAtrqe`$}bGRZx{a)I0d4No?wuQw7FpSLe@?F zZ9pQ16X`e})GAzyC-HTG!s|Fd2)#lPusi@q)oeQ;lir`t zg7a+1tTmWxs$~CuEs>Yd(RE4UTH=0r3BVVIKPsYB)07Yy4dEO&RFQI~@j8S)ge>uqbYW@KuOG8Uyxvl)vO`eYl0-eC`UUeEUI(`HT~Rat^Z_ zr)b*5kH|}y9Lz)FKz#~oyi2=3Sqr_c?A8o8D4_!*$BCKz>=)dCW$}2ah!p4%t$zoO zkQIQYmdb~d1}q9XD?P0qy#WcQFg2dv?u|9p)q|`tZ)Rgi&LMmK=k^!}}X$3&s zX$&_>dVJhmdWlS6hqa~&)A)6Y$U_1}F)L!9_=AEiD1yCL|9i6I4-RK{$0(R;7c6)!qFbxGi(6KP>;m>gPbSaNv#QPzY*lLiU~qe=e3fWq3BH?{$V zdB-xwuTB*T61K^A?VgY6?QCT)o|x)a611B1$mXT!J7+F%Ntui0O-%E0QwYE!#JhI<>Q4 zcnoTDTSK{#zvw=iV#<0Bw{*-L9M~O~XPpYazrXn{l>X(MhDe1kJQ8^~XHaaWS@OSm z6%*(i1IUy`ReW{KKzyQ9B8YOPDdpc79h57J_^(TMYzHrrr;03C7yD!`Rgt~xCcPn` z0S%Q^)sBO+Yz2w#na~KVIKP?D&vI5$9VD{je;uFX!uP9`6&4om%%8h+IP3yJ*hWiq zWP}(gr_~&@p!tsd#mDb>w30NY`Xd@v)FbQ!#&N*zO0@@jC0DIS!W$#{urMMbl~S%O z>Gjh;)EqFSEoC#Xr2eTBI+=EQmIV2rm~UTMM(_ooQFwdCAA~Cqte5i33_iX?TUGn8 zLI4)0Wrrm-(_o$jysVa8grVzj!jl_FFwyPk@O1NakN=X5dsM&Tn~l0UrVdA-Q3OPK zC6r&5(KcVVbQMtm?1?j;I%3QitDapM>Q{?^6~1Bj|CIX}+XICY0d$x_%1Y0-oz+IT%V|Wp=>s5<=hp<7Zhd5Pf5J_nJM*_QIvhzVgSXbtIJ7s?1SWG2aNdbx zRi;oo?6ie6e`Cjx{YO&H{R2fXA?kU^6+JTjX_^9)Rdmx1yw-1*oLbxRE9jUo0*Nv9 zd8Z4y6(RkvE%-3%01mO58<;%i+>v_|#O{;5z$M5W9(01dmfU7ek+{qD#K-TkSfP=O zQK?MalOd);u;N&pbF40ZN=xE-TrucsShH6j8oqKge`OZ^`Iz~9@c`PNK+`{GC^4SL zu{(&o^fvTF6io)ruioF#UCbeL1qnCZPgn#(q01c7{2SQq z)=>TvPz?cq#ye)h9_7QepCZwQ7NL84_9Z+iGUx1W?!mjC%+_6;dHjl(sK@+a4{SK^ z+|IeSCC^c!ZZ@;Ht?+H0{xTe+>N)EiXb@i$_d5XFKl~v*!#auXl&>p3W{K~CB_RvMTpr!ufwviD9etR0s_kUB}XTXendaMXz-}@E#PkUqC@USjy z??XvWKa*QO)$N~wY7OkRclhTt5dHB;|63R)SKCkT%%PxlBP=B+S1GkF@6oLDDqql- z;nms^ZvvLGJa{oN75s?8Ri)GJVn^+#V579ri}!mExgVdw9=*{9op|?aFihQC z%t_FQoVptYt9L_G+fQ4#sR{~vS9kc_wfxr;-S=Bz?(9=RFqt!SMH%zkyF0~%ZeFn6 z8R7T0B3Y4MW~AMSw-cp>78d3Uzg2oUjmZq~PC`m^as)a^q2OUdbjA;3P!qm)sP%hN zxu3d>MjZ6AV)K`ZF=bvO6KF~TH$_-T6uPMy0S3UOmMXIC6>lPpr`cZuZY>yf=Fc6Xnv%>xpE=n*?5ZAb_ zgCYn5S4B!>PTxibz8DcC1U3FC2*1F|mR$K8O(PT&qWdqK)RJd(}f`*q#y)_qn z3CS_HwGmqCDWO6)XM4Go)DH>-zBav9el*x3B+Ft+`q+sn(>RNM+TiW_>MeQQ=?y$u zDfiUgUbh|^YT>OH&v8&_)n^18e+JFWvQVw$`bqBaw-D!lEFpX?Rjcpz;l>Pws5Y4% z+Oc&VoGGV7;LSx`C-3V+4HGpD5I6`_XxSnn~Pj|oHVlVYd%Gu5$o(Jl%CFZjAv zI}I=|Gr>uz!G$@roO|55N90!6>Z5DJ+)mN?WY8@6*1RezY5vj^qg(A2V$Si(YR9iP zUf&|6!S^@h5nT6VEsO6sY#ur-rGs-pdq)>wL(`m%lUR+5W_Ld@s-ooKqJLRbp6YKI zQ@Z4+j}Nholdi#g_J(+;73Q$t_DF>jFYQI3vToFx;m^e8YT)D=X6RR{6`ohfX+E0d z&fi%2=QNmOu-|QFUIkqsJHRvvS$B@?8p{+v=7@b*x)8`xD?h^)v}+!Eh7*=G(+NYJAWn1r49g=-+OBL1p2nqi*f?}ji9Qsn4%ZaN`z9R1AV*V zZ}dC?w?=RuST<#W@>y6fQngA9%LafSEFdc>3MPOOB`B!nrx3VT70#+8gK7*Bi>T3$ z*J?}jE&ZOhTpYH9*dBVaoQ5o|{KQ()8w_+*KIIi(&ue3xfMy0$Y0Qu0lYZI2V%N($d(mV>{Fsb+XZ6CcOH^H=12Q&Sqp`=gE`H{01D~g?O zT&*Ro@CPvt!!1XypKScX=kKQWm|~Bam}nvN(K1Ld< z&JS#0E4i?4>GjyOXfkF^HMi<|(DQtiyrbrv1v1$GFCj2`kF^M!1uV9zuxt0MCG+Zi zZ^hx$>svroI8FTEhAqKun3@OwOwwqCgESs~EajDoqEmHyFzG{G5<*g!Z#ub;jnmO8qj@^XHoi(@h zwZ*;=K;hTgv0o3U;GaLwWZ>9-x|+KVjuZ4b|5c4t2bWbJvlkuh){v+Hi33S=+OlOP zD(g>>#_%5Rsj9jR_uvyeU$ku(1D3#vpr&{HnlD!O?A6nO2l+sz)pv z4Tl29%FEWNh#9`YY}2{X@GW@DUb;uP`C-ha@K5#>6vE1S;Y$Oh$Ge7+&aCzxuW7rK zvPSXMLX^~~M@9yOw@by{6bG; z83(qMdw2>HgVg2$M8~A*^ukNov1qr6h{m0$$Igy--vvEitFl+Jzmp! z$WWd4UAgI7bs#*A>k{KtmDcMS&vT7CDjlh7v2(NH9dYXwvK7HIsex42?3*u9+zki= zPalU-R#S76o#HMvdrK65JgY!xO}95;^m;Z^RD@s38!cB|A+DPyZ8F458u4(l_ha4q zs?0{ z3M76$=KbNpR+bf>og)6Y>Om8kl<+q5bI|RLk90z|OAAOM@4Cy&N3B-WV0yp((=#sU zL=xdJb-9`#(e%XfcY9T6Y)I2%ePu@h!8O4fs8N7r+vPOoncK(8$*GMm!gKuoo}%K( z^p7MHVT;(9y>(NP`))yUK}2s7tenkm-ws)Rf}qbcU90tWe{HsBvMt?c^^~(WPg4&= z5OA2BQe28<5oC;CemgLpq2W&vfpKx>zS7ZGVy4$A1qvysa>UFQBs!sz)$FMDt$1ey zYmMXK>76}zy`l+)(2izY3o1;B$?1UfE`6I`c@0_SMHVIXf>F+1fN0U6P@%#7IIDF0 zXagI?odb|&6~)+e(I94_;Yq{kviRXP!OF|Mqn)1KO>g;$HyhBXJXLj0$cIR4@~)Mw z;a$09&-VzOY!= zN3Krbn5AGT#{3Npv~~jEU=AZYPrpxuW&EGF6Z+eJ@}YG4ULx@k`y0K~#E02Dj@c^I ze%YfU+nT3Tw{(_S+gzLCo(J^u zCbz0xroNOrOMDlWEjN}mZI~Cv*(7NzMo~qSl$Dhcz;Qwl&GdJtb24u5IY#B)ID2<_ z@Zs@?HZ!=*ww+G8Za7~yosNTV^pA!Fi|PCVH&|6E)~CnO-JFO^K$0F;A0{KftFBmn zynTt+gz!x6(is^1W~%BgMWJAawGBgO?|2w>r^eZ1+3O~^OE12veLqfK^QeJH;J!Xc zx5v$ofDG=O)PMe2pjAu0OsThrTeBztPB-bA@S79iEqk!QhKI$^mbD>v17gJYa(AgUcf$Ao)wS z5>fwwh=~?}n_I4e!J;d3vI1S9Y6dY^k+KtWtkYqF@UHO=`I&mG1S+4i?AYTbk6WyBYDeXZ|3g zJ^mg&>e|tTzVFCL{=uZDUzIkvwa3$Y137R?ZQA&VLr4A}EkI6`^mE3*yM_miyKmbH zIUYYhKT0IpGe{bq{)!o+bQVKa*|6p;z63FU*;!gZY~Ra_iLx5>T3&II&uV1ni!|Ac zcO;@JFsI%br>c<(n9?+(BQiPD)tEVqiQ^KcmmnOuR2kzn`oXB*0fR=GDbsU82Zm%t z9yk`&NrE@krN4K=ClPAI92bYy4;M(JxhN@lzEWTPnH(bwgNe;?%O|3=3}s(pQ`CT% zRC?+?#6u7b2HdrwoNW#e2J>nMbEd^hG1HHj{v`ju*cmt_L5R5o&Bw6Em|{Bls--DN ziiCu#w#woy+Y!SCd0pM@QxWfwKLMIGt{W~`8mSGMHFVf1$1WEE!e5TOaann$XR^!y zeP3XRs?InTi$rKLS24YfiAFco#zvx$3T`VNm%Rx~wDHXRnuCSlo(FP_AG#W2X#=Va zwq3$=8T1+*p6sLm19)an{}*X*9TivfYz-zzaCawYg1fsrgh1o&?ry;yf;)uZ?(Po3 zT^o0o;P&18-uJ$l_14T70?)!T*Q#LRLn6; zyHic}-ISGm)?K{y1-*VvZBVo>L_K`J^i-Z?(D#rKR8A2X9ImvanY1xA(S#VKIyc** z^^nu6OP^XKG0bBNo$ulqpApovbLgMkx($9E=_=3F%TFVkWH`u+7jC$}`QzJ5tUZ<| zo%;YId#cq2t(-&Apogc|Srgx|5j5xf#uLu%=asllDenC~q0B*Yt6lu^@}fkO^zit& zb+OUCa&uP9FR9poqx!;zy=G)MYGtw8=Ae>RdT+chSWId2%27)YE^e|X|GZGzU}01|JMjvA_zp#f~h{NeEl4gh4O=PM&6$>6Si8NIjW6mI*__!I(YNUgU zPGgegB0TsMxg%#JNdr>?xybKO>Ip{lG^-{EIbz^N6jT8UymjOuNTxS7&}15M3t8Au z%=u!-wo!L?6iny7DECL5_M$6J;qB_o`sH`^+l|qOuNi&}H`dobO5uAZCsBTVUw0!t z@cz>{%N&JY>9o^!jLXcEo*64>!{*`s)MkE~{cFE5Lc2%x;><-y-82D9zWP(8nrM&D zr`+!Y=&Ft()(HCC3fc6*#ad&xBu-R6f2w;*B2&BUSt?&i>ZB{8lBQYRryaB5H)ofZ zqd({=;^Uj#u0oP7f7rWam5csFarCsWG^mI~dC~^8PnQ|7f6BmND;Pr|{cHYLNe4j= z=E6A1jYfE?e_a1x^}*w5p=!Zp|7GJeYSx#b+-f7%f$))hF$=EO%9cCiF{-g2gLK07 zOw2VdcNp4IzDFv`DKSBtzED{6d!tfKjA60drqrnI>#JhP~9_xK@ zb)E2zf?k2zO`S#IQTOZ3N36!nH*V$}b2FE_mxf6)^QRz<$h*~ZFYCPCzM-jCONehd zzGAtKs3o$#^3xGIm?6m2!jKy2zWp=^+|HC$Z$=!4DE1Ypy}w_r^%~=Ll$mmK(W40T zE;{52Oxbk$eu%&$M1&s`An4(sV<33ESA4rYIQaf}qb@QMAn;8E8^q4ZCVFn>-eZ;@ z`^NoPWfwHG&!p{?`P$Q?lAP`%;4#G>F5MTg{GOZkdx>n~r8<5uC;1l~lK6K+iCM+I z))Jn#4z8^2)rBkb2jA>AfV@sCF79$?ByDdW^NbixCT*FYae^c-@$#^!ISNS?O z4r2A^B1EQ;7Aa6}cu5bNVmNi>t)<(s=hCkA%Tfp(nQKS5&)UPiY=!E*8gKV-#G9 zxtf*gIt~pH8Pl`dd)|vwf2Xm(@&WC4Up}5lzu6L7p`35OI!)GDzC!(sxmvhU-Kk^S zcU}jlN$$mt$z{Kb&4IN-WrJrcZU911Xeg`Wu2e)s#PHadvqtjk|3_7hp6BFmez%$&=Vc1gRjJ3Ip4W>?YJ%wiD-#|9= z+0WM>|gL60prju0G(Xim0$7-Uz;C~mD#@V*4ctv(J(Lqb8<+6gM)DqLo`=kUtf$mv6johiC5M3Af`etYt$wFuIRZb!Lk2@bIw6$b%ajIlv4t z*>4K{`SYjQ{fZ1bG8nktW1AdKSEy`$2Dp#sP(s_IF+zUsE*dWG@Wa&(o`5%3hvzLB z6VuP@Yexp{W+4y=yUp#QQrEn2gn>RcyA>P+JnP}1xrnGJ96UU*T|SSpn3$L(CMNz` zJh4J5`5RJ4={9T5Hve5(NEJs!NeOkaQm4#U!QP&!rA5_E88=!YATY4S^VYJ1Vy(^1 z&e6%KOwX!D-O{BDHY*fVwwJ7ByLCKYwq%`sc753bHKSB!^zv{umdc#B4$krS_wOGX z!XP1;O#J$NsnvxR7~7z2(;~j`^fFCja_K5NE)kJ@j)2cG7`H&V#L{O8JtGe?Y`{vOGtu@sCfe){z3dP)=p4I6v#`JxL&35T36$jWAbUA~O6U`ImNEuUU_M6ZY>?`itX zP1ts(TXSf+D67&WArTva%CJKF{JXlkn$6eYfFNSJ!^`c@=qRu{G@Dr?klu%6p5RK_ zEQO&cJXXzys#UwLtRFvqP%&OUJwBpgVu}G1-F#Bn1`7vgVRPl9JiZXBSy5=W(OI#g zsIIPlTi)Pu4@OOr305c~CnM`ExRGs1ce2EYmIxHdCH?Xx3^1|HK~(*^Wo;kr;GiHj zo;l^J#aUoUkAw1+zQ7_NIQ12wu8nWm+wc9sa^@idx4PV{?0ytT6w1tm+dXf)u)Uod z1`Pk<;lUG!#*T`V^hd5}sOz7acVAy$8~m$Wr7xvJ(M>_hB@}FIb43SOz_S@`cKajZ zu>|$@idb_H0QNLL^3WTMASN!(?0l$H4zjw%Gt=?xAg)^;+w2K!Icr>%l9D={%Uk8) zk&bV6K2p97PvoRRxvXI*PpAKxpP$chRwfxsOvvks*alud^?d{u@#*eNRzaZ-^nQez z$=n54^h$kBcehZ~U}6$jGjv&$98m4ZvDb_zRs8%mCm}#`EAju-gEED#C!X zF}s$J&Gghs27#AiWge-st%HNZn#-aN6cm(-AT}CWPb#yKCI5oPqa1{TBLn%0?(^}f zn+&I<*MMZd#wGhuEC~?G|BR0p(bp&C_xwxx^=r6jC^CENxrf=dQ<3Wg{Dx&i<57UC zRp>5{nW#m`2>q1zCw{h)~v+on3dcVBc23_Mm-M8LDWKHobX%}@gJSK0?{ z1SnfXC#%SXJKBXT#Hu>;lUEp$0wY5EmD( z$Zj6DZEB){Te9W=PPX4c}+_t*xyneD>1is>$JD{pY(gz@V@q2bB3Im1J9M zc`(c(Cr#@Nm~erncIML^OrHY%UtYX{C?|O@V}gI*Dw8ueV%kJR9$1y09u831ypiaP z&b-raL^wDPo;nBw z;+I?+r%`AFD}4KghQp}aZ`lB968dFCfl`#7BTj+7F`LZ^#e*@_V6})(xP4;JRZ2bS zy!e-|mQL-yv7Y*XpGlyB%5(ggBQs$R8X9`KKsM$0YWd$mL_q=7pcy@HIbhD_xYY}N zcd;=EY>gJzGtFCk1fsL3B`nJctU?uVE@5He`2^^dGgk`f-{s{|qbZEv zb#--#4r0=UpmJHSN7$#WKztJd{ATaoz!VM)3^<=GAXR|0vV`cc5rA`|puRq_ww47j znrh982GA%5mgbIS<p7xklF|P!P8XVRbXL zniYvDDc`lVwJ)NY_q$rbpx+`^nqZ3Umnv2VUajB>_XRn8_7qg?8tkB$fdh2dIbuW2 zx|w*263+q=>H{g-jB(KGJMfv7R_T5qL1_cW(2X`cy;=EE8=iRxA1-;b0Gk+oJPp^@ zHMjV@0r^{QLnf;!yB5Jj*NH$;O&7HAA(uCYztHQSk!49n8n(q&4McHo=-eB7D+mXL zK8C`BO0rRwc#6jtoiZ{Dm80PyH%V?#@ZDY`?=%)XG~f-1u7~C|fqe+Ls~p{0)#91y zSUZPucqE6_>GdTDB{ZY`>p6aAPGTn0%fa-lUC$oG^-^Bbx|g1Z6=9ljp(j+}#@u<541_0O$wY486C#Vyhbt6@&kfH890kx!HXy>53nJ3(lzUW5wp@g1 zWk+KeJ3o8GvshCWBRRfYON^EcYok#dkD!>$ktm0&pi=t7_qJ$yadP#`9}z^f>k{uf zk7M$~CbPOkDEzpi?0Lx%qE$6{hJ>tCh*T;TWNsJO92cXKpMJ{WM#>LkL^IOiqW%|V>1th} zmiNFrDl0R}n%S@!sGvt%4kIQjcKN;#J^xIIOOQIM3Y^itvtE{y7-mbBw;OOUsp?kI z_LtWB=vtjF!-Sic_OgrqDaSXE-AhDDhXrfq?)THk(oVMczs8qG*PT@J$`Chf^!N6SibG>e`YXs&z1;{0t2k=5 z^7zPU!eqyTGF{?8+hiiFuGpHZncqzO?zmDD456hGb zqWPdBLsNHVIfpw14QVp|V(*GEi0}y!l5Y6(r@twG5C0geSYmDII2X4fn;wf^70>H; zl#4LE{ImcrW~=yZ4!JsVv1l z1dRH(W{6*#xZH!r@y~yDPltQ@_3>Z$8&)rbF0p}*K}j;=9v&^S@nNIrVag@t+2&Ov zB8MT&Mm^P~f>9dYc^8t-!aOGevn)IJBT)p+BtL$xnz3+-MQ&}v7-x=c?VhU_M(2!r z2gj}XtS-6fDvCuVTd*cGDcA_VZx9g*ew}@tksB0X!4ds>t2irzQhic~TPUA8{@sZF zFg8EVk0K{xp}%!OGKss*4CV;!dm6{ZS7Re&Jl>c%ozo0*^?FUBCd!x8N}b-1`0`}` zCZxy6?`EZcD8o5gX?ZvA6( z?ST{BkdUJqIrnnmaVTK$3VZhD9lkIDiw^UL+$r)0#HgrKzT_Mb5?7bC;i>C-BMW(* zOCNXf&XyrcjzUkX9#>ao@z?I!mKhziL?UF@7gc?S*M~ONb=Q6`KB#ai2*LIh^7YPj zJ1?|n`AffK^As%5(SmUgoX;6r!p^mMMGjM+?mIfBZGh1_8cDB&GA6fB{`SlKQ~!~Q z<&LO(?(Zyg?y~kw@LuL&HCV;?a*%{|#m-M8!@QwYqRGQ+QB~BtW{f`f-J8horF{@` z`<|j6qY8JB)*TeL)t)*jgBpa50hgaJ%I))`>*aQ_y}<(yIkef323F)I9BL+D6{LQIx#2=+kQ5`8T zg>wDxH{5*hCA-tx&Qv3Lh4Wr?Twj8gyIvAtWM+j+Fj71Cf-{o{hO5`y)x8$A-*B5^ z_b!Z$R3+mdHmjN$KO?e(B4Vr$z&4>@LArit5lF@=x_&y>JAB|9L2H(`AW2`$aLwF; zNYK7!8dmv``+M*`m-X$KBo+^KEY;8A9D3uPDZ`29jQ|vIG9~O8mE((DGowAHRnB<6 z`f`HTzCiC^UZ*Ad>Z?V}7c=_NcSyw4i5rYa^$uT>#Wj$jaC1+dj(RW&-OjXNC3Rl) zOu_(ad%BqLU|so}6N}=j0fe8g6Z!HPuSEciILluS_E0x7Wowvn=+nnbBV;{Q&PSPC zBX~qafWEWf`7=oa6@_y|OiY^4mvHY@p6C3zI?7r{eyS;X}0ey?sl%i~(2V{-N zgk0T!k7na2!ZD~7RP4T&&GY@85)BFox$c1>aj#C~41*$bZ~Eo_Bq8hFaY5NgAjzsf zL$HPce#p8Mkj?7;J1{E^t0H>W_VNhX?HhrjSL}tgk{IfZDQ5&`)f_v(L(B`D z4q)mbkh7pyo8G)3j6U8>AQZofg=?YEo zIGXdGLib*?#%jTw=b6_0v#7`jk%NYcwJtR%eCOy0lTfu=lp=Ngc#59y2#`}u z0CxhCWTUMeYWM+#&ddM# zL$%{w-F&;2k<-}9`keBRdEuiJj7aHEj>C3YBEvnlIG~>2ms3*0YpoV>Y`0> z)a1nfVa^kdD6wn-@?&aK^VZj?#jg4dlZzvnXrJuCupO;Sc7ZzLu+7&fwMeFvc{^l1 zmP|K411RB3X)=FjWv|($>bu*aFCDH0Pc;}tcQ<}j6P35AK<_joF^Hr5LOIVUTb;I> z>sD?|0t9aN2ZXn~d+`^~L6W1DJ#0lBMN}vfQik7d$}~wKk|dsKU0u(TZx>Zvh03*; z!*;sHmlbXvt>yuw53q4UJj}r&Bbjr8Hy5!WR*KIBeN{*dup-Zn(hyfSH&*X1)6glG zH{Y3F%_6n_;zy>Q>!3nuJ0taP+3VMZi-+@+&YGdHw~kxhc7!wg)QkmeB5Av#fh`*% zC}uwH>2OUnJ-e~BzV?6JXhX?oH%T_B`CGNgtL@U>O6LzGg8z~$6GG(BvULf2jL3n0 zlx4lTlSs~Cfh(Ilpd0Zhr_XWJx@E;~9pi@e)d__(oiEh`vKmnpLmP(9VY}xo;cz}& z*WSWM>EijfYzBqE@&y@%%#}$qz0Q%~fP{*bP?21ryy;)c;Wb|Z+~Q5}q~ohs&(2!) z4zdo({zHqQCo5$6*IREzjN(5{@l?ei{=L)`|NrxH41yZ=tbdV8&3RtT>wNbp)h!6O zG1y9zROiC%_RdYC%apxrvE1~w_e!i~H(Jy5A>pFdY0o&Ij0Dc~`cGE-^X?^;UfJUn z%YxwAM$!O=cPfAYF)>Z$jgXU41pRo5MMqbD|Mmeml>r<9KG@Dx%w2TMhd;z-uXb4W zG3V+!U>vjc7)et+;K5i~#I2?b3ReDe3yD+;zYassq6J%e2xl>R>XkE1U%-bEskb=( z(+3I+AIMQwu?_y57E#CLzke96wcJI@WK#!EF)>zk6-D)K8q(F{{aDdF4yatSD$MIo zv9OTrZcL*G`A9={VG85Ct)Mg`von2om#*anDzzcT)QpxuwzA-KUJ|qMeCuo-8`BBd zgb`0yoW6IK&}t`fRARZUF16Y_0x3^xye{J1*cI38dp++oaLO~Q&rx2!^DWGcfUXa7 zYzb{>omG!S%)OM!YadXm;!Jjt1<03=xEF%Y0%MwERWL_NvY|^4FIvWFRXDvMiG^=S zBZBZIixcP~eu+{0=GZ2SI0{j%UTBd(A7DL=ks3EpEpuPam25IIe_=d2**B6;u#`vl z(WiQc_31na3m41XjW;z>85~J?ZUh?l7Pil-=A~SN>lmWzp(mEqC?CFt)_3%|dn?0- za^}x(tVh*)l@0zOVs*vGFpA2zSr7SaNjQ6+_NRHxCBNax8YI5vgOEz(^uvYV&hSu( z*_t-R$5KA_iq6Y|D@iJIy(T_-qkUlE%Fp0Rnbd}|P1X?Cc4Yrm)oSFJCrx$qf_edS ztV-1F>Tf$(%8hR#U1KEwy-o0II+(9*Q?~cKGiQncsVj%w4`(z79PBZbSB~-?UzD>H zZ98a5OoAh`FcE)zkal-LO~)NPrFb6~5B1!|632o%Ma6pXF?<|hKdu08vQ|*u>}S3~ zt1B3%4R3oZ?0$Gk=0&spw?L%|1*G~(d_<}po)Vqcj_C{7^77QYSu5 zONMx|%&NM_oKeHk;3sSzP%;K3tl5f`KD;Z26}q7@l>9xHLJ?P7t?NV)K8%W^Uu;lN zclRvM@)I@6pd%lQ?5@yu-cERAEXwCE|6YJ0x=^ z)f-g8mvMIPTNL5J3{L*>p*mTkbesHSWIaW}bpV!XgDCW>Oy2t8OKe!;NWQb3H8sr_ z?(Tigdwp1fF`Eds#@EF#?ds4bLG+P4D$d3>k+hfY4>2U=#@CaE((fOppmwUDT9HYU z(Oog6im7kOQ$_M0d=DAS49+>@gCgx}{&pC~r&R771agvnt?FXn!ga}Vi(|l1c77pG z?rbLc%9W_3m9^ksk?M1VUY0|sIGr4vY7_bi=5)o0;MId6PGDI9V(4{w11=E zPBuWy{xF@a-I&?=E!yH$b+|SPahQ5`Ie{rSJa%%(l1pOcGsaTGFiSDJW;g5WUm8_a zJe>*KToDMDpV(NWv}9pjvFDkXaXA z%50D7YWEaef?w|BAu4g^N)Ju0d0Llwv3d5m5#u0K*qc^UGva{3u;$izjz+OHwYp|m z=-?r$QxSQxqJ2EnJ}XM?T9%wH$Gr0~3ORX%l;z0~ls33ZpZ`GUJ*ZLI!%Z zVAkst>-1UViFRKT*T2et7j@8kReCLs&maUo}HjzS4fUN z=g$l)f+=bPEngg*yFowu}vYgCmU-u%JZvE9Nxog_`C>GW#YJlSr?+4|7D#GXKn~#_I-G{4Rp{(ZXnM#Fu%*c%wPo2zoK7{hGajirxN6{Sn5U zVemDXR_sD?zo&xn8>35p#j->Uga237A{H{uNb50^Ntg_2M&iNKB8i{(PoM5MShlNc zYirky&NOnlrQ^jcEG)`s*a3P{qKt{lwK?1VM~A2^f#Ae9v|<{h-P6@UnCHJ@`D4Z_ zOHgE(694?$KBEjS*DG2f(!9Y}j$CBsnu90ypFy)34<|Ue`3xB$#|a0^hS_6lY2;Jc z%?@WJl_7>p=&!Cvdj##bg4jf+XTw0(5zVSjBWcZ0$Ht2r`_UMFi=&7XJ;=u zruC}^w=t+`;Mp`2uAhF|uDZoyb#hcOZ1;0>4q#L}I;8Y#u?c0*8LAF~lrG9_Pd-Nt ztyLeY35FlO5QR`-s5O1pOb~Kx_pgs7P$1rXs$HBnAI*hx>0)(Dc{Xh?p>t{ASRdb- z!3IzUu>xgq`(!O7JOaXvGg=T8fE617W;1uD)HJyY6r7*j@1X&3UnALpixA1y?i{a1 zp{UQ;l8&GK6sS!wpVe@rpp(I3>%xGiwmIt75a>x<7Z1HC7p(d z+HI17(Li*%Sr}N=lc~b4$1D!PK_=WH&CtZBle^5wonm;)<8o*T=h6yRUTR z>8iim+cW?Eb*7NEym>_x>1$`+-(CcMUnMe&SMB|L89>;PiTMuwXPlAOah>9Xpn8H^ zT}~wA1!)k&!ja{o{(2Kz%82+!RA6!Itbd9W2A|Y4e{ZEuN_s8X9;&y+FIob-a)Aq%xP{vCT>Mk^_T> zh4X38Q^DtzaiIb{$ULgQZ}UcQpXnZA;rA5d1@ar1$(7W&Z`-)3s{4RH$oJ07*zYrT zx>0_RNT14U%HQ__Bf(6HC>O%V^7hKYXvZCbhjd@@7E#c;=wW{@IJI~fbIyO_vhL+9 zbtZx*V7m2J)tIe1#HDRI%JfPwnYnPmGSbBRew*-)%GP-KM+cqvDTZNk#ZLG~N%y0X zBySgJw!VYftH&|5K7fpSgV;pwB*2}DNl4p4`o4o2J@8E8}!qXl8LiIFsqy8XRrmpgUkp0{mE~h_g z49;YqhZBvjv>!+s$qSKv*BH4zTRbSQkxh+8Z=wR(QJz(};ntZl)G13kZ8o46j;BdU z^-#Az_%bN_b%O@os_1u#4pxd?!{ce~ak`gLJoevbicgsXp7&qU50c9?>WyhzvRkvu z@P_T)t5dxmBRJJMdhzy13;D0DvNbd{R2lZhoAiF&9n6cx=r6o%)Vlk^-K6;SQlj{j zr~ijNq^gGi@FD;sUz;634|3fB<{*!p3s(+WlH2o%Z|3Nx{82FcTe=||n|+gg4Y9?g z^}hULy7;P9rX!7)(|!K5IdY}xZ5tjYWYP7)ui4iC^)vb(o#6pesR$EJoj!$In!2qqINb+$!>$D`bQB8>}+FCkLk z8SEDpBDHclV!cA`4(8=JO@?^b`Yw#D70E3|IWMJ?p)z+VT?p;yTSCL(Rz1G2_TQa^ zS3TWeYj5{Od3kI}HlY_i2FYeWs^9MJ?J?VLb{`wG8$u;d#kng&dcQ?mrhNgHfCa6m7?RcE|X`ueM2XIfa86?_fEgSx2^Chl@ z$N-nd+I8|`&oqXRd;5s2p4Tp_<(>j{jPT|8;&EWnsQF@@IqGo);*G-ye>FQ}=r}4Q zTW_RBzU*;#BIOahbARbdZk8EH;*&8i7zxo`f57eg=qJC{U@^LT)?)SWy>4W9mqSG%4t$7YwC)?VjD=>kq$HCIAexgpVtU|wzC|-YR zQ4`Awh}{@OGq#)6u*$bczZl*>Fn~r(tRNvFack*@hK9E0aggQxxR=79BhgR?_-3!k zFXe9-kKnx7FviyizY3q2jrZ-(x|0Om9jxbh)AQ_fFdjZO_(|jkSCKu7R;pUWo{&KU zNSoN3b6we6xf>N^6)K?&)!YbWQi+(E2QSN|egn30r^*^=I1~HvUK}ZF5Jr$mT_Md! zu?}`~lrsVX0j6_wrqRfy9Vk1{47)n-i^Bs1;}s75`V@kbhw+osgoC|gQDQSz-O<>& zg>HyoX61Y!9wz#rg_o9rU1Pqr!^dOB!%D0>u|V?YxHD-J;{Szp5Wc%b1{@j}7XC}W zn|FGaqu`R%JN8Hiz~`i$1X2U0wvD^7+(twvUCn_w=g}c`KjbH=c{{gL6oKrLXhKrs;UvfSV-8rggPdKCP>@>XD+i;2I|kiqAEggKci&pZB4dobZrH5ru@!Jy$M}6 znqP@!p6zD;X=&Q&>BDc~*4f>E5{v2g1cA_SlP2i%^~Ch3JD>Vu*sJ_$$c+=dr=sq0 z^|`L>-tjGKneRLD7mT#`DwNcVHGB4Gvva?gDn1>_uU-xpWuZxKoFd;!LRXBh$p zr=?zxXN+A&({4Xl!lo5%jh$~J8Lmd+hen_e1q0_ZOi6(QCj9k%x5dx>LkmKc%(%l`KHAR5}mK>+H@us)%guKt1`td-%F;aSoe{#zn2sB21eir>g zLxX=5oI~?V(BvA0BEa3-$MM z2QXHv^D)_VkvgL?$dhiLDqNbnw&~DaQo!15)_zk5g2b}<2;~yYK0@d!!=}|5bvhuH zj7&3v&PCGZiE7G2wzH1lD#HQ@52)i1|KqFvpI)xnt7ZDvoXx!li_n9q!OT8rQ@#uTGXCcjc@pfvh zG1ep>?uPz%mHxIz{K%x^1w6hwLxvF@VJyLDGtiisp_|fe_EoZZTnoSH{u;nRt&+nBx}#h>iK{zuFejI_bp>_^J;@s9Lnh3 z3|jWw>dR-B>?K~0w^`i@eLGV!l%g>T5tFS;-sdT|N?Wme7Gl!ZLv>}zx5Sa8D|rSsf=u~6l2R2W1AlI83`uSNYs=J?RU`^|1z0xm+<=;iMr(*@;F zW$+Q6a)v8ms4ArnW7(E_UuatUf$pPOXy&pgmpz49#wi|8kWZ*Od>Cd5lr2rjk#8S( z*H>jz+RRuQoe>y)&4Ic#4aSahY7~|8#};d8NC2_$+{G)D5IV=PtCM9-a^P1j{%yuu zx9P!Q=5j7=)bXoNfr^urphG0M^Z_?iF|s9GTb7ENA5v5bqVKmX*X1~~Sig?cLLOs0 zTsW}=&}%csWG^=+hFm~7RJy9DwW1=-U~Wllj(c_KuP}#1x9=6}f1@~VnFF8Hk?^eF zym&&Yw-7H*9&ZFM@D9D*gF#lr2^aS_@s{1`4iB z&0Gk07v)BhmA8Xw4PcQ8GrPJosD30O#OKDEBB8;D*H!DI-4V!kGXLT*680$bl7NVx zJrq<>?yW+$K+EndMw56Cg^++gV{GS65`V89`n<)IUuS7T+TA9OtT&}J?@~;MjO&Ly z-wvmaj1gOgrf_hX*dH2()uTTB^@OC-2>uh=x(Z%xAScgRA1GV|dR{ls8*5j3vhit$ zmY(Pr3a|yDHT~fDg1dLCcGw#7i)NhsAoJs^$@gdmdJIZvPs9N#rLy*4qlF%8SavC$ zkpMRtK6rayaN!)NTbGjGN6HhPKcjvhiXkWW!g-9Iv$AYhVGp*+9UdNb1?zcuaGTZv zZJGXXoDJdKWYp=$eK3JerfNkHZ?~)DzG7%89&&Ue`}~-;i**vt42_EMTT)NyqfVvZ zh;6?Ld`(;pl=D2=F9^Kqm5+Wzt)zSM>$#nG59=$e`>t(SKrT0C>XczBWDClIAWfUX zIgZ0pGN>w*i;}=d+^;zScO;7S|Dqkv9fkb~=nl@owyz9U{(ys3Roq*DNU1Pd0)OpKB%*rldL&$T`w@SM24sm2sx|E( zAP4^Z`LlV$3g|-gAHi3Sf6gnDE?-6QB5e#KXQOQ+judd=J`W%Zr$TX zC^}ryM`J`Y1?AraJyALR)3un}I>3(sX;vgx>a?MwPPOWW?9YFVj{8r^zMw`f;~onjuZ$&EKgMKoxAn$~G|EA_2o6}-ht`?{mfb?8J_p*z;A9p@m!Lm2N0f8Ao$$K2J zap#DmyNv}Wx%(PvVQx3>-`iIsoBZqm@JL>;@8m&Z-Lm{Q(l}c3T1+0J?696#wZP|Z2(v~fnHDF=)wv*L4Q_T%`e^vl) zTh!T<+)r?vqs^fPhMK8lt{Mh1F&t^Dy-_gHb8`P$(omij3>KiUs5(M-NV&YUmxv*h z2DpN0X=%2v4w3GW?rrJRJ4j^v*(dsi!T`kZPj;Y?#Urk+j!P~Pb(!va9V6Ah z+9B2so?JC#=(NEJkBQlj;XNm1WlhM;B)mIYl?0+(DH$2Z3x1NUps*${-~h|v9Mq;j z#ksn_KLF^<1=ZE4#rS6l+1Y^1M%T$sT#{`2OqXo^{2-dyZJB93#uv54a+s(akSA!~ zw#E=Tq1oNvG^R$nH|bi%UJW*T<4ITP2|_?Z0`+HBvkB)`1jF+MfOp^9+iTB7 z2(U%50nYUinTt6@Te*TY$701PpTuAlobAl=Phdas_}G z+f#Q?UCjj8w`2_AXHL$hr{}}@G92KZz~xY(eZuDEUq1W~!NJk!ePNr#MyC(A&l8*A z%OzxPZtlFBfuy)N8X@5rir|Zq&rWf%-^xTD90eCwDu6*j=(FJiROdTm2cGuL&CTOV zKu&y(yg|8%q>#xqZ7!QzTZ;~m@`V60?X5XJz<$gk1PIEvNKUnMMh%)kfCbR;0iOA8 zHp-T0q6{U#W(P(lEe)sBsv^&^0Fc!uviT#&P0M^M|Lbc3giX~imm{NMmVR*CmVF1Q zftIG{75Eh|;FHBX1g_b52L}fjI5+@ywdN0a%7TJ|+fH7MFe-oh*7Hj^uvCj!WYxVMnrzhZ7OxtJ;c%+8 z%T3#JY4+JCEurrLD#PEC#n{;*MWZG)`Of^=BPRT^LJUR*hC;w}fWaoo>?PeWcxVC^ z1JKSZh^qk|CBPB|AZTUa53p@feLZFci1s+gb#odDK&9c}O43{TDJX(!S!x7|Xw$|` z0X!`PfHrT3F?yUb^L_xfsAn}E0_b~=-`oMI(|}<~_Kh@9Vg?xa{{>`x7r*xesQ2Vd zOsEMXD*Rw+z`rWFob?&jg#DNCom}sY{u?zwz9*ALFH+3;&w@upzyqx4W`IFo8hQnP zjzdfw1IQqfl9CRcTQ&2xfTbG+*dd3jO^h-H@c`wPL9bF%nURq()*liEgtCKGF%$y! zeLe#;&p#6qzJtfz04~Dz_BJ4Q1jHsvAdmz=-~yIiYE5cMMjre(Z{D(I+4^?kIxsLW zqy1iFZ3(0&8R0gW%@2gf6}qAQI=0w1jr|1u(LqR8gGTMl69jMrDpU;UfH~;6fmN;8 ztHZ)zfu`v@Gg!Smw0aRubfdr*xwqolY|CDqe*u6>GXLgb0B!{C2k`z;0ZWJ_<_Fx( zHee(SI(f}?*9Go4SXhrQurzuRZjxKk%s4vg6?2&$R)9)VN=}Xf*rQ1@13((r0_X@5 zsxWlaZvhXb_nq0!rd`YY}x$RP|zoK0%SlF~@^qIi*Ck3Amiv{ohbBK#8 z=Z^p0xfsP@Z(;KfV{A78#1R?i4A{(ec)%SKC`#t7ro!Btw1L2%kfH6TSHk~y7SnQXM){BE?DFUFfDi3vg7BhMyAhxt zIaB>7dlABn;EDEBg<#Fr5IW9>lf&f<$zSy(#UYf$zw^Qd6Z|;}Y)Qq>SeM2cN~BI< zV(m;64ZU4+y!TFi*+LnOg;H(m5Nz6p;^#2^%rKK$}7Z_?c$`l94+M07oYf03Sf z;vdF9Cu|ObAH83=3ecD@7m-$nzfmwTn=nxVb)Gm^+`e%I!#(Z0^YM_k9b8^#%$@k( z%qE3#4lOPwhvxY2?JUuc&zIfrG-M`P?(2+m!n@xzR!QQvUQLNyD`j!!<9uz8504-HB=TpsG+AUa zPA+Rb5YnnC_iex%eVRN}Jdd4tKajI2&WB%8g%CWnRTKM@%a?}PLDSK#C}CBqn^&uy zfJ~D#VAGMtkcz*r^x%M%qc+G$(y?ZK*_Nj!T0Up*cZUluWCJ_L@~ts3&}=^A z3)*lMRY~s&txt|MvRaa}&^E2BVObGxX-R9pFKd%Vqzgh5?k;wk`kX=L&@%rY^ z5%jYfcH-HQ>%X}G&MT#rD5|U}wXo8e!^QTHco#n1HGjC`hXye=S9)8;%%u{&Hk1k| z+2|5cvl>g2zc+3+xZ*`3VoTfBw@t0K(5?&2hvPgYsMt>Gj8Q@JM*9GWpE@>MU%IeZVwR*dLWK8t*_#08ATK!gh9G#(E1?w!>E-<)pm_8hwm5AyRXL=a*Ml0$kF}po0{3akmq`JQAfER-jDY)`5hB>_}8(Jqp$WZUfqsc{ln{I1Zg>Yi?vp4>cMQ2(Jt(|CUizxJT>eQ>;NgWV9Z`#|Ulv)QewAtN0QX|Jq- za7Z2oi(vEup2R?SNQ+4*4|QTFL3<6-9xNFzE{01lOt@Q+u<~jOmL&3z>bjvMPCbWF zaXi%vExBg>eqeqW%BIgza!WE-_5l6isRt3IEVnwCpo&S}wR|vM}SjV zOUhu{A^yENLKUTadkbjt+Lft<_Ntw`*X9b2vtdbVtLAtW^;&7CU0uy1d)+MLY_^`C zZRbN#2~2t=$fjr-79KHDyt>AsM1|P+psznhKY8Ol{Nu@qR9r}o6?!GMx{E1X@AYeP zyrX?Gu98-TFdXmt`eYtT*VxS+KH>X%Zvw$x(-B1{s?WED8n%Tl2SFpov_EH&Io+e9 zd3VzM%667cu{_}nVsLqri2$Xpky?&w|*I$c<2GHc@=xzSXHVa!Ss7 z@Erp?`#wF#4pr9fO$TMJ(19}KX0`<(&1qb}(|7md`sl^-v2)@2(mpW(X>{xtP2QQ} zvNEgd#nhm%L*)>tbB|pK#N{Mhhz_q-j@MpQS~b?uxbPlhiF5oTPhvdMgAnmPqEc4V zF?~P`j7Y$NNJ2saMAy`c*}H&D^K`v~AY=T0)pk{3QAS%E5EPISkWOjo8XA=Dkdp3h z$pHieBt?*}8I&#wK`H4DX#}LD85)L;vpqNe^?z~ZYG$5iKi{rz@3r3bu6ISu?YR-{wqPIedjq1-xr{Ap;Pbrj%qy~L(?0n6Y;Y*tr_<)Qoz2^~I6Khfy&-+7rnx}N5 zrYM1dV71@#mi8%x#2NeW2IR1qLIDEJpr!MDYMSDW4Usf1DQM)X!3bSp=oFS zQ0NK*p<->#n8c#N%*v|hguG5m6lhUX>HIVQRN-l)aB8Bw18oC^Qq| zDcy9{!p9n;IJ&yg(a|CY$`!vsf_G0GWn}3O$(d-B<3;n{oTX(iPkIYz8Z7vR;gXO} z+D|-wr^7K2{mrZmb(3IlUjn_gt?gf*rnx}8E_(rx+b#z(3L`fbxN9NG9>J9D7Z-E$ z-cB0&xwIbE#~qx$NH5MSjCf%*hd{5-6WgN*Bp`nK6c2%vH7`+FX@4cWw~+w!I<(Oc1gMDy=bF$>Oo^G->s~hvu8XefxZ8#xe+bH9|~Pv zK@3H+pl60feBp!;A7&W_O7-+eKKEVKX9~E-<(;Nu<<0#EM}H%l*ebyl_uZ}L{A4I6 z6#`ZBg8$pwM0B`6fBqarh;8zQ@X70umVE6gP`AF4adRI`%pgO>(N+vR#C2_X%N#UJ zVJu;yFh=ibx1?s?Mh6er_K8smUPhd9UmOSpcPGll6D>Wjy4K=p&R%r#U3-4 zmo6iGwx?_G3d5W!%DMSfH1wT=5n^eK}l3_eFH)qYJY zIWjXNE3e?Y&(G3z>2H)3pc5i6f1$6huK)szk!Wj^CE9j_&IpsOsi~>8jSWY55y9KI zw6u@;82G1Nf>$YvlN&T1c=<>2E%dU9Nl7IoC1X19!S!bVY3TQRP5R2jL`_~kVm^*b z_R;J*lzyQUoWTtLHdO+s=2Nc6o)di6Q26FsO$}%%;-=ZXv7xW4D`#p-!^Xx2cdQZzC9_e46}KCW}+g_ig}%Vgfi9C7J^yS>c9lrz`XkF~x=enWL*eygTN zMzNhMpTB)ejF-A212H%tJ7Cx>1i0dd>uF}+0HeROw6wUShSf z*(#He1&|uMFONM$8A z0OtN>rVny8eX!_^0JujuP#uG*gJgaY=rQB<`N1Yo835YQp{1qIsqh|grIk&M`MoEQ z&gQ10Lpl7+Kw#ha$!!bYMVRH^;5|HeK|UvVJ!?=wh=`4yJr59XK+DpyG7nK$y>SII z%LZs%8?S(YnN9l)cT+Z7oduE=_xG!rrCRp^cy?(|aC~6<6 zDDEhRV+F)Zs$O=c=zahG-R_=Kfkqm0%mbX#FOn3-Am}nqX>0I+3YiWWOnT8Cs|V=A z`0xJwxr$U3prJaNA5nD21lQH!7K+2DA*-S}_%Xdjg!+aH3zt58%YT7_4i`Ukr&0*D z!I%q>mzsK(3E_ES({2W7JIDwZ<={K6Mi&ka093@sYqa9xqsVC;9vwNnNlD+LYz(+f z^)5Mg*1kIN9dqGa8s}m7qF=GxQpOMzxY9%|-bA0lMgBuG+Gb29Hu}WHuYKTMU5*W+ zZb3{jtKIx_V`HPyCR?MPQ|A^dy@zqYEeEqa`n&HTr`(X5Tc<&&hK-KA@!&xB3f1d- zivDhw9JgSTG(%@krtFK$#~FeUV(qy1LlQ=y1S6U;{Sk-Z8)~Z|5$CtI_#2d@{7#Jd z&9`W@)QAhnXQ?341roZidrbA^+s?snxmo6yy>z_P`~+TA*-2zgl(p~W=l*=$AwxP) z`;y7iQSicyHqbhd1($a57 z^>G%ma_GhXELEw($r*4_`+lftM*TQ7jtW0xpaVg|TXx9wih-H0hb3|^_>wZ?%Y=al z_W07<6d2TH_95$so7r2AIK|ZhWI(mRgwYgjYwTimmn-RS%f}?7NXjKMaphcJEcJEt z0NnyTnv8!#(k_QD)VN=x%sCwhEL^t|C4ZZ@}eBI%Z^^?v45HMV9SyqC(# zm$Y77)NaQ$mhTkBrQgy4tr?rWu*cC@@V@W*arvhQmEZ0BQdPJU}Nn{<$SnnMZI=*SB`;k6uZs4_B1FDhRyVbMS7 z3u8~Gf!_HO?N9khVvb7&X+1msuo3N3TWt4cP5xNQF)qh}hiLwEV z);LafQI$Gl39IjxzU=9N#N}ocn@6Dy^R3Q|!rZls^;M)stgP-9-fVvU2T^r(_=18z zDsn!}segZ<+G0!GR={rsc?W>lAMcfn6_RF&#L^7TE4m!|tv7}Aq3~)-Z zho4EEo>f!^*=<*x?I%usTiJ<%c+ORmkZ8IWK1OgbMU+ge^4^{>EwaIeV-R7PFk_d~ zgFoM1DH~5E2Y%0p$77cbpX(Q&x|rZH)_aU0mA#FR`MjV@0=|;RfLTeQ#QHR!beizx zAjL@hpcC|5 z;IU3DYAld@l;4M_>0U2hdm`c>Cg1swPj?nzdZ`6l=q$(ai9z=G+mUCETnd%bIk$_M z>s-rQbtHxA*#oB>q`hH9e9Udq8+919r+E;CZk~LB4CF}ao6F+@)tuy>$O1i9so{@m z0c==tZP~%$JA;*|Zv3`p7_r|ktr9*4k~UX2bLXoxRvF&D{2qxw&xpk4(J*R{DWU(Y z&6AONX}nrctZ3D1r@(7coqWA-pxDu;d1GWWFs|dXKTL1mgRM7AtKQ!dKXZPIu`^Zw zbHae?PTk_~*K>Y$LgR--wsk4l!K9x*n)G#e^gy^Pw#W^qr+=8p8!GRd?d3Q0xUWi_ zADytvR0qOTH^xq;vswaGDJTeE-dq?A!QKkx$*Yg*M3`3g+6&~CIwq~e40~H1*!oy! zMQIog{AIIM4{S++HE{Zh8l1#y^Lml6(;5!=y;1xn{PGOG$C!q1w<2*~y<5tQL;l(P z@*EUWAu#~O%Z}MD9q2i3lp2Y*k9d) z56{g}>gsaeyu_cH`_m+di=Vvnlx6Jwq3l}`@y1u6oB!uSqel_s%3yOYc@JKAe)Tp& z3N1`+kUm3w7Cy>>cuk>kZ-M3Z^8|PlK##8&KAQE%7)=)Qi_+DX=9_iob-h23Avhj~ zBKIPTzov*WHokIm^28#bmu;fHXKC;g`5C6mZnhcOL1hZ{eCq0};%glB zaVyS+3p~cYZlIJ;kL#XaDq0PlPS~EOY#)kimV@H#L+)9?e%b?>euRN>OY*s}uy6v~ zWyu@)d`#%8?Nm75nGNyyXdWqK+ctFxN+i|v{8P9}DC@AK@+XGG{N8=KUH9kE&4s6> zgmtG_BxL6v%aiu06-K|P4%}vZ%g5F;#+1qpV6vli!JGEv>A95~n}craS7mm*R<9Vd zg=IlU-^M)08qBeq!j)Q&DdA5Sd*f2}k|K0WOO$S%&r*+@?F2R-VeCxXzu3RT! zs=d~^8cxLJVMEzvC((J8umg>DVEl>A6~5oV+XDW|pI^JrBk-HTKexS9Gvy~!?TUj% zw7ahaXwTahI~n_1v4^uQHD-#Td%X691eiLg*-uA zG7tzRUc|PEh<|*2z+ZXERJGiQ(&B&lga*2cWhy56A^*~&b`#q@a!~289z6nXBAv3O zVs+&9Iz!ciqmk+3%`c6UKCAe{Q#%MD=?ML|Zj$PxY6rnGeW?xx?xMd^V$taLB&qE% zjYYQLXAC<|;$GIog$atewV!9_}d7U z-DiHRp&sP)e=?zA&}fY!cF^>Waz8L7(J5#+`-_B0NsG(yxA(#j&dmtQ)D!p}vul&` zz7j%Nf`)FV*e@6RE-n5fg3z}=BT7?8XLrU0MIBpP(tK{ZXimS1Ki)ZjgNQPMTQe^@ zo`dl^{AB39=ZQ&Gyom@hlJ6;bPSrLoR2b>P+O)-EP;iHiZ|^{!y|(d`2ogv@pz0-y zK_00l8>iDQmiVPGX^li$?pPkabb&>#2EYAG7y#wyZh|EFE9b5nOG-+Xn>3|*to8_A zx3rs^gA`}ufW7Yv#x=G$akhR?<`OBC^^(98O4Xrab&7(Q0_^_wsAl&y=&UPcZrEg! z&mTG*RZMiE<`PQK$8^YSTU)R3c}Y9pYRMZ=+y}T5DB&*-{+ebY*z=4zmAM4>)%0xN zFe|fee#G|j*ZC0qm*~oqG{knDs6|7lRT8P+TW-abnK{A$Vm+pubzkv=PC5z$J=FNwW*&)j#QmZz0E%U$lx$l*#P}{|6bXt zg9Ed7n;jD52A@UWIQ2A#57P+p6vWi+#DL2eSfGUAqolc zKa%Z1?;$v8qX>Ff93;tF&5TDC|4ed=@zJ-5+1r<**L+ESyLMRUcGh)n56`^D)ql|i z-N>Rnw5hpf#mRzhd`-ljthTsFQd@x|%kQo=NyKZ5Qyx|)9Zj>$__3ywpD*;&)ion+ zgcExEW5~NSLbdM)It>g|x#|*x$8)sJV2&PK=DjxD%-4$a>U3iKq&Z3!#kD7xPI!!HA>dEe|D% z*u^Dzadp4|(_?taqxcs(xxET+j&GN${E<6Xt-g5t&Vd}v+5*!YnV?+iEsNT_yX9J^ znUFvI!U-BfH%^$6nesk)XxR#_&m>qsiL4G&&0gRzwl!V8%6x9URAHQJ%R)H+hfh=0 z^qumq+*506Pqc39&8(>A;?Vh@dZ}F|s(SF~nInthVw@PNh_A((9eLzUK6`&pMx-`> z8@VwF3)kzve}r;5OLg9q9RB6S3p9OMP~}V&VsFBB;wPM@PL6`J9D}t&C6&6)tKSUG zg^QsbeYE-8Cp|88QO(~AWrQW25Jj;=h^MoOKTHq^+C(8RI?uz?HIrZEj{d0*=4=Wkd-E<<{3=?HnEFyP#fvVai*v z(qNs_kSKgV!?sIGnl!g#TAZ{y@`J$h8k;5yo0!&og7R|DbpP&|h)4!dApl&4D#L); z+ec@?RY$=U)=1BTngj}L$=e4xefqNnJ}nI?;R=w$yH~{`XZk_sa@Pt5Brcm7oegEx zj1md5yR!&?7;%y7S~m=Kh}a$YCRbTtBYfU+HxpVxVM?_Of5hcW*-){p?rA|n>fwR=9*3(%s2qw5#p1aLdI}6K|ct-5Vx`-BBx$xU~%mJhaahgVA2bm9FKphU493zkZ&CahQ{zaOnPx? zH@}DV_R7HMh%@Gb29}XL!rPw|fYsJP4Ck6S8yR1$#gf2f5m%+_^g6dNV0p4#*EwYV zUVqcCr6}f|-#nMAGW<`vl~_)_5_9CUAm+TQ5F`sXtep;Vjqy50-Tm1;E3i`CIW{TP zY$gFNWMUGR%gP!PnwF-|GX3pf>%*CAEEo9z`ir88eteZvN3OKTKofC}6CZ~pSE4bH zm8fn`4dAW%Pr1VhawGedyy(F=6~S=AM`b~;>BE@;qzMN13FV12=9g$NvC^1oj9@aP z?f+R>1kyynUiu$5Y5cgD^TeYN@97uQ%sz>^oV7hSvt?v6yMcHwZ3z8IrEUAo@()sC z;{NhSMA5k0n%<3Sva;w89^r{!-zF` zoy&?d+z6^x;#;-5Z4hPMV>xX~465X`)h;c9Qkozf*`DoSQS8FW&YH-kLKuZNt{D%Z z5clKx^y$;DyUXjkX4DVz3^BRO2*jL&H=raXpge)8Z*OBqNvgY9>WsVGaPhv=fWJ+1 z$st~KM%2b&nFX^B{#tF?mCi;i*~#>4CW{6As@zNuY4WQ$#Rx*K@T5xdMk+BpEyZK1 zSy^<+$ItHc436u~K*-p)OY$+B{$h0_`|i!8J7H#Nr?tslnwCcTPXUgrcY2x>(53+< z4=7SQJ3BQ8?;|2C`7(gcTejp|3Yfa@nIKsCZp-#>ZXj2YW{ay zeeLunMtea^Onhn-l9`!FQxUV}nasny!21r26c#``VB3}5FH3bD?%g^cHjNbW^frIkr|xWk+W z@XrAh4s;v;QDXqi30611uyDh9TIc0U)Q4fHHM1Y49GL)zC$h7Bd!mR9sE3Y>!~?Vs zAef?_pX8)$1Sn9qI|vt)ZTfT{`#E3Va;Q7cHGDd_-xSnaI3)?bU(D&pUm7`~B*dH& zx*hUmo8`HD2>;{%s3un@FmyP1@A=wtYA4m;ci-bTUetMGQNTfRt<%=k^)?mK)75<{ zC@2VOVyy1%#r0c*0(WR>gFu2w1Oy*;wKRbgAK;|1kjR`85z^%b%wj7+y(ww)Yc29< z2>Z*;mX~d?zS`!C+P28un-SSEuW6vD`R`2K(ux$}fut!Wlfb!o{ z(Y`1G>I4-5?qI|{6|c%J9J?(6#1{e~#WNjyZwM@2$HdMG6+s)U4eD-!-2y^jLl(P)}$ zgujp-UrH(8hd*BTje_CpXHH`3PH${YoLu!CjFC)jY^{wM9St3fjcpvwY@K%RGz-Ct zs1PrD0O=lKORtQ~yPr_4vbm8al}+vis=9cqTr+-(~PnkUMXQ4s5!+Ijzom?j-WN?yRLb z3`QX%BjY_re)0JI6ovH;`h9=W`;0O7N#QSlF;S!^@B;?2+rjYF+b2&%;49=$!ANBA zDs@rh$M97P8JQpAJvdKpiNdFR5B~qYRgyjvHMzr|^MVIEUFM)xc)au<=Z6ap*J9)s zX5FGl<`1<$9Ck0BBw~4m_3JhD*3WgmTqbNfH+hsJG7Pp_-J<2MIu2I&d9X+;PYsmF!pMqdc+rQyhHx|taot2p+9PeJx%|8 z*2kR^pY97o!jLNy;=?B1cT0QR8hch|Dz{I22FvxkO``dQMM>%{t**^*%5mBkIZioe zz1V7=g@^nym^l00XiE{rc|*J>HZtvQ8Mxe^dMmRuZpOp@rB44Qoc7IrPx%AH-5I=!N%R}%^_7rO=`b*z4lm>Tr-x^ zr!96U-SNv8_p%Yq5KKw>NDtm*Hh~Anu9tKCbYyxO17A6J*}04`ZGCvceih~jOj7&D zyJAhV74fZDucgOyu&hS0q7$%&vfA3dvMlre*tLecH%TL5phqCx-X7SlqTIxMzuE}5 zJ6YIvvHu;hoy+s|nKr1e0bsGPmDDoL<8AG?uNnb5uL26d(@6OWKWPTO5C-_I7 z#uSk`45&5SjG4=LuF%i=T99TQ-_@p5U7)!W7e=jvB5&wbeRDVQ16Q${_OgN%T@AX? zrtENGGQG)zZ)|O;wUDFu zu)eQ^X?UeQ?y(fUd?qS@($swrIIJMnJn%*@<-2rcsc2sa)BTsd8DvhQt_;3I0sYK_ z2Ct?t)DIWz1dJxE5=PS9S!L&iSArO>hzKTDFMVU@ z(KU9{@~sBv{0mW}S_vN4^ww#Kw(s$@zjSB%xIdjZ>ilhsT)XLsU3UsNkO;&c16mS^>fScRWz=KN8Ehx5nt8t+MVOxm$e>Ut(^=s(5y#c5ceBu+6=;KdENAalFjqm7O6w z^)r6Ap{+rlD=Te};VQKt2U{Sib8>qvb!?4wciZfbAxVzJwW?6<_PnC)*NvN#yE6Ob z7GGKgi{~u}?bqmU$~f${$29uRtI9s@T1FjJh*jV>g?gGqo9`hRCU>pbX9^tt5h?iR zgB7-Z;_t_Do83S|2}xmKKfNha@+*~9WHh_5ZWbY_A?|C{+Q?jt3rGHb4VTpPRzrQ% zV-lJYuab=WvdJuDSI4FFD=d0V?u64F`z^!xLeukY{V7QgVf zv|@}!3ZLIeW4MfzcX_p|CQM=0JEeU(SNuo+O#*-EQ;SOc{&3&Y8YjXmzO#(9BN|ho z=|Be$<2(t)sGijuueL;8lJ6Pmp9_00&K9SxmdWo|(vsVBGni~N8J?+G&acp3vUjuw z>F++vV@P%o?zPZTChRQJcv8t+?nm=(8f|MtAwRR>ud@x?1C5NTAtf6IF0&dO-~K`@ z`z(=9lZsbs1RBX_2F^Hwx~CX7XKRGwC^WL%l5;;>G1m5nd^}rwxDN)B57lF)QoB5e zt*nQuLcMR?gIwV%7c+gnJ-QTeYUO;yJl8%E9^=+$j7!oa6yd(v`;0S>83nzD}?RWqL@DWbSd>r@#*tv#dtY#K9~HfbECzBgkexZs^aJez?~s+dgW zn7ySCb@3V7AG#3CuEzNTjc9MOjfMLe8J$lZ@o>K_y!8KUgK?gLWXAozS!jYFW@wnm zlZApIV#zej7#FK?wd}149;%4rK3+FAQ;j74=ddydg@O@3Oiu!_N=j?I15u3Pt5Zrn zrl~t)fJ@*7(sI&*lqD`n2g@3z@sI+>N+!mBBbBlKQ zC&P19+>S^TBl4Sep95X*%P1^FZ`7Te)&N*& z7KLAgOcp0yT3%jHA8^l4hN^8~;8C73Rx1KB4w+Ftj+0hdD{Jz;V0-%mDO-uQGn~k9 zbF?TdF3clOnNC_-I?4J50X9{#?nc&06K_vdoBsKxSg28s5+>djBQGT7;lYyorR8&mibN+a-Qy(#uB|#n~85|z|_51h8XY3}C zadAK4dx|tz|90m{8g=EBm9h5s_hql_@4k4vqaizs^w!eSQpp&LghXCi8r8?gr@g)X zyZw?l%0vA3N_ohn&!4{|;&=H~ZYp18J13{DO}MbIKociTC704Q+vqt_=S;uU86iOz zM;6~Z(d;X1Z*N~=KKvn$Ud>@=nioFg&mR(%$H)t5CY6|yuROJ}v9X7$thJUlGcrg& ze)`lsFz{8c*{5s8fdVt6^dTiZeed`iI!?|wKR=|&3iE*Sa(2`}|95xpeHq&v+B_Pc zn7CzkyruEaL(tvbJ(P$aM@wr;>$s|{y1IEdg89BPrr`B^&o~un((2(Qy64YtF*7s2 zeED+k!}X%2myZ?s-^K# z!xSNJd<+Z>VvqIbzkmPs>&Y)p%D?xxNVWIFLjt$cg`j6FZ;>P-h*uVWe(G3(>!+Zi zVzV4&U}I;ul$C#+6urJrJ#9&G>((ulnsQoeG3-cE4w)mRGSUiV18FTFB@5yIJx4J= zpdNzN%$KB4s&nyqOhDkAx~VP2E8#LwN!2^NuJ4jY7N0D>2>Y{6UfGr^ZIXH(zC1rlyjA|5p0)8aYlCbd+&?mkvah$o`iz~hhp7Ei+v&F1((2!$~6NyF7A(s8YN}CPKVF!C;B0R^0*6!BPrRd(d`kl(8a^qZN5E`>K3Q%-|>!XzP5j^ zxMPzGaBohh_@sL|8ZHJOnDxF+W@MHfMOj>0Y72fsetOW$?7rK0Lc_pdH2z&5NnpSI zsdj@~lq8I+Gt-_wnH`> zFI!^6Lg|o8tb6w!5oTm$Ao(?Vp8U>|z_7Bix{r>&e6ibnZ@wdpU~94x4LVEA zc5iR*+0mGu`)Z;okKMfA{#AacQb*NJIo;SGsw6zEM#4(Gy4C-P^kq#yvZ~+MkSti(78+^J52J!%O;u zt&VhcUSmE2)4s)0l6c1~^){_n9}eSp1QRcpPO?|mI9+izv41rYxKxEB9#N_snsSxz zH)R*(djcY!&A&_rRJO|;l-ztoA0&P z`N{6!&ri7SCo`_JjEs~N6xp*4y(7?UvhQHikZR)5kkfkvGdTG~=xZd&Wm(B(khtDr}Of*x3Y?4|?rOG}2@s_YpLT2u2;$-=p~1hAiU zZ^>@*Otl6`ywGPt(zK>+zC6bE@D_{N@$K(8i;cw^tt*so=jjOdXp%a76MyWZlgP1P zD{vN%VfWp3_HPdr8fk59ZB6~n#M{-at-g#G0bPTGoj-s6q+o$^!IhVnFXxz^nd$Cj zXH2WMog;=O3rnX_X@Ol@TKezr$X?fUWFi_7At4^S33~5m#-(K2rp$^z4MX`~zO+)L z=Id|V%`Cn2{`GiEg^N(Owuiq>JgG9IfuXKk8u9Jb}0{@```c*WT#s{U6V z9b!0MoMBf%T=twt0`|C%qGB%A`scm}nM`B9pb$L7#ElNO+xqk9Bgf|meY1zMI3nRh z{Ni&>-iihE@&~u>pukWCy@XdvN~)v7-qh6e!y_VT{mhM2Kj|EU#r6bv-XtU> zunU7^s9upp%`Yzc%jMP=HN?fnzO}WDP|9luzyt4$S#fc3SwHgV>Fs@hj?NLjrKPC& z?qw#@JWP@IK|yzZRa&Y+`Hs#<$B|AKR|kd7GtYSLpZ2WC=+;iFr;cC-1b?mZ+f{1} zfxqN$*kjo3;~I%iNG98_UlAm8FxPUkjqh#ddYvE1G+kU{H(efUlsPJe_3nNkRZ>;$ zYo%n~TbguDx;l%cvJ@Tc>Uuj>W!-T8w?HyS39$y%Hbe@%McaDU2e>foMKv^@0T{~4 z&PK+?#r5*`Mzqe*88=IpJVCcTQkbd@hXee0=qSb7wXHDK_YMy&?Pg(`UmR~QZCB4x zF*6TZb!~5sl{lUq7z>@w-%(Oh>gwteDN>DzjQrs^DA?Jb%so+ViUBibf#l}&9wjBE z>mP6R$2K`0dpxPSyjLy%IO#PwjXy}WdAyEvzhblCmk@Ge&mVAxyBbo7<<>90b}Aq2Gi+?(v~rC!_goG**VHVH*i=xa>(WxMd( zYK5WDOaEXQ?tL>TP}n;O`mpETXTzbnO|+ZK%IIdxFI;$i?*VSa;>J#pw6WS+odz#$ zY#cVX{!Sk+zx&?7mRL>w)kIaT_X);jm9=8b+Rw52rzOn|(a^_jeONGM?O(*KWm@pK zf*_gsAmmJh&iisl^_mAqR@}rPb#r+&$DZ{}`RVH4Q45r)g1oh}f18Q@(eVRB}KV8r@j6(v%M$5#6EQU+qO@oC>NJLcYe)v_a?FC>|=s~uY1c@>qva-lArB>c^qXfKb zXc*;n4<)^Sf)XbUnj{%Dbtl~m(bdJp1LEm39zk4YU87$aFOX4C%DgXJnDmqGyxCSu;dM}}Q*OK&SDC)c%7jY}W& zkU%_wSaAJMUd;YrN5==)C**O`VZJB>-ZfWqDoEc%W(YHBWDV#s))C54Ci8`@;SfEQ~-K$*DpDlAmAPb6$1mhudi=e zSs7xYP*A+jd=>P&)KG%YY28meD>LJVlblVhwp_E%)jBNQ^XJdko%n2~YhJ`N>DbxZ zm#*^{E6us$0%($w3K`k?zN^BPxD`I&OF%&IukWaQg8LnnG*gsRQFHMklDbaQ{%F^TzILUkKGSj~S^a9NG}iITOz z*a&>7ubpeb*Zv`&Sd>aONff%P>}P&HzQx^L+atQK2?=<>IW~1lGF|zhEVgDF5jCyN zlON`waiXYu#< z|Fz*nDCCs@$OjAcE);FY#6%*jy5nN|gUhQc^QkIE-^=}b(mBzJS>jKyv5m_jY-Z}7 z5E6#U&b>!(4Gmwx$`XFItcDS zt(_llf0CghXJ?Or;xUAAUR5MYrr(#qy1cTIsajC$vW*Y$nQCqbs>q`MCV|zEhL-k4 z!AP0!jZgVfR7}h(b#*)_bfr4{W`+0;U;dqUxMXx-goXZ?5^_QT+O)P@%~P@ap~h_ZbQ_ z-%Cpu{^Y%Z7PYvwCF9`0))_%ka#9~7o%3&Jh5)b@%;$R;#1t$nk6m3|5pXw6&@)tJ zVPsGxQKqBDeu;*OiHesu@$~c*<;o>+QbW)Worn($3GSBIbBopIYy}tP5n*spP$yB2 zRylx4m(#a^<~x&i~XXg zTq>WKnHf!BVBpqNH3l?$kCUDLotgSuH6BNF@HqwmYinw19%EyR!QR^#FC~ZfE?!^k zQuFcQ2ekZX!w~KRMS_NzIT!YBihw(!CmI+UBJ>t&`LuQ@(*$lnnV_!(h-c|9cSZ5= z^6HzK27UU3)_i$@2K_gbkQdWW$om}e%zqCLKfu7Ch8CBdovkU1@wVaVLo~E3t!f)l z35lik^{xO6Av1K+>+9=|o3YrKm^Kh#o^hBxhP4sP9eyV4tHM|Bx+|Wkum?m}K}o3< zDvLn*x8?BHhd``E#l-Y2EqnJC+9t}3(V$Nv=q8K;kE6eVFdu-Af$$R@74-u48X!Un zP&qdponvL9QgzHzA-8by&VHCMJeXBGfSIsKOKvH%QEST7b>*H)S2} zK_-Faou2PiT&ZE_?7_XE$1N=_Ac{3f1xSjB_?7Z)t72ke+SE3j>(ISba@E$>4wcUN zqEqkEVUYvWgN%$UbaSr8{*E7riTDM*z<7Yv{p|052-J5;OqVGhwq=lbRtwBiAV^xP zjG#V2pNUDc0JsVZ{FVg|*hnrg3H7AHIy?ZJu$&mt(a{!H0-T4b$;q$9#O}f$Dlsz{ ze+C8y7X4Z(555EKhS5bxNVxa!pAl?$k(I!27U?SKhm)0-HMO-PQ>vrII{bix{dG&Y zRoi&@`D4=3c=kSj=MTt+h*5J6ij~O9 zjDz*m=eW4c5sP;{1dqA7xy#X9b@Ib4>s_|{!h_^;vp@yx>XHQAr04qT;$6?-m-90P zm2K@eI=kGlQ0hRIPS4MO1Ba+{+GqtHAv4+y)C;=n;i@b^wjnbPk(Cp5m1ZCMv$M0) zvols0XGUy^8n*pfsRi&X$lTPWvQQ#nwU;(Gd!ZSCW>Bu;FA2Lc^aWyAyQa(*jgOCW z(763bceQmkpQ2f~fA4>10jjJUT~$3AXP1Mtob4Op@+|@Z(jZnp#u!>?_SdhY$>J~q z#z9!*ww*PC0ork5Ss$#tdr2B487aNHrAWrl&tIXrZS5TT?QepjSe|kcL%bw~$p~X* zo_hm4Dz?PXfB&4IxI;=xn1RI_L#uY$8jngzA$&x{Cn_l!Y@ZH_8`TKu$Z$daBsvC0 ziSyLoon@X;0b-=7YTI1cw8(ewF6{0OLa75^Vdo|J`0=BiogItnRh8w~qeK~TMa3sj zrBoY7fZ0F{P2Qe@-dgx%W^gb9emCOTj})jqgYj&A;$Ik%o=yV&WNW(iDK#~9Yg=3Y zXz6h+U4mr=c~W$AD+tT#%|3!kd4KRk3x_sy>4L4szh@5QV_2Gv73&0yf-F#Jg%auz`aKsH7u4PJ zD9NsYfsVO35@;IG-q;e+16o90ytoA$QpDKLLXu#p2qr<&vLaJ_az*?|Y+Rfe+^a(^ zVUCiy6&e8LHo(^a7a}4sczJnaKYw0aU%#)gd6xlZ96(8!=fGDH+DdYAFZ99Sn3$&z z9z1}-IS*Pj%xYs}W1x-=4Gp2V6z~8HBWU6GD=J2n)^Pz$u(jRQtS;i)fs5@BSX*69 z@{%}OX)az`09oJ_hM*{DPcZCR(>gf^gFvx>`EP=r0?G>sObq}@i@>6x6Dqhf)z#N0 zr#3%&^yvME5BO74`4zTvX~5BM;}uxiy0R6W3we(4TC(8n?eDWTm5gmh^{#|D6r--< zS0dk5(A6CPR0l~CCd1BfN;bHJ6IHF-pX$JE%C}zFBO0e3Lubn4!}%w(}l7=T^X(((>4PRu1@00oiwBmk^BI5<$C z!P2gGL7>RJF7gPwrH+S7cS*UpK0_s7K6&!O#)b(-l$C;19+Qxe5R}dEX&uxteD**p zzuM+GpeOA{j}dia$E|TzB)~ht!NDwC?W-#*u++ack7QDKN=J1aG&D4_b932Uw$#y4 z?gU|wi02O5gFt`&Z{wAyC^Age6drqjK)f*CQmoPJd%C9&FA7k~qhMqV zgHbL0`gMO*V9zHV*huzHP82Zs;cbST2JkVkG_}Upn|#F3D*)iIn)KlTK0&~V%B+_L z24u&_$I#0G$cq>leA(9y4+(h=YYw~#P_Js|3uD`MkQ$x%h~ORE^+SlM0C4K7tyw`AjFd;f-W@gk+wBLZr zL2&*2+Vpg9;o;=KTbA!o3R<%>PV2u~wwQ`aq4YDA^8N}?9Yu8;)zsG`1VflS!J(lP ze0-Dn?y=oQrxzClQ^=b5fI?xF8SuR`i*@zGR^$fnEzCMnQa(S+?LVqcXnDPX%ZXA98X{j>M&X`qXQE;#qt~;i0oLg2D?OXLq*I9){Rk z77+VAB{2LRd`;r>yZ_P|Hka$a@mE>MkslQDlo4|ip`L3U4L=4K1u=#Jui|-X|1s)T zpaO1xlFv`nlRa@;((y?EEvo;oNexSupZQ3?*TdFb(Ng88QC?v|_K9qWRrCLufx@Ts zlZ-p2Nxh7s+rlSzM;H@KZ%2^GsvUD@{37nVIkEd-l#tF3F;=+mSsQT;R2B>Cx4zf( zlxW;*MMDw*eBvr_KMSH)zjz8qgPGp&E(rb;rrDw=X<}E7eIAu(XC{0^i z3>szcVQVD{UTrTe1q21%oAbR9gqkpcy*%1eg$Rp1rx&_s;4vgWb(LR=e0$nK@-Zwg zzz^7HM#jd8VdopWyAjpZ-10(a6u-Xf4}dN6zQec@Nh#VHpWuYanYq~~f4~^{`s_kl z1QEaLTA>-N!^`1vO>9-4OBAP{6CGns}PD+wi zS5L5iv;qiRlYR=`>K|CBdOMG%pW1ruliT~|k2>{faf5z%|IB_rigI3l{cpt?!#y=R z`X$(ui9+6dfKyD$hjX&CBSB8nm}@gn!7McreNu~j(&IBqq4SKAA9%V znSXO}p*LQV=g;U3>rFkW$C^(RZ~rHN=P)L3Zpk8I@($}2V=sk>mV_Xugj8(`{YtI# zF;=Yf`iySpT7+5TC|k62hb*n_%~-gxFKyknb#hG0TT&*fC)|3tlXpGw=OS*d$b57f z75I|}+c3-7KG!55XFEJI)P3eJzve>K`xJw^+;ZX;y2qU-5l*>lZ8P{+j?+C6<*yX- zZccEsA)FBZmUhYxnEehsjM?Et)d465&F*-=Me3VJqx1rY>r+xSi zTRyu7?@uWk3;ILawdKl5G);bH8azw`d=dOQYexs0&0uA!pWVZ^5?^-ICLnY9)&(`>4R4mbT7R?`ffgxPVUEz3kV_0VR|MCIPI(HlJyF5f+t zK;AsrMDG`7*gNM)w<8rvjnC;k7uo3p#6nbCC!C(W;)@%r^C?yJ|LZ&EEkSUH`(yuR z2GdH{p7RjFSbeH>veu8Qrc3W(M}cZ1{QY~;r_L>09SwZ3Hm~^LeNy_gbd|qPHO;d! zD!%nO)u#5GPKpkYvV&1Ju{r3?6UG+5c(SJvK4 z@+zsuc2+vFBFS8jJ%1V0w|o#amqfSe|I4Bx80q(b;^5wZFUqGcY!9xB&-q^8n>i9Lf<^=}%Z!swVm5m|4lNtcKl$~L z0#n+?WTep{Y~dzK((Z7#C(II;=lGDC$I~@?>f0Itcboq&)XHEar8i1dp-Z$5J@giO z#j5x;!z#K+XwsB5lZo6dH$GA?nn@2aWnNV^5>ODO?bbqOK~OD;DSK8AB!I%J_IeK4W!`@}X%+w(^?~sadj1K7Jlsm*=!~VJWg*Kl0GM z7d#CUt$>6St#~Agk8?U%q}9|j9S&H#^tkHlaWzhGGKJpZRt?wu@O0P zu(3IHY!msf)0oTW^afAJ6G@O{M_lT~gN+A?STnyO)7)mPyb)!_=M#vUn%4JvV!S>d z1Sm6`&lzI`c6M*xC@Dz4kb5#2?eKe%-TdkL5PJHS{9O&T{d(W~NXIs_Jf6WJFIiDR3$ zyI)P(4>5)uWsKFdJk>ZO8dyIjYXmxN%nez2bKBzbooxSb9V`?rj@{qujl*5Nx%TAt zCfz-tlo#3EN6q`RumHp^(Czpy|_8~4Q0 zQte`6VeNrM%`H}i9|w{fU|2BSB7t5W#y4LEa#>H>9V|<6+0I&(*(fMHeo8GTnyK*J zdWs8_Pq}m9751qAl_c;ZViFR9o129}Y(VHDkeOK`IuCuu-qqdR0$LD4dI1d1YVn5##1zm% zOaVZHTy4^q@IqA;2QCx%XAm%*NO00YhINN%z^w#9Pc9I-Od!P5a5Sp*Z*wf-9 z`nuWDF$h(FK-wwNf!J{PQ~@Vi+H~2Tw1;312vH}c_G$(QIhy-G!#-<-gD>8H2kC+f-561t)BY5A;Ob5C{ z^Y-@E_I4t#BNb@T5Yd|TKG9JvRBu^J_YE%xgXv2fN00fLT!#9bX^z zf3jI@PU{Mw5g#ANm0Ot@8%HH-@Up5|Smf`AH%ja4KZiscs9Q@L8xMZO(!N=rt}SPM z=m#PX{xAZOGJ$x!@n>yoqMQcnBj4eJxEC1>4GtZ0MXHwQOiFpNWo5!^(?b^YrZ>Q; zI-K*F@6piFwT_OCp7PPq1Vh<8tFB%?C*^1|$!kS@^vDD@dc*1BCxkc;J~6v-57uX9 zy;+)z7)c5|bl-tgA)vrtva-OgTv`Ysp`xY^0$DWu!BZ+0_X8swW?d`ov9Ymuh5QC$jq(pFNV5WACj%}d!#Ky*opUI3? z8292dLGuRp=xnU{`X^MUl$@N+jupbCf%K2wt_NrH+*gYD-l$&NUDMeAlR!)1bwmL^ z0TN~3XhG4#*52Mvu;qb0hPvJma9L$kRAA*Og@B$=JXNH{4K5ZUT>_o~ieD)*1PBZo z);-ATa05;RZ4S+7Ic80U+l&CZ3kpj+VNEkp=kBh5n;t;4ziUTAlDYEMT;SL3vSIWRB) zY7BK| zWj&$c-@o?`4sIj-IOxxaqLoV%kb;kdnm~9te&4>)KudeZZ9}M4WyShgI$yiiArKVK zeNdiQyw9CLA?pBhH*A}laY4M0N=xe=nMe3)H}x!a#bJO+@12+4?zS`Z|L_P$*IVw0 z}VlM*E3~FtC*>wJw9!6^zh=78g z$IIQ+>FpyUaf8A)DY@xBlQrHj!9a=aOW}PAty0(b%30TIUo7LGy$JQOf<^;;7T2s4 zFCU+hwl=ylPL^er4i66xUi=-9APvBi1K0j+YVNuNA+@p^APZlgW_7#d<>le1lyFwl zs}-|ATo>|h3D6#yn@-6%veG}cga&)Ym z@9=M3F`QV?!ixwO*VKxoSV@IQ^Xdl7PsFb!Bt-vFJ?CS2us02l9pa-BMH}F~AN<7C zOIXP{_xyyz$fl($NK&~{-UH(+fybTdnf^3cG2B8c+8+kSf|xmf!vH z?he)N(fUxp>8Trp9&Bs3XZl7*-|ftxB*(p^8X%gWc$Y<$Bi+(oOMYf`9L+CRpm94` zx1^-92c+#s+BJ49>TDB@p4=D$dq{vbKzoBI7#P=!2P?gZ<8;-VvyJ=+fYlw{b4RHW zFFiKas0_o?^Fj+Jh(HP3fCW!dT6(eZWR@H{GkheQLHqs5D(ha@&rS|Ml z9c0m3$2D;70;;R4C+rI>`B`p!l9eoXUwVK52T_U#XlP4NZmRNnbaB6-M}o&p+7=+ea{ zaoop8M?owsm(8^LZj&=#YIjUI*bt&cs(0dAa#mC2KsGNfF76nssshuM>sz5F1YN$k z?#?16ELeLfP-wS7tpjVT@vL?V@d!P4Qv^Lf`(7@QQ1v?V5e*7mFhJoUUSHC5j#{bN zpTuctlV~%N>S1lp1bJixI0IL}bGK1#^SIJ_Y#Lwl{QHMbxXekh=sP%`Cp*>^6-OR| zpdDIQ{M%JF8O5G-JLO&%_+H7m8#h6igJHQ_uQ4eCs#rf%SXdhtLVx`)Hwj$jA0s0p z;}=UX&|&g?fFi@+d=(H7fT;ej5H{ENfI$)tyA>hZf{Hxg4Tg5|9}T@4yLTXZBmIK- z3_Q16q1HqIP#}hf2JsHS(h%kApnDG?IJ@PpCp|qqU=M$-sTm*Fg6%#sj`lSNdekC7 zb1-L<;KGM;UgLuA5BM69t%b-9*sJh)h?oJyh9Ic00p@hT-*21J!(i4!AAw{gd@E-E z9&Ma7a6M-m>ihka&o^k$no>FrMr`$2DrWaQ#b-vX4=r!!Axe^6QSm2XwmXKJ7ZHWESyA<#zJ>Yp=>{Yx z0f>SC9|NuWyTMOXNOhb-xEIN7BPSk_I9VG^M?gUat*TInD#x5J0VV@finy`kal^~G zo2&HFF$HjOc)ianR3fwtp8=`|$@)uSVJJk2pA`fOU2L%nov)KaHUW;Ya=1*$zQfPv{DaG4QDAWPZ&+Ry|DcE#+-5+H0OMU_Cc`}) z1%FZBm(A46cE8APoD8JCIe5Xo{PnD|21&_zDF5142e&KlO!mS7lpEuZn&$oi+QYP!e>UQuFtHHG2PjK$@V7#0-}8;!GuR$b<4?g> zM9{&zXgHC;5y3e|bj9wrikF)%J8#Ks!>&@2d1bZ2Mh&D%IhmzZ*nd1E7Dxc^o&?6y z*47pxm;o38Z2MBr6C1E|`@c3~xpq|Nn&ind#V@U_1Qry~!|v`d)dh$F<}J#`#s*Bm zB|MdWUR4!G2UpiMm^U5w3q!ns!HLMpJ%V?b$X|?k{ZKXY9w3r7+Q0Gd zd==x#eRjQjj-;fummuKI1_n=&s}|VFCjCi)Fp)qifMYEmNgW6*P$D3TwY0XTw(A0Z zGWff+5ZwA|4YLCRBdenP-zBpKi@;S9g?=kPEh=(oh9U48x>qH4u@%>!U<^@%^Uw$6_i zz{eEGKRi5agK$02_$0zzEh5iUIYd4YJUhl~e+>nNw+Ds6nnhd$RgDn41%9AfP~^-j zqN_{1va(WPIVQ)ERQ(dW4pqq8f_8y?*Km64Ia2#is%HV68J$J!K@pTYf5rbL>k;0P29q`Tb2FJ3L2h zQ2^~GY?c$-;GuTdf5xBW?}8Ku@cojXdR+`E&M6Izjf}+ZRTih0Qs$wm-xROh=ZI)2 zc!_{FogdNaB?Fp|X(7-G_)FOg0VaaZ+%i#EOQ6V9DnZrkKBF^kHvV`$%eu;Q3RLcA zi8NST;{~#|AB#pMnHIZ%Xf6mNQtYI1|v^3JT@$xvlMYjutN z3S3{z)!KTpqBiTQii4A2kdg67MUgHJM;Jqi(>R}W`r$FLdgs`u^fi+g;rorJ?ZT(a zbMi?IW9o22>XdHky*dOU`N}%&Mr;#A7z;%TG%~~tR^Rw*FKqJi??v$!iRW*>82CVZ zGnLAu`WSzBJZf-h>(7V5YxYmm4d~(NlikrI*BuqzwoA+Fv(pon4vec~=0$aTqnAb% zr1xFLnbZ!Z`vy<-`=`7PRycYpCKN>&IZW@zVt=3le$P=L@5c zPC~Q(=W#`J*RE05)_xubqxXR;L7s9)VID~->tDy#mlwmeOEYhB{`}DtMS9mGJ_fCn zAOA&lmC*4+d%%UCP9}aoLq|b4*J-ECM6PUSZ;nFRkQZ#A7r{Y<-Tu@t6W6iocR$j} zH)l7{eaA}lY<~s3ABp??89)82O;LV_Lzyz9QYTn#yY@@IePc<^y0-Z}nVye}U!489 z_GN`}KX<{`%46>_l*oKExsi0;k~P5M5S`G7O_R$7$?B{s&4!7QaRpAo6z!P*OTxW? z5a_3L)UnMYsJxlupH(1vLIPlVL_SMRS((EX=RMA%%(PGW$-uQw<}(L_8SlBuTiMbh zfw_NeoPZ$WDTQVstWlufP>9PA7TI;J|Ajd8WvxtMhvbm;tQ z4GCjI7CZ~DF;f;i8X6i>LBW}gr`0Qt4_>N#{K%2=iFU5ps1x^s$N~2Y{ZRSRrz@8U z)hss@1GYpKJanL22u&7pn9o=;RvTG-Avg#uK@2(>^g3fc%#Tk+Mr5d-if2VZ4gq3g z`w(K=uY-{VLX08^oI_>|juI1C>co5WSg6YmwPp*{v5Lys&sPm7)(pb!Sg)09$82>Q z`s-uV4*p#Z{loin7guGbj#gPh?F;qsvayR-X?ilQ?UZq{+{{y=o?~%Wd;3g4W^V58 ztUzVpe1o!a0YjtiV8~Ny9tbA@(1mOqaHO7;zV|(GI|1bXq4^M>!F>E!0bUx@Eea7? zknu*BpBum-f}kAgy6LH@3!NXUrf5e+|5S7~=*MB=IUM~}fzFB=cpD%F`~xzYTU%CO zD~|&k1G)&0HEYCTXv0ZPCwIqcr}+z4azE5fRC>A|RjF3e`RCYJg!T&BG}y$YRGX9N z1mySq56eRzFZ@$c&yg1p&l*Zvhu{ZaYB&@ggl|BwdX=dF(KZm%$N*_WgadID2pYZX z+H*Oxk@CI;eW|+7ytE8+5d8pYTzyRg4sXFZ6wp#2uyx+hUDgy_OUl70moAdANMRsF z`q^BWu`M=1h6xBwP4rB^Q`hX7xhbg%poYAG8^=fF z+s5)DIVJCb3~3fsO^5U;IXm27f9HuB(cPFpX542G6j$9yM4^|)CtLOZ#le|`c!TKY zr->9&qtB%osLOs+m;c@jd78LQ_fg0Aid3}WZl=4L@&n|a^jHS!(hHI*waW0f^%pU( zRP5n@*!z#*@4{Wg^PEr}c@#KF>d>jwjwWlKbFhrWmzi?i$2yI41(|C;zBhBoVS zvF*^SR;Uh|`e&Wv=ixe;6&|liRdhnkIq;h<4_^DBG*dFo7_9q1oQgrODFsw;HVESZ z!!h<&PluF1H5~-b<(-0AALI_ao?8C+6 z_;5co{m(Im`b``YGv9PX{6`1@z-0gcuTZxUVGTH*?C3yR9ViRO#mO!;8JRyMqv7Z@ zkJl*+9Al<}BaQY;9na%?d3`Qdx!?b$ar&JyH=Q{M7nY-iw{~V+;-Tqo{H8>l?;J?sHS|sw z&suar!L*1}A^D`>|VfWZ+57m|~KgP}2MRYA@=1{Nqwfd(?Mh5KBosnR)Lvqd-84S3=v zE9&^5rGj$-9Rv#QohRHF%P&BmOWWIF@Bz+5oQy?({(27Y)>H&*QQbq=ul#~<++H9W*JCqnS zG(P9QMnwiaeSJod^+Mpn4sf%e!!@@E;RIWbh z?dl?hQ@lW$lVBIB@CJ#$y1XCX{Kn_eZLJ(ril|t^!Lx)c?cj^ro|CGFe_Iox$~O)~ z^kRE@NSOGeb@f_T3YtrIf1%jS5eFr#~ZTr7csZ>NI zq$x_J$CgAPE2KiUB75<e3oS2uT!@kiC+Xm5`N9D6*1d|36pnZ@mBg9`Ab`&(oW) z`+MKleO~AJSr-KmTRXTw@L;F9Y#UmPG_p>8JU)5x!UZBTC&*>%(A;YA=$PXHL7EGu zLSRe6S#@*P=`varvE>x)-Mja=W-e`R+va0jT!9!IGBE!UoKj3&d<_JGP>g!Uy01il z5P&qc8v64_oSuk3KcUKX|s5eGUaN1FMp~n zyzP|vz#Byl$4Rsbw@7V|gx2E^EL|?T2ApKp@9vGQG-r7XyPQ zG^gPwS0`=5R$SGx^#Xc5`j&pQM1HiGAr+ZpWccM)nIJ6wqXfzT&XQTnA9cvN=T3;j z@X|Sc*Mi*lZNncm&jncA8`CeDJ^Z_AQ>)vIdTOh&T#F!fzK46y5c__%S;s*)owbVD zeAmp?hAGoKuW&mbrYW7Db^GQmt26fVZ10BS-mbir+-;I5v){Y&c<`#(*j>8MR(Z7Q zJ$Hm$;rwuvKj(vMl!j^f$iYrUZuJ`n<6~jzU6iALxbgEfq$5``g4Zu z-{^N=dVl@r`NxNrW;!c`MaRV@18&BwcVCDM`8?(_TfbH=(TCj!g1IB|td9(mpZ*gr zTE84LMdeA+H^%be?mV+moP~GxOXFW!+t#yyWCCmJ10AT2i6@eP^&Sc5zUYVfXqHe-PPP1Zc z!PBsUld3%Dv zJBB#jr~KzAh*<W-ZZQ_CgcJhL>GGuK97E}K2!=KZCf zzRXZC-^ihVaPW#jUH0P1*F8>SC&pF_^75=FG%NHTZITaGNg?WC(WQPGfGU7WQ_}!h z8hm^L=Q|Cw<&M(x5^J|ViL9yc>*J3WbXwA|v1z{P^gd->&iAk1bUXaeBvsmm_4hT&F`JI9hYp5FquU)e#k8FQ7AAZEiG-g zisZ{r7e$xLv73`^97ac6(u1xpP0uhHWWy+&V9h^ydp6Sv+)Gx+TSs zq9Q9=)n1VEqq`Xe)d#62WTjDn!C4y9SvC3ebCX{cs`3Y91OSp!;A(r+JxpmVH*<)W zVU};-#&=64%EB~r4EthV=eV}Ms1doSUp~#r>!855&+Aj}*RP3}6;)S$3!GS1DPxeN zy)P+)H)FKjdve}o{KD5EL-p_qom2HvDFz{GY#Z+?GUByN+7z$zK1HA6*b0~Lt0y}? zO71i07D(eVZ#t;%v~Vi_=H30gYX=TE@gykb{$n}3xws?WD?mjdUgBywb;EM>gcg-h zKD3>@bmUbIVl`~d+&5MB`r^0gKH7YToc<_FB2X4_-bl2m(l}y>LAFleD0doi-{yFOz)U zP)tk=#;kA+i?l$s=sWRxy&<}4%ns2_4tN6;xElQt1Z2j4b$&1GWJvi?XS+ih5eH^= z9V2a4nPfY+M;H|0=!4~X7{3rc&!_KutI-{LdwKO`Hi)Fmu9UrhzZ=XXv>~7)5zT|^ z6nzvFba|K$G8gq!$&?oUQFjkCQAk`;c}DiAy}~cj z0n^gHOIA*~Ia4o?2Y?Oeqg*7fHxvLEp4C9S64M$0pzHnbJ|f+dOd} zqU|9g1v?t97HGk6x;Eg8*1o(HkD*c*Hy$Y8UJ!ajhk`1D-s^2~4#Iwv zCoUR&QA$o8Inx1O51O#0=>YfVxw(&_uW7emMS(X!p&JT|l4JWl+1r56`!gJ6$)S1m zUn5l8Pu}M>x}r$YfW7e@5J14gUn2}-`eE|5kIdJFdk@aC>6WWH%Iaa2duqRAn6B-} zwT_=%UHute0v-Q0kbE%($=4xxY1O@c}3IkVu|Nmd7oZW@1-~ z3=CG@h!-(VK4@%-`imP<5eS>g&=p16^I_7aOnfX4!?*VKhfM^rpPT7O*@p- zVlvi!yTfc48!TC%Sa>#l@75rO>*M`R^sj;sDYF8(*~-k1dE`F0Oo&P_nQji2vQGs{%wW z!4chD;_gnw5b!g;vdv@O@_>6Xly%eJxyd%K%_Lv+9A-F)_{LAk%Eu_67xH11(Pw&! z*$u4dQJ?KT$Ib_Rwj=p>;T$mmHQv&%P0 z>4=QPjNaAN^~&AT(-V*vY+0CKQ3k!_*iwG#o`mQEmQK_$*@TP(KHV=roAPMmn$S{J z11KQcn^c{%aIprl63;ez2)Ls_6*YsYrXX14?h4+3vIkp%p!JQ6NTFEefeZrK z@eI|C0;olcle^f=^UFy_@RMWp90t)1BOnDy3rm!b0;rcWTLen#;cl+G7`kjQfA{t5 zw8L_NuyPQ&F8cm0Xqd*l5gG9tLMf4@DRE$SH^0Ss-gdq^m=GKiuR1X`ex5B@c|je* ztus!)_G3i>IN^c73>Ip^v!5vDG);x3ug;T0()F|AN6} zu11b&gC91F>V;Jz<%IPUAS`&ZcR+tgA{6SjM8aj1=p{<%azs{_Y=OZd?CU7nG$GP? zk}nT%Mgop9ym5HPJyEK_o>BGbtS&G>tAGlq(k`6-^yzIf&LUIbsS?L2mp@h!r=K)^ zj5$Nyb%8aZqO#HkiUX2i&|zjUR1-=Pppeb5>x6pwp#WiB*WFj6mN&`6OrP14rJ|1& zg}-16^&PY`4;qcWVrCIu{E>-18+(SOD{I`#D_my?o(BU(aPWk?PP z+VP*zDu$VI0IN{KFOPAy?L5FNBrG0J04=?8u5M^zkjs@zb2_V zGd|MF?#I}nuq@@M_ReDy*Q(YLogGK0!-*&RKQo}vqt*bUcNjZm#vSpbcyoL)F)%|1 zcKOPcQ`oofwVm~9Kh5^r;dH0QgfLt_m_S4Qbq;^lgbSYd=dprHu_>dy@v=QfX>Ipo zJmW=TheoC)q2hNeeaz& z3K>ngr)=AgCIHP_>M}dNWME(*7HeZfcFZa99~Eg*w4;~segfM~Z@r+CXOmF6pviY1 zZEdJVV)OS`EOsmyRGYB}aSu)&W@kVBR_^!kmUITR&ZB{slfFXzhDCfT|46+LoSKIY z-NOqhn14h*@}|{+jkD5F&tEs+b2d)VD_4NRn(83_C{B@JPu59SzVnyzniaNTT4gC#G^pbK1e6#=8Dyw6bh}1nnS+m!oB5^hO*Eq#UG- z;_nc-52;Sb*pVY*_-wQ5fU%+Vd|kIPH`%LWA(;W`%vB+!%pzCh@qwZaQOkyC6Z-xO zH*X3OQ6Q;k0NzM{1k6*&;l~Hk&f(rd^I?F7b|`!CJ@v_d^eVRyjH1Jg4f`)JUE(5W z>wl7xi9Q`f_$`Fk)f^A69g>R{3MDcy)G*S_Awo{SNcPplY?Jjd2r~ezf>;*nRyKg& zHqT|A8{49z!Jh#9zCLc@%WTArX@&af^awQqQyk`OgR+fkHI=H`6Yg7>vxBQ z-&PNEN9(Ou(Zw$hWqG1ZcO0t)hmLQfmG2;ps%_HcI z@$gezm)$Vxp}c`=J-DF3`wx)W&7SwtN1o7&RXjQHds`7DSA-m)RER+WEYUR4&LY{9 ztRm$`s5=arR+`FO?#+Ji@SoYIt?#t8^_evE<8k9A# zy=98n;_2@!mSPK5quGV<-HBPyA@JBxbQN$r8iZZ945TZm^QvmjQ90Ez2DXGc_i z*mZ62W^Qgfh2%Pnre7`H7o+UeBqcXM&uKT&6+vWGW*yvMn2_US>Q{LmVHw*}f5XNT z>Za}6yFB3t^^HH^bZ%e{cwxmfHJ{ARB6tEJNXcE2SV*^>XMg&KAj-wsnv3YtA^*RP zVnKRpWb384cE1n03F-r+?)K$ld-jWn>_tcoh@~3rBGbQr52GmKHtc zUXU#THh*{RpD7fEMo5Z7WD{oBSn02<%#~!?kEx9HqBX>VBm*pZ^E{}B@q5p99A!Im z=1gj^NJPECm>c@<8o3RiBHEvx2u&VD@&y^|OIDXHiUR5VVRzD&G;+J#;8pXIR7{iE!&PX4@ARw@< zp%?q*lo`Fh{g3`@@dho+i3r<2j%_n;a4)S|NL4_TvfP} zsJwNUzjpJRsV0miBqXFejH?rxV=RNuv{g}4vyNOTnkDIr7Y!kpCpbJE-P(5uUodhl zQFxI#p$D&+Tm*N;!WRfaNkZxP;`Onmke;%cq^WhDskvR8~iY<5(Ix6s6>IoR(=|R{7OKb0HYBb~5<7EnD-6Rtv zaWHL-Gg>{sbYd*33I_6OTKjTasV;Oy+5Cv+nwX;}=MHSV`)`2Vr&r!o8F#OpE;vjp zwMFd6M*G8TOmyoyJNWK+wbF{|LFPf)386O{$Nn9nJfA69YOF@*ITX8pV|KbJPfudc ztJgvwMQA+n>1Vt>zx^-2<^KQ+KG&wY62iI(77b0#v=*noP$XUBg?7n<6aLptyT%hv zxx4%O7oE3V+%`JCO(KZ(vq_e`&Pz>BsZ^CiG6NfuvL(7$;yJBz!iN`@(T|;o>~vJL zjJ=$2Svl7Q9BturB{P)C*og=vhANfX!hgR;c~>T}1z^`VWp8NHcJ{+w*i(4_(g>h<`gmd%p&4!)jC}1K|6pY)*_l{+cxp^1O z9Iyt{tgUf2p%p)XT$O?X1>{0PelSG}B2Ozd`+(BxjK|%1wp3`xNZYw>+lHt3TEI@8 zwU{9Akd0J)yU0cY5m@#J7lZ(ysTw(BJ+KawL28Q-7QY^5npu^^%BQ95Ax%cSB-8|d z`i^yw#PL<#nFcs9*u_A8|I}eGG!@!Ow2E{Y& z?%f|y&*uB?(HJ2Nt3h~X3)U_otcWJ)zYwFoo|Y*R5sNI+<%L;Y+AGY6n;{kL{rmSA zR&U_@fXa(Rc|hM%_FN{I$ee*`A!s-g=pU$Tj5ll`Zt;bI*rMe|RK#O68)$C7ehV0) z-InwcLVmF%rJ#t2YSiyNm+l)J1-e2|Cs0pt$FXQVF+gM65qWk6dutPdoXC6)=p$mz z51kkM7x-!$&SI}Xn-6u;^k9JKT5O~rQP&74fELSis-dYJ0j`oq z#$A#1yleT5^0g2y$e}w%B_#pz)lEWEks0O1La0%ELbrT!+vMa2VxByy!{kcvXRHv^ zVFJX!=mfO7bxz-q6nkcZH>HXTM95)z42+Jdr`UANEM^-1kpItehYiAP#=*qq^&Mwr zFJS2JR)>$*{7;-d?g&ea)96tOI5**1IEV;5QPC0;QD1DdaFi8k>rBk%yHS1IJ-66_ zx`J<}@#k^yb=nJ^lDP6x2R{b5CbB|Q% z=<(BUq`HnB{1+TCL)o%8LV@|&-ohJdl4Es0{BKMiyG{*lLDA*p)TVR>>ogWapMs+H z#l@7gc7~lhLzCE&lNJQqLUzG`jMfs4cD|`{?8Bg+ip#srlJbUCh9lVC zPf{;>G!k;7%t#H1B&9EJW8~P5(d%uW`(hoI+j&86Va3cf?AoNQyfM@=Smh*G3Ag^$ zRwk3OjuJ{|k+O7+wRx*y^*6Yo(g!-2wO~_kz=O>!zOntAC*{=C)Ydj?Ky>Ro^iA#ePTwsYU;e z4zzmv>gxj4*q?E)nU>BF+^!wZGH!%vVv8SwD?AM(yU{lKeWP*CXi zfJSyEOmMWgBY>w-ToA0Z7ViaK-jS(==|s8mZZ2##RgAhP>Aa%za&rmXh8N_HbE*pr zk?3{#+j6K|=&c>MYVz}f ztFr|Yk1M8DuBiI1!dt-*0;<0?ItOm&5i8>SYcU1>6CsDvNl!jL5-Ti~MX0~0Y!sJFzjJ5Hr+rn%d`3INU^Ise z5k~N_H(SM)F%Fi&=1R@LX$9C7@)Q=W=LYdW0npQc^{y!z9vkyzl|itg?f4hIvAL#f za}CgpIN+kvjGst@AbUm~FR!YiXJ)=|?b@}>Ji46o*uCJl+^I68w5yhEG6*q)7K9?D~WevP`uM;s8mp8>M*JeoWYmIV%9 z43jbTN_XL;Q=ZA=Wfr`KOnw_uRj9iP zPUk!+#J44>?eO?Z2T4F)59qtj(deJu<>DsH%DZZz=N$Xl)awZXF;%5g_^bSt;ZG{6 z0}hI`Pr*pGUb?;pHjI^}3t8ZkqhLJf1ZG+U#`XoN1 zwa|IC9@ELwXyrRWwM` zi|Ln?)3M8t^GN_fQOpGusHZ04abl-~?&~JdUzW=jhM%8q!jRTCKEAcfbp0ho#ZQk4 zEVBVzW9G(}g!2)_ZeX-!lXOryC5)Cq!5=@Y%oPQtxm*Em(SC|o=-4netWAw?BHX8}bgA+t<}&}y31HFMQeY%!eU&3rKKbZto^owg+S9%P z0WZL!;u^&}Fu5cq8 zAG#-6!Ei=;$AG3@0l}#qoHqd0;bQWW4!X}OGf<^0uAUl(AYdV_onS>kj|8!LcP=u~ z;cEU1@8=gt+Bp*WB@j{oxJR`^ zTk~~LC9uP1?j0!Gj!l&c7PPs7YrSi-*Tv;w@&a(p8d{R1^VVoP;ldHgP0$9sO(g2S z(x0UkEEY$1BF>yS={`>l8z)H%giL_P`L`L6wJQdD&He)MVE+tGy^_*Tk3~KNA^xFn z6#n%|99bH0wf~jD`uG4*SIMu(WBY`;0%k723I@<7qx%bsu#7)d)eWu)kNoa6hL-ws zMHu4I1S95L7gq&l&I*Z~9BtS-hY?;#LBm02sm_8kdwjYO90%TG+iFBM};6 z11XdZGF<$=I}gqV#OhWwLx5AI5$y>4Agjmy3VZUKb*0;qQ-qFka9|F<#vO6w`0=kK ztpQ9k8OKRt1N5Jyuwksg;PCb}16C>_@|hHFI_aR9i1kkbcHo#K=@AjNp5C3V?+8+yHJSazi#eIqRurCVmvvfZJfxXki%u6ex8_f zf|Y=@;%veARjIZ5+>Gc$ppcA3SS_~f#zz$vzX)vyh&z-L>Z!jz(Xafmqw|5sTVG>Sp1+r*>0|3lUGxOHv}6Be``QUDGyk2@`n z3xwzPHXLOSVFC*T8?PZqLPG3fT!e6E3x3lcQMV^Ro|=G~6TlYTU7r2eE?mwK`pd%A zl||$j1VgW8gB;x+_f=Oi;^CSxtYEeI!;%~gPyo{RD$uR48aom0^B^jfh2i`O6r2u4w%(_eJ^BPE%l2&bMeM5@0^qAlT#a_ z6buedWT#x{x{xV%WGKXOf64N1X57ianFdiqq;^6@`BtuA&IgU@UF6tgTq|7*)V>V# zLde~=^^@XGyg`HwWXuh@#;FUQMzpU3W}sogie9Ui`>MH978j}1Hh zvi5f`xf?RZHJhK7|59eBk7Bxmc>&j(UWq7$8~^$$#|A<+l^gN?Y}K%AEOK_llBbT#caq+V8EY%-|Vu9YoTA>w?Tj!f*8CVVUIL2Z-kj@hg2e`t3T9 zB);U5x5B4!{uLD>M#TL@FI@Qt_hW0Au0 zMxyX9XzuFq@5x;&B`dQhNfrt5HJ~HIL0Czb@;=6j5*rms2?4ip;PBy!;fD6df&&v} zjK5EOmb3f8rQ*KKaZ#B6-k3nVTFE?=Joi939>;{G(6RY~73bs|J)P*P$SdtYd6AhGJXGk(*0KP&B;?F6Y$?Z6y2^h)m#fum5zD>_h>GSl| zsM4MLzN!+ozs|tcqu#CJA2u;@C-_$&@lASb8KfwRBDwzEo9ktJG#;_NZe)Gr6C{c5 z`R0%Gs&6eR3;&aKWF5^bbjs8XyN0^O5`vN5?XO7rA5CdMJ0u#=#pCJShC21>>CyY z|CRzc2EI#|WZ2>O+PW{LZEDk&c=;C_P1kyVDcGiUX-%H7H$jr=8;{U!R2)84g}j0? zY^o_*z+h)kT0r%hj-RoRIViS#De27P>>pL!wTXo}!LPm^TXLtaI`vZN?*8_uX7)#m z`;bc^QYcv+{}wB#3ET@HDb4tG2!@eEiOsR-eg@Qtv%=Ehv-XPJUPbF5#VjM4oO2iX zt-K*dJ?WRYGi)e=yr<^sqBwsJoKn1s8st*zsHk{=R^l0$H?Kq!8lBRrfAnfYV9d?xI|+27W$E<2GxJ}Y|X1bRmX9#njo|5WBcC1%C|DUM&FNe7LEqpQx`LI~k%OVmWKM?TDXZ?~ct3!}Xe_0vZg{jWxz0-+XTN z=an=Ujh&TZNUgSg&@SM!P8T@pIY6Xl+L(%Z6W0Yr9JQ!eAIJT_39CC5`^D-{p-9A@ zm8B7fZQDcWFm~1u*{To8H5l2XvmAvwMLQ5`z=| za{Jq@0E!UXl{(1P!s`w2!S~~RF&wQ|=X88i_R);%A2$==UPxU6pG#V^ z3@g!D>I{7Tt_6x`z~b-BqY!;oHk0)cvI~iQ7U~R~r1-9)BB9A1WRU3cFM8yEC%$~S z5ed&0?&el!76hXgMf%^ko<4>ncB*dx_-@VfzGVyRZ2zd=%oCv^U*G8$W?vq7IgfW7 zn%EhbW!c-aMbO^S{Q{3Rq|4~>FeE_e3z6?`A-f|OQh^sC;0``}!jM5gKvE0QzM}hq zmI@Bx@@~(qq!lookJ(T{&GpTv<4@(y%de|=zqzeH_cBtyJHKP_KeYgneUiavkFSQ- z4uJ^3u^@O+5pD(I5k?aH2^1#ksk{6n?*S}E%8a_XzRceJ25hfV-)-yP61~L4;suG<8L7w{s<0M1RK3#f#Ifzxw@;da9n?2qOT?#3^2IdsB9j?1oMf2b- zB4Lj{e1|vEaDRd!-CI^DY42;NofJ4busoT5{Ma($kk`doFv+jI^OJqGY11=1--k^! zHRlJ2>8T~w(_=13jboPOnV;=%L2DlNvU{5U4K@5-{e61!al=O zZM;>J&wD1$Dsd=#)KYEP{f=lu-62GbhVtR_RW4PGkvK16VGd0Jv&&6~P}%bcDZDpv z#Yi=jWs(B2cVVcF2?7Lyo^0`?m`=E2Pbx%Qw+osBzPb#W$g*V^+TtVa@JjLX}JwwA;iGzCj3nGu74l7 zZVC6}$a@dx%&*D2vj1r_Z!8;DoVho`l@TImvvPdRY7;d04Fzd<<#JkBf zZu#FL@s?y^`9N{?>k;+C+-G`6>Z8b^FQC(il@r^`K!1NJrrG)LFJj3hvIP@yFOomO zD6_-+inf?UtB`>YjwFP?keP61q;}g?O(i8I19zZyAn-QhoE*@5aKP#!Zvw4n6i_p8 zamii1XwV3=h0n!)iat#qWEjpjfoRnhe|3XzLSGjQ;Utj@L-QdVO#&e)bj#k=H#i*k zz!_}N5L1uAn;ebj34s+(HN$wHY0)k8%c&?{R3W|z-7+-1xfo8cG3tJJbbw42I1A*E z3>|q|=;A1d+J_A2L`8)86ajM|F~ne|Ge83k*eej(Gvo-H|6<_CGNE#pt+J8UkM{JW z0*!ecl5c!4!);)u(7=e{TZpbM@sMagNT754v!ifuCP+^rfhNQqCSyc`(x3w+q5#0w zmAIq`L2KPSp%4FNZ-vSK*3W8^#6-^GFnoW1Q>(>1zDGvSIi+k-53zG+&r+ZlW#i=Z z$Ninb0wYt@jT_IU_lhy%Ap-Ql(PMC`5jh!hJD8EUXH(U<42}GM&*S_q{miE@C35p^ z1MZ4Pg?k|l-U|y^AG8~V3$6gI0bIwG2m;N8Qj36ZOuQR$dK+0=IB)_URMhVvl>HyU zRPEE-p7W>fho4@xz~O||Noyj|Cl4L31nG+L$W!$+)J6pm)PY>c4HNS6Db3#jx8w7$ z*dag3{4Iy~3cy8gehGRug#7zL`&e5`jesq{0Z2oDKNpz%d$jBDVm11LQy)(xLw#{DAiY`8AK(*%CN6f(i#)ka#f`@*~**=g{zQoJsTFE?OnFTknb}t20DF++*=#$|cu|PpGSV zl~I^5Hv;t^IfHjJC5<4*@ zGjcI0NP|2jm~q}!=gGeY|InB~iOV*^BYOTe2PO>3;P0%=@CsiI4vG7q$Dnw^&-7#S z#)5}h38V$^{Xg*NM2NbJ(46^&36l}4U0XEb zy!-$e-#m_3ZUEtkR)icV$I8a0a&r%#ed?09(a=6IX{)Q7v*tAmtY5cwzBKN-7%k)h z2DahM+#a+TkW=4#`}VCYy)=530O6k-!N;&dY>yj*9)y(C(5u=wQY{#Ck~Ii`y|=z@ zCwZtSvZh*YQiMd*4e#$dKl|`gyK?VU5l45Fh+l|ttt#1``Bs88|TYZn@$0&F&+hYvLNI+lvkhf(B{u|ki z1R=D5l50}=x0a2;Jt|=>iwI|GO9Q!E z4QmqS_n{n*ks%=+?3fJ@gv60$=1QSGa|

P|mDvCzx+=jF^9PXv`Q&<1S4WeP& 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 literal 27628 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 literal 25855 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 z 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 diff --git a/desktop/sequencer.png b/desktop/sequencer.png index a3926c5839550cc86e29672b1b627b257788fd2e..679d27ebca1afdb4b93546cda5671e59476fe75f 100644 GIT binary patch literal 67173 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

-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 ADgXcg