From 1082e4fa8c48c982be128e82a84ffe16d8458445 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 10 Oct 2024 00:47:03 -0300 Subject: [PATCH 1/2] [api]: Implement Auto Focus algorithm --- nebulosa-autofocus/build.gradle.kts | 20 + .../kotlin/nebulosa/autofocus/AutoFocus.kt | 420 ++++++++++++++++++ .../autofocus/AutoFocusFittingMode.kt | 9 + .../nebulosa/autofocus/AutoFocusResult.kt | 31 ++ .../nebulosa/autofocus/MeasuredStars.kt | 9 + .../src/test/kotlin/AutoFocusTest.kt | 115 +++++ settings.gradle.kts | 1 + 7 files changed, 605 insertions(+) create mode 100644 nebulosa-autofocus/build.gradle.kts create mode 100644 nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocus.kt create mode 100644 nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusFittingMode.kt create mode 100644 nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusResult.kt create mode 100644 nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/MeasuredStars.kt create mode 100644 nebulosa-autofocus/src/test/kotlin/AutoFocusTest.kt diff --git a/nebulosa-autofocus/build.gradle.kts b/nebulosa-autofocus/build.gradle.kts new file mode 100644 index 000000000..6703ef12f --- /dev/null +++ b/nebulosa-autofocus/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + kotlin("jvm") + id("maven-publish") +} + +dependencies { + api(project(":nebulosa-curve-fitting")) + api(project(":nebulosa-image")) + api(project(":nebulosa-stardetector")) + implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-test")) +} + +publishing { + publications { + create("pluginMaven") { + from(components["java"]) + } + } +} diff --git a/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocus.kt b/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocus.kt new file mode 100644 index 000000000..009cc496c --- /dev/null +++ b/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocus.kt @@ -0,0 +1,420 @@ +package nebulosa.autofocus + +import nebulosa.curve.fitting.CurvePoint +import nebulosa.curve.fitting.CurvePoint.Companion.midPoint +import nebulosa.curve.fitting.HyperbolicFitting +import nebulosa.curve.fitting.QuadraticFitting +import nebulosa.curve.fitting.TrendLineFitting +import nebulosa.log.loggerFor +import nebulosa.stardetector.StarDetector +import nebulosa.stardetector.StarPoint +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics +import java.nio.file.Path +import kotlin.math.roundToInt + +data class AutoFocus( + private val starDetector: StarDetector, + private val exposureAmount: Int = 5, + private val initialOffsetSteps: Int = 4, + private val stepSize: Int = 50, + private val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.TREND_HYPERBOLIC, + private val rSquaredThreshold: Double = 0.0, + private val reverse: Boolean = false, + private val focusMaxPosition: Int = Int.MAX_VALUE, +) { + + // TODO: Usar sealed class e remover substate e algumas variáveis como moveFocuserToLeftmostOrRightmost. + private enum class State { + IDLE, + FOCUS_POINTS, + EVALUATE_TREND_LINE_POINTS, + MORE_POINTS_TO_THE_LEFT, + MORE_POINTS_TO_THE_RIGHT, + VERIFY_DATA_IS_ENOUGTH, + DETERMINATE_FINAL_FOCUS_POINT, + } + + private enum class SubState { + IDLE, + MOVE_FOCUSER, + TAKE_EXPOSURE, + } + + private val direction = if (reverse) -1 else 1 + private val numberOfSteps = initialOffsetSteps + 1 + private val measurements = ArrayList(exposureAmount) + private val maximumFocusPoints = exposureAmount * initialOffsetSteps * 10 + private val focusPoints = ArrayList(maximumFocusPoints) + + @Volatile private var subState = SubState.IDLE + @Volatile private var prevState = State.IDLE + @Volatile private var state = State.IDLE + private set(value) { + prevState = field + field = value + } + + @Volatile private var measurement = MeasuredStars.EMPTY + @Volatile private var exposureCount = 0 + @Volatile private var remainingSteps = 0 + @Volatile private var initialFocusPosition = 0 + @Volatile private var currentFocusPosition = 0 + @Volatile private var moveFocuserToLeftmostOrRightmost = false + + @Volatile private var trendLineCurve: TrendLineFitting.Curve? = null + @Volatile private var parabolicCurve: QuadraticFitting.Curve? = null + @Volatile private var hyperbolicCurve: HyperbolicFitting.Curve? = null + @Volatile private var leftCount = 0 + @Volatile private var rightCount = 0 + + @Volatile var determinedFocusPoint: CurvePoint? = null + private set + + private val isDataPointsEnough + get() = trendLineCurve != null && (rightCount + focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } >= initialOffsetSteps && leftCount + focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } >= initialOffsetSteps) + + fun add(path: Path) { + if (exposureCount > 0) { + exposureCount-- + LOG.debug("added. path={}, exposureCount={}", path, exposureCount) + measurement = measureStars(path) + + if (exposureCount == 0) { + measurement = evaluateAllMeasurements() + } + } + } + + fun reset() { + subState = SubState.IDLE + state = State.IDLE + prevState = State.IDLE + + measurement = MeasuredStars.EMPTY + exposureCount = 0 + remainingSteps = 0 + initialFocusPosition = 0 + currentFocusPosition = 0 + moveFocuserToLeftmostOrRightmost = false + + trendLineCurve = null + parabolicCurve = null + hyperbolicCurve = null + leftCount = 0 + rightCount = 0 + } + + fun determinate(focusPosition: Int): AutoFocusResult { + LOG.debug("determinate. position={}, state={}, subState={}, prevState={}", focusPosition, state, subState, prevState) + + when (state) { + // Estado inicial. + State.IDLE -> { + // Salva a posição inicial do focalizador, para que possa voltar caso algo dê errado. + initialFocusPosition = focusPosition + + // Iniciar a coleta dos pontos de foco. + state = State.FOCUS_POINTS + remainingSteps = numberOfSteps + // Próxima passo é mover o focalizador para a posição mais distante. + return moveFocuser(direction * initialOffsetSteps * stepSize, true) + } + // Estado para obter os pontos de foco e calcular a curva. + State.FOCUS_POINTS -> { + // Ainda não terminou de capturar todos os pontos de foco necessário. + if (remainingSteps > 0) { + // Terminou de mover o focalizador. + if (subState == SubState.MOVE_FOCUSER) { + currentFocusPosition = focusPosition + return takeExposure() + } + // Ainda não terminou a captura. + else if (exposureCount > 0) { + return takeExposure() + } + // Fim da captura. + else if (exposureCount == 0) { + LOG.debug("HFD measured after exposures. hfd={}, stdDev={}", measurement.hfd, measurement.stdDev) + + computeCurvePoint() + + // Continua a mover o focalizador. + if (remainingSteps-- > 1) { + return moveFocuser(direction * -stepSize, true) + } + + state = State.EVALUATE_TREND_LINE_POINTS + subState = SubState.IDLE + + return AutoFocusResult.Determinate + } + } + } + State.EVALUATE_TREND_LINE_POINTS -> { + leftCount = trendLineCurve?.left?.points?.size ?: 0 + rightCount = trendLineCurve?.right?.points?.size ?: 0 + + LOG.debug("trend line evaluated. left={}, right={}", leftCount, rightCount) + + if (leftCount == 0 && rightCount == 0) { + LOG.warn("Not enought spreaded points") + return AutoFocusResult.Failed(initialFocusPosition) + } + + // Let's keep moving in, one step at a time, until we have enough left trend points. + // Then we can think about moving out to fill in the right trend points. + if (trendLineCurve!!.left.points.size < initialOffsetSteps + && focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < initialOffsetSteps + ) { + LOG.info("more data points needed to the left of the minimum") + + state = State.MORE_POINTS_TO_THE_LEFT + + val firstX = focusPoints.first().x.roundToInt() + + // Move to the leftmost point - this should never be necessary since we're already there, but just in case + if (focusPosition != firstX) { + moveFocuserToLeftmostOrRightmost = true + return moveFocuser(firstX, false) + } + + // More points needed to the left. + return moveFocuser(direction * -stepSize, true) + } else if (trendLineCurve!!.right.points.size < initialOffsetSteps + && focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < initialOffsetSteps + ) { + // Now we can go to the right, if necessary. + LOG.info("more data points needed to the right of the minimum") + + state = State.MORE_POINTS_TO_THE_RIGHT + + val lastX = focusPoints.last().x.roundToInt() + + // More points needed to the right. Let's get to the rightmost point, and keep going right one point at a time. + if (focusPosition != lastX) { + moveFocuserToLeftmostOrRightmost = true + return moveFocuser(lastX, false) + } + + // More points needed to the right. + return moveFocuser(direction * stepSize, true) + } + + state = State.DETERMINATE_FINAL_FOCUS_POINT + return AutoFocusResult.Determinate + } + State.MORE_POINTS_TO_THE_LEFT -> { + // Terminou de mover o focalizador. + if (moveFocuserToLeftmostOrRightmost) { + moveFocuserToLeftmostOrRightmost = false + // More points needed to the left. + return moveFocuser(direction * -stepSize, true) + } else if (subState == SubState.MOVE_FOCUSER) { + currentFocusPosition = focusPosition + return takeExposure() + } + // Ainda não terminou a captura. + else if (exposureCount > 0) { + return takeExposure() + } + // Fim da captura. + else if (exposureCount == 0) { + state = State.VERIFY_DATA_IS_ENOUGTH + return AutoFocusResult.Determinate + } + } + State.MORE_POINTS_TO_THE_RIGHT -> { + // Terminou de mover o focalizador. + if (moveFocuserToLeftmostOrRightmost) { + moveFocuserToLeftmostOrRightmost = false + // More points needed to the right. + return moveFocuser(direction * stepSize, true) + } else if (subState == SubState.MOVE_FOCUSER) { + currentFocusPosition = focusPosition + return takeExposure() + } + // Ainda não terminou a captura. + else if (exposureCount > 0) { + return takeExposure() + } + // Fim da captura. + else if (exposureCount == 0) { + state = State.VERIFY_DATA_IS_ENOUGTH + return AutoFocusResult.Determinate + } + } + State.VERIFY_DATA_IS_ENOUGTH -> { + LOG.debug("HFD measured after exposures. hfd={}, stdDev={}", measurement.hfd, measurement.stdDev) + + computeCurvePoint() + + if (maximumFocusPoints < focusPoints.size) { + // Break out when the maximum limit of focus points is reached + LOG.error("failed to complete. Maximum number of focus points exceeded ($maximumFocusPoints).") + return AutoFocusResult.Failed(initialFocusPosition) + } + + if (focusPosition <= 0 || focusPosition >= focusMaxPosition) { + // Break out when the focuser hits the min/max position. It can't continue from there. + LOG.error("failed to complete. position reached {}", focusMaxPosition) + return AutoFocusResult.Failed(initialFocusPosition) + } + + state = if (isDataPointsEnough) { + State.DETERMINATE_FINAL_FOCUS_POINT + } else { + subState = SubState.IDLE + State.EVALUATE_TREND_LINE_POINTS + } + + return AutoFocusResult.Determinate + } + State.DETERMINATE_FINAL_FOCUS_POINT -> { + val finalFocusPoint = determineFinalFocusPoint() + + return if (finalFocusPoint == null || !validateCalculatedFocusPosition(finalFocusPoint)) { + LOG.warn("potentially bad auto-focus. Restoring original focus position") + AutoFocusResult.Failed(initialFocusPosition) + } else { + determinedFocusPoint = finalFocusPoint + LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y) + AutoFocusResult.Completed(finalFocusPoint) + } + } + } + + LOG.warn("invalid state. state={}, subState={}, exposureCount={}", state, subState, exposureCount) + + throw IllegalStateException("auto focus has reached an invalid state") + } + + private fun moveFocuser(position: Int, relative: Boolean): AutoFocusResult { + subState = SubState.MOVE_FOCUSER + LOG.debug("moving focuser. position={}, relative={}", position, relative) + return AutoFocusResult.MoveFocuser(position, relative) + } + + private fun takeExposure(): AutoFocusResult { + // Inicia-se a sequência de capturas. + if (exposureCount <= 0) { + measurements.clear() + exposureCount = exposureAmount + } + + subState = SubState.TAKE_EXPOSURE + LOG.debug("taking exposure. exposureCount={}", exposureCount) + return AutoFocusResult.TakeExposure + } + + private fun measureStars(path: Path): MeasuredStars { + val detectedStars = starDetector.detect(path) + LOG.debug("detected {} stars", detectedStars.size) + val measurement = detectedStars.measureDetectedStars() + LOG.debug("HFD measured. hfd={}, stdDev={}", measurement.hfd, measurement.stdDev) + measurements.add(measurement) + return measurement + } + + private fun evaluateAllMeasurements(): MeasuredStars { + if (measurements.isEmpty()) MeasuredStars.EMPTY + if (measurements.size == 1) return measurements[0] + val descriptiveStatistics = DescriptiveStatistics(measurements.size) + measurements.forEach { descriptiveStatistics.addValue(it.hfd) } + val stdDev = descriptiveStatistics.standardDeviation + return MeasuredStars(descriptiveStatistics.mean, if (stdDev > 0.0) stdDev else 1.0) + } + + private fun computeCurvePoint(): CurvePoint? { + return if (measurement.hfd == 0.0) { + LOG.warn("no stars detected in step") + null + } else { + val focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement.hfd, measurement.stdDev) + focusPoints.add(focusPoint) + focusPoints.sortBy { it.x } + + LOG.debug("focus point added. remainingSteps={}, point={}", remainingSteps, focusPoint) + + computeCurveFittings() + + focusPoint + } + } + + private fun computeCurveFittings() { + with(focusPoints) { + trendLineCurve = TrendLineFitting.calculate(this) + + if (size >= 3) { + if (fittingMode == AutoFocusFittingMode.PARABOLIC || fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) { + parabolicCurve = QuadraticFitting.calculate(this) + } else if (fittingMode == AutoFocusFittingMode.HYPERBOLIC || fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) { + hyperbolicCurve = HyperbolicFitting.calculate(this) + } + } + + val predictedFocusPoint = determinedFocusPoint ?: determineFinalFocusPoint() + val (minX, minY) = if (isEmpty()) CurvePoint.ZERO else first() + val (maxX, maxY) = if (isEmpty()) CurvePoint.ZERO else last() + // status.chart = AutoFocusEvent.Chart(predictedFocusPoint, minX, minY, maxX, maxY, trendLineCurve, parabolicCurve, hyperbolicCurve) + + // status.state = AutoFocusState.CURVE_FITTED + } + } + + private fun determineFinalFocusPoint(): CurvePoint? { + return when (fittingMode) { + AutoFocusFittingMode.TRENDLINES -> trendLineCurve?.intersection + AutoFocusFittingMode.PARABOLIC -> parabolicCurve?.minimum + AutoFocusFittingMode.TREND_PARABOLIC -> parabolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection) + AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve?.minimum + AutoFocusFittingMode.TREND_HYPERBOLIC -> hyperbolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection) + } + } + + private fun validateCalculatedFocusPosition(focusPoint: CurvePoint): Boolean { + LOG.info("validating calculated focus position. threshold={}", rSquaredThreshold) + + if (rSquaredThreshold > 0.0) { + fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < rSquaredThreshold || it.right.rSquared < rSquaredThreshold } != false + fun isParabolicBad() = parabolicCurve?.let { it.rSquared < rSquaredThreshold } != false + fun isHyperbolicBad() = hyperbolicCurve?.let { it.rSquared < rSquaredThreshold } != false + + val isBad = when (fittingMode) { + AutoFocusFittingMode.TRENDLINES -> isTrendLineBad() + AutoFocusFittingMode.PARABOLIC -> isParabolicBad() + AutoFocusFittingMode.TREND_PARABOLIC -> isParabolicBad() || isTrendLineBad() + AutoFocusFittingMode.HYPERBOLIC -> isHyperbolicBad() + AutoFocusFittingMode.TREND_HYPERBOLIC -> isHyperbolicBad() || isTrendLineBad() + } + + if (isBad) { + LOG.error("coefficient of determination is below threshold") + return false + } + } + + val min = focusPoints.first().x + val max = focusPoints.last().x + + 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 + } + + return true + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + + private fun List.measureDetectedStars(): MeasuredStars { + if (isEmpty()) return MeasuredStars.EMPTY + val descriptiveStatistics = DescriptiveStatistics(size) + forEach { descriptiveStatistics.addValue(it.hfd) } + val stdDev = descriptiveStatistics.standardDeviation + return MeasuredStars(descriptiveStatistics.mean, if (stdDev > 0.0) stdDev else 1.0) + } + } +} diff --git a/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusFittingMode.kt b/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusFittingMode.kt new file mode 100644 index 000000000..9eeb12f53 --- /dev/null +++ b/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusFittingMode.kt @@ -0,0 +1,9 @@ +package nebulosa.autofocus + +enum class AutoFocusFittingMode { + TRENDLINES, + PARABOLIC, + TREND_PARABOLIC, + HYPERBOLIC, + TREND_HYPERBOLIC +} diff --git a/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusResult.kt b/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusResult.kt new file mode 100644 index 000000000..bac40dc4f --- /dev/null +++ b/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusResult.kt @@ -0,0 +1,31 @@ +package nebulosa.autofocus + +import nebulosa.curve.fitting.CurvePoint + +sealed interface AutoFocusResult { + + /** + * Should take a exposure. + */ + data object TakeExposure : AutoFocusResult + + /** + * Should move the focuser to [position], [relative] or not. + */ + data class MoveFocuser(@JvmField val position: Int, @JvmField val relative: Boolean) : AutoFocusResult + + /** + * Should call [AutoFocus.determinate] method again. + */ + data object Determinate : AutoFocusResult + + /** + * Auto Focus can not be determinated because it failed. + */ + data class Failed(@JvmField val initialFocusPosition: Int) : AutoFocusResult + + /** + * Auto Focus finished with success. + */ + data class Completed(@JvmField val point: CurvePoint) : AutoFocusResult +} diff --git a/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/MeasuredStars.kt b/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/MeasuredStars.kt new file mode 100644 index 000000000..220a632b5 --- /dev/null +++ b/nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/MeasuredStars.kt @@ -0,0 +1,9 @@ +package nebulosa.autofocus + +data class MeasuredStars(@JvmField val hfd: Double, @JvmField val stdDev: Double) { + + companion object { + + @JvmStatic val EMPTY = MeasuredStars(0.0, 0.0) + } +} diff --git a/nebulosa-autofocus/src/test/kotlin/AutoFocusTest.kt b/nebulosa-autofocus/src/test/kotlin/AutoFocusTest.kt new file mode 100644 index 000000000..d95b03649 --- /dev/null +++ b/nebulosa-autofocus/src/test/kotlin/AutoFocusTest.kt @@ -0,0 +1,115 @@ +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.nulls.shouldNotBeNull +import nebulosa.autofocus.AutoFocus +import nebulosa.autofocus.AutoFocusFittingMode +import nebulosa.autofocus.AutoFocusFittingMode.* +import nebulosa.autofocus.AutoFocusResult +import nebulosa.curve.fitting.CurvePoint +import nebulosa.stardetector.StarDetector +import nebulosa.stardetector.StarPoint +import nebulosa.test.* +import org.junit.jupiter.api.Test +import java.nio.file.Path +import kotlin.math.cosh +import kotlin.math.ln +import kotlin.math.max +import kotlin.math.roundToInt + +class AutoFocusTest : AbstractTest() { + + @Test + fun trendHyperbolic() { + executeAutoFocus().shouldNotBeNull().x.roundToInt() shouldBeExactly 798 + executeAutoFocus(700).shouldNotBeNull().x.roundToInt() shouldBeExactly 800 + executeAutoFocus(1200).shouldNotBeNull().x.roundToInt() shouldBeExactly 798 + executeAutoFocus(exposureAmount = 3).shouldNotBeNull().x.roundToInt() shouldBeExactly 798 + } + + @Test + fun trendParabolic() { + executeAutoFocus(fittingMode = TREND_PARABOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 807 + executeAutoFocus(700, fittingMode = TREND_PARABOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 800 + executeAutoFocus(1200, fittingMode = TREND_PARABOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 805 + executeAutoFocus(exposureAmount = 3, fittingMode = TREND_PARABOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 807 + } + + @Test + fun trendlines() { + executeAutoFocus(fittingMode = TRENDLINES).shouldNotBeNull().x.roundToInt() shouldBeExactly 800 + executeAutoFocus(700, fittingMode = TRENDLINES).shouldNotBeNull().x.roundToInt() shouldBeExactly 800 + executeAutoFocus(1200, fittingMode = TRENDLINES).shouldNotBeNull().x.roundToInt() shouldBeExactly 800 + executeAutoFocus(exposureAmount = 3, fittingMode = TRENDLINES).shouldNotBeNull().x.roundToInt() shouldBeExactly 800 + } + + @Test + fun hyperbolic() { + executeAutoFocus(fittingMode = HYPERBOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 796 + executeAutoFocus(700, fittingMode = HYPERBOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 800 + executeAutoFocus(1200, fittingMode = HYPERBOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 795 + executeAutoFocus(exposureAmount = 3, fittingMode = HYPERBOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 796 + } + + @Test + fun parabolic() { + executeAutoFocus(fittingMode = PARABOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 814 + executeAutoFocus(700, fittingMode = PARABOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 800 + executeAutoFocus(1200, fittingMode = PARABOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 809 + executeAutoFocus(exposureAmount = 3, fittingMode = PARABOLIC).shouldNotBeNull().x.roundToInt() shouldBeExactly 814 + } + + private data class DetectedStar( + override val hfd: Double, + override val x: Double = 0.0, override val y: Double = 0.0, + override val snr: Double = 0.0, override val flux: Double = 0.0, + ) : StarPoint + + private data class HyperbolicStarDetector(private val offset: Int = 8) : StarDetector { + + override fun detect(input: Path): List { + val index = STAR_FOCUS_LIST.indexOf(input) + val hfd = max(1.0, ln(cosh(((index - offset) * 2).toDouble()))) + return listOf(DetectedStar(hfd)) + } + } + + companion object { + + @JvmStatic val STAR_FOCUS_LIST = listOf( + STAR_FOCUS_1, STAR_FOCUS_2, STAR_FOCUS_3, STAR_FOCUS_4, STAR_FOCUS_5, + STAR_FOCUS_6, STAR_FOCUS_7, STAR_FOCUS_8, STAR_FOCUS_9, STAR_FOCUS_10, + STAR_FOCUS_11, STAR_FOCUS_12, STAR_FOCUS_13, STAR_FOCUS_14, STAR_FOCUS_15, + STAR_FOCUS_16, STAR_FOCUS_17, + ) + + @JvmStatic + private fun executeAutoFocus( + initialFocusPosition: Int = 1000, + exposureAmount: Int = 1, + initialOffsetSteps: Int = 4, + stepSize: Int = 100, + fittingMode: AutoFocusFittingMode = TREND_HYPERBOLIC, + rSquaredThreshold: Double = 0.0, + reverse: Boolean = false, + ): CurvePoint? { + val autoFocus = + AutoFocus(HyperbolicStarDetector(), exposureAmount, initialOffsetSteps, stepSize, fittingMode, rSquaredThreshold, reverse, 16000) + + var focusPosition = initialFocusPosition + var focusPoint: CurvePoint? = null + + while (focusPoint == null) { + when (val result = autoFocus.determinate(focusPosition)) { + AutoFocusResult.Determinate -> continue + is AutoFocusResult.MoveFocuser -> focusPosition = if (result.relative) focusPosition + result.position else result.position + AutoFocusResult.TakeExposure -> autoFocus.add(STAR_FOCUS_LIST[focusPosition / 100]) + is AutoFocusResult.Completed -> focusPoint = result.point + is AutoFocusResult.Failed -> break + } + + // Thread.sleep(100) + } + + return focusPoint + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8cad64316..9bf125d99 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -56,6 +56,7 @@ include(":nebulosa-astap") include(":nebulosa-astrobin-api") include(":nebulosa-astrometrynet") include(":nebulosa-astrometrynet-jna") +include(":nebulosa-autofocus") include(":nebulosa-constants") include(":nebulosa-curve-fitting") include(":nebulosa-erfa") From e34dfb8b455c4ce8c32d954e93a94e66ca226ff8 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 10 Oct 2024 19:04:44 -0300 Subject: [PATCH 2/2] [api][desktop]: Implement Auto Focus Job --- api/Main.run.xml | 16 +- api/build.gradle.kts | 1 + api/src/main/kotlin/nebulosa/api/Nebulosa.kt | 21 +- .../nebulosa/api/atlas/IERSUpdateTask.kt | 6 +- .../api/autofocus/AutoFocusController.kt | 3 +- .../api/autofocus/AutoFocusFittingMode.kt | 9 - .../nebulosa/api/autofocus/AutoFocusJob.kt | 458 +++++------------- .../api/autofocus/AutoFocusRequest.kt | 17 +- .../api/cameras/CameraExposureTask.kt | 2 +- .../api/focusers/AbstractFocuserMoveTask.kt | 9 +- .../BacklashCompensationFocuserMoveTask.kt | 22 +- .../kotlin/nebulosa/api/javalin/Headers.kt | 6 +- .../javalin/{DatesAndTimes.kt => Queries.kt} | 1 - .../nebulosa/api/javalin/Validatable.kt | 6 + .../kotlin/nebulosa/api/javalin/Validators.kt | 7 +- .../app/autofocus/autofocus.component.html | 15 +- .../src/app/autofocus/autofocus.component.ts | 33 +- .../src/shared/services/preference.service.ts | 4 +- desktop/src/shared/types/autofocus.type.ts | 3 - .../alpaca/indi/device/ASCOMDevice.kt | 4 +- .../alpaca/indi/device/cameras/ASCOMCamera.kt | 2 +- .../astap/stardetector/AstapStarDetector.kt | 2 +- .../kotlin/nebulosa/autofocus/AutoFocus.kt | 30 +- .../nebulosa/autofocus/AutoFocusListener.kt | 16 + .../nebulosa/autofocus/AutoFocusResult.kt | 2 +- .../nebulosa/autofocus/MeasuredStars.kt | 8 +- .../src/test/kotlin/AutoFocusTest.kt | 2 +- .../main/kotlin/nebulosa/fits/FitsHeader.kt | 2 +- 28 files changed, 271 insertions(+), 436 deletions(-) delete mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusFittingMode.kt rename api/src/main/kotlin/nebulosa/api/javalin/{DatesAndTimes.kt => Queries.kt} (99%) create mode 100644 api/src/main/kotlin/nebulosa/api/javalin/Validatable.kt create mode 100644 nebulosa-autofocus/src/main/kotlin/nebulosa/autofocus/AutoFocusListener.kt diff --git a/api/Main.run.xml b/api/Main.run.xml index 7968b5ad8..2d1a2e146 100644 --- a/api/Main.run.xml +++ b/api/Main.run.xml @@ -1,19 +1,11 @@ - -