Skip to content

Commit

Permalink
[api][desktop]: Support Auto Focus
Browse files Browse the repository at this point in the history
Successfully tested using ASCOM Sky Simulator
  • Loading branch information
tiagohm committed May 31, 2024
1 parent 32e5c0f commit 2e04410
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 50 deletions.
65 changes: 63 additions & 2 deletions api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,74 @@ package nebulosa.api.autofocus

import nebulosa.api.cameras.CameraCaptureEvent
import nebulosa.api.messages.MessageEvent
import nebulosa.curve.fitting.CurvePoint
import nebulosa.curve.fitting.*
import nebulosa.nova.almanac.evenlySpacedNumbers

data class AutoFocusEvent(
@JvmField val state: AutoFocusState = AutoFocusState.IDLE,
@JvmField val focusPoint: CurvePoint = CurvePoint.ZERO,
@JvmField val focusPoint: CurvePoint? = null,
@JvmField val determinedFocusPoint: CurvePoint? = null,
@JvmField val starCount: Int = 0,
@JvmField val starHFD: Double = 0.0,
@JvmField val minX: Double = 0.0,
@JvmField val minY: Double = 0.0,
@JvmField val maxX: Double = 0.0,
@JvmField val maxY: Double = 0.0,
@JvmField val chart: Chart? = null,
@JvmField val capture: CameraCaptureEvent? = null,
) : MessageEvent {

data class Chart(
@JvmField val trendLine: Map<String, Any?>? = null,
@JvmField val parabolic: Map<String, Any?>? = null,
@JvmField val hyperbolic: Map<String, Any?>? = null,
)

override val eventName = "AUTO_FOCUS.ELAPSED"

companion object {

@JvmStatic
fun makeChart(
points: List<CurvePoint>,
trendLine: TrendLineFitting.Curve?,
parabolic: QuadraticFitting.Curve?,
hyperbolic: HyperbolicFitting.Curve?
) = with(evenlySpacedNumbers(points.first().x, points.last().x, 100)) {
Chart(trendLine?.mapped(this), parabolic?.mapped(this), hyperbolic?.mapped(this))
}

@JvmStatic
private fun TrendLineFitting.Curve.mapped(points: DoubleArray) = mapOf(
"left" to left.mapped(points),
"right" to right.mapped(points),
"intersection" to intersection,
"minimum" to minimum, "rSquared" to rSquared,
)

@JvmStatic
private fun TrendLine.mapped(points: DoubleArray) = mapOf(
"slope" to slope, "intercept" to intercept,
"rSquared" to rSquared,
"points" to makePoints(points)
)

@JvmStatic
private fun QuadraticFitting.Curve.mapped(points: DoubleArray) = mapOf(
"minimum" to minimum, "rSquared" to rSquared,
"points" to makePoints(points)
)

@JvmStatic
private fun HyperbolicFitting.Curve.mapped(points: DoubleArray) = mapOf(
"a" to a, "b" to b, "p" to p,
"minimum" to minimum, "rSquared" to rSquared,
"points" to makePoints(points)
)

@Suppress("NOTHING_TO_INLINE")
private inline fun Curve.makePoints(points: DoubleArray): List<CurvePoint> {
return points.map { CurvePoint(it, this(it)) }
}
}
}
5 changes: 4 additions & 1 deletion api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ enum class AutoFocusState {
IDLE,
MOVING,
EXPOSURING,
COMPUTING,
EXPOSURED,
ANALYSING,
ANALYSED,
FOCUS_POINT_ADDED,
CURVE_FITTED,
FAILED,
FINISHED,
}
89 changes: 56 additions & 33 deletions api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ data class AutoFocusTask(
private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation)

@Volatile private var trendLineCurve: TrendLineFitting.Curve? = null
@Volatile private var parabolicCurve: Lazy<QuadraticFitting.Curve>? = null
@Volatile private var hyperbolicCurve: Lazy<HyperbolicFitting.Curve>? = null
@Volatile private var parabolicCurve: QuadraticFitting.Curve? = null
@Volatile private var hyperbolicCurve: HyperbolicFitting.Curve? = null

@Volatile private var measurementPos = 0
@Volatile private var focusPoint = CurvePoint.ZERO
@Volatile private var focusPoint: CurvePoint? = null
@Volatile private var starCount = 0
@Volatile private var starHFD = 0.0
@Volatile private var determinedFocusPoint: CurvePoint? = null

init {
cameraCaptureTask.subscribe(this)
Expand Down Expand Up @@ -94,6 +97,8 @@ data class AutoFocusTask(

obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken)

if (cancellationToken.isCancelled) break

var leftCount = trendLineCurve?.left?.points?.size ?: 0
var rightCount = trendLineCurve?.right?.points?.size ?: 0

Expand Down Expand Up @@ -138,6 +143,8 @@ data class AutoFocusTask(
obtainFocusPoints(1, 1, false, cancellationToken)
}

if (cancellationToken.isCancelled) break

leftCount = trendLineCurve!!.left.points.size
rightCount = trendLineCurve!!.right.points.size

Expand All @@ -156,13 +163,15 @@ data class AutoFocusTask(
}
} while (!cancellationToken.isCancelled && (rightCount + focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps))

if (exited) break
if (exited || cancellationToken.isCancelled) break

val finalFocusPoint = determineFinalFocusPoint()
val goodAutoFocus = validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken)

if (!goodAutoFocus) {
if (numberOfAttempts < request.totalNumberOfAttempts) {
if (cancellationToken.isCancelled) {
break
} else if (numberOfAttempts < request.totalNumberOfAttempts) {
moveFocuser(initialFocusPosition, cancellationToken, false)
LOG.warn("potentially bad auto-focus. Reattempting")
reset()
Expand All @@ -172,8 +181,8 @@ data class AutoFocusTask(
exited = true
}
} else {
determinedFocusPoint = finalFocusPoint
LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y)
moveFocuser(finalFocusPoint.x.roundToInt(), cancellationToken, false)
break
}
}
Expand All @@ -195,16 +204,12 @@ data class AutoFocusTask(
}

private fun determineFinalFocusPoint(): CurvePoint {
val trendLine by lazy { TrendLineFitting.calculate(focusPoints) }
val hyperbolic by lazy { HyperbolicFitting.calculate(focusPoints) }
val parabolic by lazy { QuadraticFitting.calculate(focusPoints) }

return when (request.fittingMode) {
AutoFocusFittingMode.TRENDLINES -> trendLine.intersection
AutoFocusFittingMode.PARABOLIC -> parabolic.minimum
AutoFocusFittingMode.TREND_PARABOLIC -> trendLine.intersection midPoint parabolic.minimum
AutoFocusFittingMode.HYPERBOLIC -> hyperbolic.minimum
AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLine.intersection midPoint hyperbolic.minimum
AutoFocusFittingMode.TRENDLINES -> trendLineCurve!!.intersection
AutoFocusFittingMode.PARABOLIC -> parabolicCurve!!.minimum
AutoFocusFittingMode.TREND_PARABOLIC -> trendLineCurve!!.intersection midPoint parabolicCurve!!.minimum
AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve!!.minimum
AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLineCurve!!.intersection midPoint trendLineCurve!!.minimum
}
}

Expand All @@ -214,15 +219,18 @@ data class AutoFocusTask(

override fun accept(event: CameraCaptureEvent) {
if (event.state == CameraCaptureState.EXPOSURE_FINISHED) {
sendEvent(AutoFocusState.COMPUTING, capture = event)
sendEvent(AutoFocusState.EXPOSURED, event)
sendEvent(AutoFocusState.ANALYSING)
val detectedStars = starDetection.detect(event.savePath!!)
LOG.info("detected ${detectedStars.size} stars")
val measure = detectedStars.measureDetectedStars()
LOG.info("HFD measurement. mean={}", measure)
measurements[measurementPos++] = measure
starCount = detectedStars.size
LOG.info("detected $starCount stars")
starHFD = detectedStars.measureDetectedStars()
LOG.info("HFD measurement. mean={}", starHFD)
measurements[measurementPos++] = starHFD
sendEvent(AutoFocusState.ANALYSED)
onNext(event)
} else {
sendEvent(AutoFocusState.EXPOSURING, capture = event)
sendEvent(AutoFocusState.EXPOSURING, event)
}
}

Expand Down Expand Up @@ -256,19 +264,23 @@ data class AutoFocusTask(

val measurement = takeExposure(cancellationToken)

if (cancellationToken.isCancelled) break

LOG.info("HFD measured after exposures. mean={}", measurement)

if (remainingSteps-- > 1) {
focusPosition = moveFocuser(direction * -stepSize, cancellationToken, true)
}

if (cancellationToken.isCancelled) break

// If star measurement is 0, we didn't detect any stars or shapes,
// and want this point to be ignored by the fitting as much as possible.
if (measurement == 0.0) {
LOG.warn("No stars detected in step")
} else {
focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement)
focusPoints.add(focusPoint)
focusPoints.add(focusPoint!!)
focusPoints.sortBy { it.x }

LOG.info("focus point added. remainingSteps={}, point={}", remainingSteps, focusPoint)
Expand All @@ -281,16 +293,18 @@ data class AutoFocusTask(
}

private fun computeCurveFittings() {
with(focusPoints.toList()) {
with(focusPoints) {
trendLineCurve = TrendLineFitting.calculate(this)

if (size >= 3) {
if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) {
parabolicCurve = lazy { QuadraticFitting.calculate(this) }
parabolicCurve = QuadraticFitting.calculate(this)
} else if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) {
hyperbolicCurve = lazy { HyperbolicFitting.calculate(this) }
hyperbolicCurve = HyperbolicFitting.calculate(this)
}
}

sendEvent(AutoFocusState.CURVE_FITTED)
}
}

Expand All @@ -301,8 +315,8 @@ data class AutoFocusTask(

if (threshold > 0.0) {
fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: true
fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: true
fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: true
fun isParabolicBad() = parabolicCurve?.let { it.rSquared < threshold } ?: true
fun isHyperbolicBad() = hyperbolicCurve?.let { it.rSquared < threshold } ?: true

val isBad = when (request.fittingMode) {
AutoFocusFittingMode.TRENDLINES -> isTrendLineBad()
Expand All @@ -318,14 +332,16 @@ data class AutoFocusTask(
}
}

val min = focusPoints.minOf { it.x }
val max = focusPoints.maxOf { it.x }
val min = focusPoints.first().x
val max = focusPoints.last().x

if (focusPoint.x < min || focusPoint.y > max) {
if (focusPoint.x < min || focusPoint.x > max) {
LOG.error("determined focus point position is outside of the overall measurement points of the curve")
return false
}

if (cancellationToken.isCancelled) return false

moveFocuser(focusPoint.x.roundToInt(), cancellationToken, false)
val hfd = takeExposure(cancellationToken)

Expand All @@ -346,9 +362,16 @@ data class AutoFocusTask(
return focuser.position
}

@Suppress("NOTHING_TO_INLINE")
private inline fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) {
onNext(AutoFocusEvent(state, focusPoint, capture))
private fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) {
val chart = when (state) {
AutoFocusState.FOCUS_POINT_ADDED -> AutoFocusEvent.makeChart(focusPoints, trendLineCurve, parabolicCurve, hyperbolicCurve)
else -> null
}

val (minX, minY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[0]
val (maxX, maxY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[focusPoints.lastIndex]

onNext(AutoFocusEvent(state, focusPoint, determinedFocusPoint, starCount, starHFD, minX, minY, maxX, maxY, chart, capture))
}

override fun reset() {
Expand Down
2 changes: 1 addition & 1 deletion desktop/app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,6 @@ function sendToAllWindows(channel: string, data: any, home: boolean = true) {
}

if (serve) {
console.info(data)
console.info(JSON.stringify(data))
}
}
26 changes: 24 additions & 2 deletions desktop/src/app/autofocus/autofocus.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@
<neb-device-chooser title="FOCUSER" icon="mdi mdi-target" [devices]="focusers" [(device)]="focuser"
(deviceChange)="focuserChanged()" />
</div>
<div class="col-12 relative pt-0 text-sm text-gray-400 flex align-items-center mt-1 gap-1 text-sm" style="min-height: 25px;">
<neb-camera-exposure #cameraExposure [info]="status" />
</div>
<div class="col-12 mt-0">
<div class="col-3 align-items-center">
<span class="p-float-label">
<input pInputText [readonly]="true" class="p-inputtext-sm border-0 max-w-full" [value]="focuser.position" />
<label>Position</label>
</span>
</div>
<div class="col-3 align-items-center">
<span class="p-float-label">
<input pInputText [readonly]="true" class="p-inputtext-sm border-0 max-w-full" [value]="starCount" />
<label>Star Count</label>
</span>
</div>
<div class="col-3 align-items-center">
<span class="p-float-label">
<input pInputText [readonly]="true" class="p-inputtext-sm border-0 max-w-full" [value]="starHFD" />
<label>HFD</label>
</span>
</div>
</div>
<div class="col-12 mt-3">
<div class="grid">
<div class="col-4 align-items-center">
Expand Down Expand Up @@ -51,8 +74,7 @@
<span class="p-float-label">
<p-dropdown [options]="['NONE', 'ABSOLUTE', 'OVERSHOOT']"
[(ngModel)]="request.backlashCompensation.mode" [autoDisplayFirst]="false"
styleClass="p-inputtext-sm border-0"
(ngModelChange)="savePreference()" />
styleClass="p-inputtext-sm border-0" (ngModelChange)="savePreference()" />
<label>Backlash Compensation</label>
</span>
</div>
Expand Down
Loading

0 comments on commit 2e04410

Please sign in to comment.