Skip to content

Commit

Permalink
PixInsight Integration (#432)
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagohm authored Jun 8, 2024
2 parents 9df5cdd + d2736c0 commit 3a9b0ce
Show file tree
Hide file tree
Showing 72 changed files with 2,850 additions and 333 deletions.
1 change: 1 addition & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -177,7 +188,7 @@ class CalibrationFrameService(
name: String, width: Int, height: Int,
binX: Int, binY: Int, filter: String?
): List<CalibrationFrameEntity> {
// 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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ enum class CameraCaptureState {
WAITING,
SETTLING,
DITHERING,
PAUSING,
PAUSED,
EXPOSURE_FINISHED,
CAPTURE_FINISHED,
}
34 changes: 31 additions & 3 deletions api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -29,7 +31,7 @@ data class CameraCaptureTask(
private val exposureMaxRepeat: Int = 0,
private val executor: Executor? = null,
private val calibrationFrameProvider: CalibrationFrameProvider? = null,
) : AbstractTask<CameraCaptureEvent>(), Consumer<Any>, CameraEventAware {
) : AbstractTask<CameraCaptureEvent>(), Consumer<Any>, PauseListener, CameraEventAware {

private val delayTask = DelayTask(request.exposureDelay)
private val waitForSettleTask = WaitForSettleTask(guider)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
) {
Expand All @@ -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)

Expand Down Expand Up @@ -160,6 +181,9 @@ data class CameraCaptureTask(
}
}

pausing.set(false)
cancellationToken.unlistenToPause(this)

sendEvent(CameraCaptureState.CAPTURE_FINISHED)

liveStacker?.close()
Expand Down Expand Up @@ -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)
Expand All @@ -237,6 +263,7 @@ data class CameraCaptureTask(
delayAndWaitForSettleSplitTask.close()
cameraExposureTask.close()
ditherAfterExposureTask.close()
liveStacker?.close()
super.close()
}

Expand All @@ -256,6 +283,7 @@ data class CameraCaptureTask(
cameraExposureTask.reset()
ditherAfterExposureTask.reset()

pausing.set(false)
exposureRepeatCount = 0
}

Expand Down
10 changes: 10 additions & 0 deletions api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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,10 @@ 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.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 +19,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 = 1,
) : 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand All @@ -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)) {
Expand Down
Loading

0 comments on commit 3a9b0ce

Please sign in to comment.