Skip to content

Commit

Permalink
[api][desktop]: Support PixInsight Live Stacking
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagohm committed Jun 6, 2024
1 parent 96a1e6e commit 9ba998a
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package nebulosa.api.livestacking

enum class LiveStackerType {
SIRIL,
PIXINSIGHT,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,15 +20,28 @@ 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<LiveStacker> {

override fun get(): LiveStacker {
val workingDirectory = Files.createTempDirectory("ls-")

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)
}
}
}

Expand Down
8 changes: 6 additions & 2 deletions desktop/src/app/camera/camera.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -255,15 +255,15 @@
</div>
<div class="col-8 align-items-center">
<span class="p-float-label">
<p-dropdown [disabled]="!request.liveStacking.enabled" [options]="['SIRIL']"
<p-dropdown [disabled]="!request.liveStacking.enabled" [options]="'LIVE_STACKER' | dropdownOptions"
[(ngModel)]="request.liveStacking.type" styleClass="p-inputtext-sm border-0" (ngModelChange)="savePreference()"
[autoDisplayFirst]="false" />
<label>Live Stacker</label>
</span>
</div>
<div class="col-6 flex flex-column align-items-center justify-content-center text-center gap-2">
<span class="text-xs text-gray-100">32-bits (slower)</span>
<p-inputSwitch [disabled]="!request.liveStacking.enabled || request.liveStacking.type !== 'SIRIL'"
<p-inputSwitch [disabled]="!request.liveStacking.enabled"
[(ngModel)]="request.liveStacking.use32Bits" (ngModelChange)="savePreference()" />
</div>
<div class="col-6 align-items-center">
Expand All @@ -282,5 +282,9 @@
<neb-path-chooser [disabled]="!request.liveStacking.enabled" [directory]="false" label="Flat File"
key="LS_FLAT" [(path)]="request.liveStacking.flat" class="w-full" (pathChange)="savePreference()" />
</div>
<div class="col-12 align-items-center">
<neb-path-chooser [disabled]="!request.liveStacking.enabled || request.liveStacking.type !== 'PIXINSIGHT'" [directory]="false"
label="Bias File" key="LS_BIAS" [(path)]="request.liveStacking.bias" class="w-full" (pathChange)="savePreference()" />
</div>
</div>
</p-dialog>
13 changes: 9 additions & 4 deletions desktop/src/app/camera/camera.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/app/settings/settings.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() { }
Expand Down Expand Up @@ -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'))
}
}
2 changes: 1 addition & 1 deletion desktop/src/shared/pipes/dropdown-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
3 changes: 2 additions & 1 deletion desktop/src/shared/types/camera.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -293,6 +293,7 @@ export interface LiveStackingRequest {
executablePath: string,
dark?: string,
flat?: string,
bias?: string,
rotate: number,
use32Bits: boolean,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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() {
Expand All @@ -41,25 +46,26 @@ data class PixInsightLiveStacker(
} catch (e: Throwable) {
throw IllegalStateException("unable to start PixInsight")
}

running.set(true)
}

stackCount = 0
running.set(true)
}
}

@Synchronized
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)
}
}

Expand All @@ -71,37 +77,53 @@ 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) {
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++
}
}
} 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<PixInsightLiveStacker>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ abstract class AbstractPixInsightScript<T> : PixInsightScript<T>, 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) {
commandLine.whenComplete { exitCode, exception ->
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))
Expand Down Expand Up @@ -57,11 +61,21 @@ abstract class AbstractPixInsightScript<T> : PixInsightScript<T>, 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('"')
}
}
Expand Down
2 changes: 1 addition & 1 deletion nebulosa-pixinsight/src/main/resources/pixinsight/Align.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 9ba998a

Please sign in to comment.