Git commit 13ad15bc469522485ae9e53e0b322060d25853cd by Wolfgang Reissenberger.
Committed on 15/05/2023 at 11:23.
Pushed by wreissenberger into branch 'master'.

Aberration Inspector for Focusing

This change introduces the mosaic view well known from PixInsight's 
AberrationInspector script that builds a mosaic from all image corners and 
center tiles such that they can be compared directly.

M  +40   -23   doc/ekos-focus.docbook
M  +-    --    doc/ekos_focus.png
M  +-    --    doc/focus_settings.png
M  +1    -0    kstars/CMakeLists.txt
A  +174  -0    kstars/auxiliary/imagemask.cpp     [License: GPL(v2.0+)]
A  +150  -0    kstars/auxiliary/imagemask.h     [License: GPL(v2.0+)]
M  +168  -50   kstars/ekos/focus/focus.cpp
M  +20   -1    kstars/ekos/focus/focus.h
M  +197  -123  kstars/ekos/focus/focus.ui
M  +11   -21   kstars/ekos/focus/focusfourierpower.h
M  +9    -12   kstars/fitsviewer/fitsdata.cpp
M  +3    -2    kstars/fitsviewer/fitsdata.h
M  +69   -26   kstars/fitsviewer/fitsview.cpp
M  +9    -11   kstars/fitsviewer/fitsview.h
M  +16   -5    kstars/kstars.kcfg

https://invent.kde.org/education/kstars/commit/13ad15bc469522485ae9e53e0b322060d25853cd

diff --git a/doc/ekos-focus.docbook b/doc/ekos-focus.docbook
old mode 100644
new mode 100755
index 15be57eb45..2879607e9c
--- a/doc/ekos-focus.docbook
+++ b/doc/ekos-focus.docbook
@@ -483,6 +483,15 @@
         to manually select a star.</para>
       </listitem>
 
+      <listitem>
+        <para> <guilabel>Suspend Guiding</guilabel>: Set this option to
+        suspend guiding during an Autofocus run. An additional check is made
+        to only suspend guiding if it is being carried out via the primary
+        scope, for example when using an Off Axis Guider. The purpose of this
+        is to prevent guiding from having problems with defocused stars during
+        the focus process.</para>
+      </listitem>
+
       <listitem>
         <para> <guilabel>Dark Frame</guilabel>: Check this option to perform
         dark-frame subtraction. This option can be useful in noisy images,
@@ -562,29 +571,37 @@
         <para> <guilabel>Full Field</guilabel>: Either use the full field of
         the camera or select a Sub Frame around the focus star during the
         Autofocus procedure. If using multiple stars then this option must be
-        selected.</para>
-      </listitem>
-
-      <listitem>
-        <para> <guilabel>Annulus</guilabel>: Used in conjunction with Full
-        Field, Annulus provides two input fields that together define a
-        doughnut over the FOV of the camera. Stars falling outside of the
-        doughnut are discounted from processing. Setting an inner value above
-        0% causes stars in the centre of the FOV to be discarded. This could
-        be useful to avoid using stars in the target of the image (for example
-        a galaxy) for focusing purposes. Setting an outer value below 100%
-        causes stars in the edges of the FOV to be discarded during focusing.
-        This could be useful if you do not have a flat field out to the edges
-        of your FOV.</para>
-      </listitem>
-
-      <listitem>
-        <para> <guilabel>Suspend Guiding</guilabel>: Set this option to
-        suspend guiding during an Autofocus run. An additional check is made
-        to only suspend guiding if it is being carried out via the primary
-        scope, for example when using an Off Axis Guider. The purpose of this
-        is to prevent guiding from having problems with defocused stars during
-        the focus process.</para>
+          selected.
+         <itemizedlist>
+           <listitem>
+             <para> <guilabel>Use all stars for focusing</guilabel>: Select 
this option
+               if all stars of the field should be considered for 
focusing.</para>
+           </listitem>
+           
+           <listitem>
+              <para> <guilabel>Ring Mask</guilabel>: Used in conjunction with 
Full
+               Field, this option provides two input fields that together 
define a
+               doughnut over the FOV of the camera. Stars falling outside of 
the
+               doughnut are discounted from processing. Setting an inner value 
above
+               0% causes stars in the centre of the FOV to be discarded. This 
could
+               be useful to avoid using stars in the target of the image (for 
example
+               a galaxy) for focusing purposes. Setting an outer value below 
100%
+               causes stars in the edges of the FOV to be discarded during 
focusing.
+               This could be useful if you do not have a flat field out to the 
edges
+               of your FOV.</para>
+           </listitem>
+
+           <listitem>
+             <para> <guilabel>Mosaic Mask</guilabel>: A 3x3 mosaic is composed 
with tiles
+               from the imagecenter, its corners and from the edges. This 
option is useful
+               if you want to inspect the optics performance - you might know 
this from the
+               PixInsight Aberration Inspector script. The tile size can be 
configured in
+               percent of the frame width, with the spacer value the space 
between the tiles
+               is defined.</para>
+           </listitem>
+           
+         </itemizedlist>
+       </para>
       </listitem>
 
       <listitem>
diff --git a/doc/ekos_focus.png b/doc/ekos_focus.png
old mode 100644
new mode 100755
index 8cbdd3c256..d221baa30d
Binary files a/doc/ekos_focus.png and b/doc/ekos_focus.png differ
diff --git a/doc/focus_settings.png b/doc/focus_settings.png
old mode 100644
new mode 100755
index 7f6800240a..6d4d0c1e6c
Binary files a/doc/focus_settings.png and b/doc/focus_settings.png differ
diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt
index 6a7fa668eb..befe411118 100644
--- a/kstars/CMakeLists.txt
+++ b/kstars/CMakeLists.txt
@@ -855,6 +855,7 @@ set(kstars_extra_SRCS
 SET(kstars_extra_kstars_SRCS
     auxiliary/thememanager.cpp
     auxiliary/schememanager.cpp
+    auxiliary/imagemask.cpp
     auxiliary/imageviewer.cpp
     auxiliary/xplanetimageviewer.cpp
     auxiliary/fov.cpp
diff --git a/kstars/auxiliary/imagemask.cpp b/kstars/auxiliary/imagemask.cpp
new file mode 100644
index 0000000000..7db1423436
--- /dev/null
+++ b/kstars/auxiliary/imagemask.cpp
@@ -0,0 +1,174 @@
+/*
+    SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger 
<sterne-jae...@openfuture.de>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "imagemask.h"
+#include "cmath"
+
+ImageMask::ImageMask(const uint16_t width, const uint16_t height)
+{
+    m_width  = width;
+    m_height = height;
+}
+
+void ImageMask::setImageGeometry(const uint16_t width, const uint16_t height)
+{
+    m_width  = width;
+    m_height = height;
+    refresh();
+}
+
+bool ImageMask::isVisible(uint16_t posX, uint16_t posY)
+{
+    return posX >= 0 && posX < m_width && posY >= 0 && posY < m_height;
+}
+
+uint16_t ImageMask::width() const
+{
+    return m_width;
+}
+
+uint16_t ImageMask::height() const
+{
+    return m_height;
+}
+
+ImageRingMask::ImageRingMask(const float innerRadius, const float outerRadius, 
const uint16_t width,
+                             const uint16_t height) : ImageMask(width, height)
+{
+    m_innerRadius = innerRadius;
+    m_outerRadius = outerRadius;
+}
+
+void ImageRingMask::refresh()
+{
+    long const sqDiagonal = (long) (m_width * m_width / 4 + m_height * 
m_height / 4);
+    m_InnerRadiusSquare = std::lround(sqDiagonal * innerRadius() * 
innerRadius());
+    m_OuterRadiusSquare = std::lround(sqDiagonal * outerRadius() * 
outerRadius());
+}
+
+bool ImageRingMask::isVisible(uint16_t posX, uint16_t posY)
+{
+    bool result = ImageMask::isVisible(posX, posY);
+    // outside of the image?
+    if (result == false)
+        return false;
+
+    double const x = posX - m_width / 2;
+    double const y = posY - m_height / 2;
+
+    double const sqRadius = x * x + y * y;
+
+    return sqRadius >= m_InnerRadiusSquare && sqRadius < m_OuterRadiusSquare;
+}
+
+float ImageRingMask::innerRadius() const
+{
+    return m_innerRadius;
+}
+
+void ImageRingMask::setInnerRadius(float newInnerRadius)
+{
+    m_innerRadius = newInnerRadius;
+    refresh();
+}
+
+float ImageRingMask::outerRadius() const
+{
+    return m_outerRadius;
+}
+
+void ImageRingMask::setOuterRadius(float newOuterRadius)
+{
+    m_outerRadius = newOuterRadius;
+    refresh();
+}
+
+ImageMosaicMask::ImageMosaicMask(const uint16_t tileWidth, const uint16_t 
space, const uint16_t width,
+                                 const uint16_t height) : ImageMask(width, 
height)
+{
+    m_tileWidth = tileWidth;
+    m_space = space;
+    // initialize the tiles
+    for (int i = 0; i < 9; i++)
+        m_tiles.append(QRect());
+}
+
+bool ImageMosaicMask::isVisible(uint16_t posX, uint16_t posY)
+{
+    for (auto it = m_tiles.begin(); it != m_tiles.end(); it++)
+        if (it->contains(posX, posY))
+            return true;
+    // no matching tile found
+    return false;
+}
+
+QPointF ImageMosaicMask::translate(QPointF original)
+{
+    int pos = 0;
+    const uint16_t tileWidth = std::lround(m_width * m_tileWidth / 100);
+    const float spacex = (m_width - 3 * tileWidth - 2 * m_space) / 2;
+    const float spacey = (m_height - 3 * tileWidth - 2 * m_space) / 2;
+    for (QRect tile : m_tiles)
+    {
+        // matrix tile position
+        int posx = pos % 3;
+        int posy = pos++ / 3;
+        if (tile.contains(original.x(), original.y()))
+            return QPointF(original.x() - posx * spacex, original.y() - posy * 
spacey);
+    }
+    // this should not happen for filtered positions
+    return QPointF(-1, -1);
+}
+
+const QVector<QRect> ImageMosaicMask::tiles()
+{
+    return m_tiles;
+}
+
+float ImageMosaicMask::tileWidth() const
+{
+    return m_tileWidth;
+}
+
+void ImageMosaicMask::setTileWidth(float newTileWidth)
+{
+    m_tileWidth = newTileWidth;
+    refresh();
+}
+
+uint16_t ImageMosaicMask::space() const
+{
+    return m_space;
+}
+
+void ImageMosaicMask::setSpace(uint16_t newSpace)
+{
+    m_space = newSpace;
+    refresh();
+}
+
+void ImageMosaicMask::refresh()
+{
+    m_tiles.clear();
+    uint16_t tileWidth = std::lround(m_width * m_tileWidth / 100);
+    if (m_width > 0 && m_height > 0)
+    {
+        const u_int16_t x1 = std::lround((m_width - tileWidth) / 2);
+        const u_int16_t x2 = m_width - tileWidth - 1;
+        const u_int16_t y1 = std::lround((m_height - tileWidth) / 2);
+        const u_int16_t y2 = m_height - tileWidth - 1;
+
+        m_tiles.append(QRect(0, 0, tileWidth, tileWidth));
+        m_tiles.append(QRect(x1, 0, tileWidth, tileWidth));
+        m_tiles.append(QRect(x2, 0, tileWidth, tileWidth));
+        m_tiles.append(QRect(0, y1, tileWidth, tileWidth));
+        m_tiles.append(QRect(x1, y1, tileWidth, tileWidth));
+        m_tiles.append(QRect(x2, y1, tileWidth, tileWidth));
+        m_tiles.append(QRect(0, y2, tileWidth, tileWidth));
+        m_tiles.append(QRect(x1, y2, tileWidth, tileWidth));
+        m_tiles.append(QRect(x2, y2, tileWidth, tileWidth));
+    }
+}
diff --git a/kstars/auxiliary/imagemask.h b/kstars/auxiliary/imagemask.h
new file mode 100644
index 0000000000..ecd0edd356
--- /dev/null
+++ b/kstars/auxiliary/imagemask.h
@@ -0,0 +1,150 @@
+/*
+    SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger 
<sterne-jae...@openfuture.de>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+
+#pragma once
+
+#include <stdint.h>
+#include <QRect>
+#include <QVector>
+
+class ImageMask
+{
+public:
+    /**
+     * @brief ImageMask Create an image mask with image dimensions
+     * @param width image width
+     * @param height image height
+     */
+    ImageMask(const uint16_t width=0, const uint16_t height=0);
+    virtual ~ImageMask(){};
+
+    /**
+     * @brief setImageGeometry set the image geometry
+     * @param width image width
+     * @param height image height
+     */
+    virtual void setImageGeometry(const uint16_t width, const uint16_t height);
+
+    /**
+     * @brief isVisible test if the given position is visible
+     * @return in this basic version, a point is visible as long as it lies
+     * within the image dimensions
+     */
+    virtual bool isVisible(uint16_t posX, uint16_t posY);
+
+    /**
+     * @brief translate which position on the frame has a point after masking
+     * @param original point to be translated
+     * @return same position as on the original image
+     */
+    QPointF translate(QPointF original) { return original; }
+
+    /**
+     * @brief active is the mask active (pure virtual)
+     */
+    virtual bool active() = 0;
+
+    uint16_t width() const;
+
+    uint16_t height() const;
+
+protected:
+    uint16_t m_width, m_height;
+
+    virtual void refresh() = 0;
+};
+
+class ImageRingMask : public ImageMask
+{
+public:
+    /**
+     * @brief ImageRingMask Create an image mask with a ring form
+     * @param innerRadius inner ring radius in percent
+     * @param outerRadius outer ring radius in percent
+     * @param width image width
+     * @param height image height
+     */
+    ImageRingMask(const float innerRadius=0, const float outerRadius=1, const 
uint16_t width=0, const uint16_t height=0);
+
+    virtual ~ImageRingMask() {};
+
+    /**
+     * @brief isVisible test if the given position is visible
+     * @return check if the position is inside the outer radius and outside 
the inner radius
+     */
+    bool isVisible(uint16_t posX, uint16_t posY) override;
+
+    /**
+     * @brief active The mask is active if either the inner radius > 0% or the 
outer radius < 100%
+     */
+    bool active() override { return m_width > 0 && m_height > 0 && 
(m_innerRadius > 0 || m_outerRadius < 1.0); };
+
+    float innerRadius() const;
+    void setInnerRadius(float newInnerRadius);
+
+    float outerRadius() const;
+    void setOuterRadius(float newOuterRadius);
+
+protected:
+    // mask radius in percent of image diagonal
+    float m_innerRadius {0};
+    float m_outerRadius {1.0};
+    // cached values for fast visibility calculation
+    long m_InnerRadiusSquare, m_OuterRadiusSquare;
+    // re-calculate the cached squares
+    virtual void refresh() override;
+};
+
+class ImageMosaicMask : public ImageMask
+{
+public:
+    /**
+     * @brief ImageMosaicMask Creates a 3x3 mosaic mask
+     * @param tileWidth width of a single mosaic tile (in percent of the image 
width)
+     * @param space space between the mosaic tiles
+     * @param width source image width
+     * @param height source image height
+     */
+    ImageMosaicMask (const uint16_t tileWidth, const uint16_t space, const 
uint16_t width=0, const uint16_t height=0);
+
+    /**
+     * @brief isVisible test if the given position is visible
+     * @return check if the position is inside one of the tiles
+     */
+    virtual bool isVisible(uint16_t posX, uint16_t posY) override;
+
+    /**
+     * @brief translate Calculate the new position of a point inside of the 
mosaic
+     * @param original original position
+     * @return position inside the mosaic
+     */
+    QPointF translate(QPointF original);
+
+    /**
+     * @brief active The mask is active if the mosaic raster is present
+     */
+    bool active() override { return m_width > 0 && m_height > 0 && 
m_tiles.length() > 0; };
+
+    const QVector<QRect> tiles();
+
+    float tileWidth() const;
+    void setTileWidth(float newTileWidth);
+
+    uint16_t space() const;
+    void setSpace(uint16_t newSpace);
+
+
+protected:
+    // width of a single mosaic tile (in percent of the image width)
+    float m_tileWidth;
+    // space between the mosaic tiles
+    uint16_t m_space;
+    // 3x3 mosaic raster, sorted linewise top down
+    QVector<QRect> m_tiles;
+    // re-calculate the tiles
+    virtual void refresh() override;
+};
diff --git a/kstars/ekos/focus/focus.cpp b/kstars/ekos/focus/focus.cpp
index 42baf2b19f..d53716d076 100644
--- a/kstars/ekos/focus/focus.cpp
+++ b/kstars/ekos/focus/focus.cpp
@@ -91,7 +91,7 @@ Focus::Focus()
     // Display on screen the first tab in the tab widget
     tabWidget->setCurrentIndex(0);
 
-    connect(&m_StarFinderWatcher, &QFutureWatcher<bool>::finished, this, 
&Focus::calculateHFR);
+    connect(&m_StarFinderWatcher, &QFutureWatcher<bool>::finished, this, 
&Focus::starDetectionFinished);
 
     //Note:  This is to prevent a button from being called the default button
     //and then executing when the user hits the enter key such as when on a 
Text Box
@@ -256,6 +256,7 @@ void Focus::resetFrame()
             subFramed    = false;
 
             m_FocusView->setTrackingBox(QRect());
+            checkMosaicMaskLimits();
         }
     }
 }
@@ -785,6 +786,7 @@ bool Focus::setCamera(ISD::Camera *device)
     resetFrame();
 
     checkCamera();
+    checkMosaicMaskLimits();
     return true;
 }
 
@@ -1195,7 +1197,11 @@ void Focus::start()
                                << " Sub Frame:" << ( 
focusSubFrame->isChecked() ? "yes" : "no" )
                                << " Box:" << focusBoxSize->value()
                                << " Full frame:" << ( 
focusUseFullField->isChecked() ? "yes" : "no " )
-                               << " Annulus: [" << 
focusFullFieldInnerRadius->value() << "%," << 
focusFullFieldOuterRadius->value() << "%]"
+                               << " Focus Mask: " << 
(focusNoMaskRB->isChecked() ? "Use all stars" :
+                                       (focusRingMaskRB->isChecked() ? 
QString("Ring Mask: [%1%, %2%]").
+                                        
arg(focusFullFieldInnerRadius->value()).arg(focusFullFieldOuterRadius->value()) 
:
+                                        QString("Mosaic Mask: [%1%, 
space=%2px]").
+                                        
arg(focusMosaicTileWidth->value()).arg(focusMosaicSpace->value())))
                                << " Suspend Guiding:" << ( 
focusSuspendGuiding->isChecked() ? "yes" : "no " )
                                << " Guide Settle:" << guideSettleTime->value()
                                << " Display Units:" << 
focusUnits->currentText()
@@ -1862,6 +1868,53 @@ void Focus::handleFocusMotionTimeout()
         appendLogText(i18n("Focus motion timed out (%1). Focusing to %2 
steps...", m_FocusMotionTimerCounter, m_LastFocusSteps));
 }
 
+void Focus::selectImageMask(const ImageMaskType newMaskType)
+{
+    const bool useFullField = focusUseFullField->isChecked();
+    // mask selection only enabled if full field should be used for focusing
+    focusRingMaskRB->setEnabled(useFullField);
+    focusMosaicMaskRB->setEnabled(useFullField);
+    // ring mask
+    focusFullFieldInnerRadius->setEnabled(useFullField && newMaskType == 
FOCUS_MASK_RING);
+    focusFullFieldOuterRadius->setEnabled(useFullField && newMaskType == 
FOCUS_MASK_RING);
+    // aberration inspector mosaic
+    focusMosaicTileWidth->setEnabled(useFullField && newMaskType == 
FOCUS_MASK_MOSAIC);
+    focusSpacerLabel->setEnabled(useFullField && newMaskType == 
FOCUS_MASK_MOSAIC);
+    focusMosaicSpace->setEnabled(useFullField && newMaskType == 
FOCUS_MASK_MOSAIC);
+
+    // create the type specific mask
+    if (newMaskType == FOCUS_MASK_RING)
+        m_FocusView->setImageMask(new 
ImageRingMask(Options::focusFullFieldInnerRadius() / 100.0,
+                                  Options::focusFullFieldOuterRadius() / 
100.0));
+    else if (newMaskType == FOCUS_MASK_MOSAIC)
+        m_FocusView->setImageMask(new 
ImageMosaicMask(Options::focusMosaicTileWidth(), Options::focusMosaicSpace()));
+    else
+        m_FocusView->setImageMask(nullptr);
+
+    checkMosaicMaskLimits();
+    m_currentImageMask = newMaskType;
+}
+
+void Focus::syncImageMaskSelection()
+{
+    QRadioButton *rb = nullptr;
+    if ( (rb = qobject_cast<QRadioButton*>(sender())) && rb->isChecked())
+    {
+        const QString name = rb->objectName();
+        ImageMaskType mask = FOCUS_MASK_NONE;
+
+        if (name == "focusRingMaskRB")
+            mask = FOCUS_MASK_RING;
+        else if (name == "focusMosaicMaskRB")
+            mask = FOCUS_MASK_MOSAIC;
+
+        Options::setFocusMaskType(mask);
+        selectImageMask(mask);
+    }
+}
+
+
+
 void Focus::reconnectFocuser(const QString &focuser)
 {
     m_FocuserReconnectCounter++;
@@ -1924,7 +1977,7 @@ void Focus::processData(const QSharedPointer<FITSData> 
&data)
     resetButtons();
 }
 
-void Focus::calculateHFR()
+void Focus::starDetectionFinished()
 {
     appendLogText(i18n("Detection complete."));
 
@@ -1939,8 +1992,6 @@ void Focus::calculateHFR()
     {
         if (focusUseFullField->isChecked())
         {
-            m_FocusView->setStarFilterRange(static_cast <float> 
(focusFullFieldInnerRadius->value() / 100.0),
-                                            static_cast <float> 
(focusFullFieldOuterRadius->value() / 100.0));
             m_FocusView->filterStars();
 
             // Get the average HFR of the whole frame
@@ -2057,50 +2108,48 @@ void Focus::getFourierPower(double *fourierPower, 
double *weight)
     *weight = 1.0;
 
     auto imageBuffer = m_ImageData->getImageBuffer();
-    QPair<double, double> filter;
-    filter.first = focusFullFieldInnerRadius->value();
-    filter.second = focusFullFieldOuterRadius->value();
 
     switch (m_ImageData->getStatistics().dataType)
     {
         case TBYTE:
-            focusFourierPower->processFourierPower(reinterpret_cast<uint8_t 
const *>(imageBuffer), m_ImageData, filter, fourierPower,
-                                                   weight);
+            focusFourierPower->processFourierPower(reinterpret_cast<uint8_t 
const *>(imageBuffer), m_ImageData,
+                                                   m_FocusView->imageMask(),
+                                                   fourierPower, weight);
             break;
 
         case TSHORT: // Don't think short is used as its recorded as unsigned 
short
-            focusFourierPower->processFourierPower(reinterpret_cast<short 
const *>(imageBuffer), m_ImageData, filter, fourierPower,
-                                                   weight);
+            focusFourierPower->processFourierPower(reinterpret_cast<short 
const *>(imageBuffer), m_ImageData, m_FocusView->imageMask(),
+                                                   fourierPower, weight);
             break;
 
         case TUSHORT:
-            focusFourierPower->processFourierPower(reinterpret_cast<unsigned 
short const *>(imageBuffer), m_ImageData, filter,
-                                                   fourierPower, weight);
+            focusFourierPower->processFourierPower(reinterpret_cast<unsigned 
short const *>(imageBuffer), m_ImageData,
+                                                   m_FocusView->imageMask(), 
fourierPower, weight);
             break;
 
         case TLONG:  // Don't think long is used as its recorded as unsigned 
long
-            focusFourierPower->processFourierPower(reinterpret_cast<long const 
*>(imageBuffer), m_ImageData, filter, fourierPower,
-                                                   weight);
+            focusFourierPower->processFourierPower(reinterpret_cast<long const 
*>(imageBuffer), m_ImageData, m_FocusView->imageMask(),
+                                                   fourierPower, weight);
             break;
 
         case TULONG:
-            focusFourierPower->processFourierPower(reinterpret_cast<unsigned 
long const *>(imageBuffer), m_ImageData, filter,
-                                                   fourierPower, weight);
+            focusFourierPower->processFourierPower(reinterpret_cast<unsigned 
long const *>(imageBuffer), m_ImageData,
+                                                   m_FocusView->imageMask(), 
fourierPower, weight);
             break;
 
         case TFLOAT:
-            focusFourierPower->processFourierPower(reinterpret_cast<float 
const *>(imageBuffer), m_ImageData, filter, fourierPower,
-                                                   weight);
+            focusFourierPower->processFourierPower(reinterpret_cast<float 
const *>(imageBuffer), m_ImageData, m_FocusView->imageMask(),
+                                                   fourierPower, weight);
             break;
 
         case TLONGLONG:
-            focusFourierPower->processFourierPower(reinterpret_cast<long long 
const *>(imageBuffer), m_ImageData, filter, fourierPower,
-                                                   weight);
+            focusFourierPower->processFourierPower(reinterpret_cast<long long 
const *>(imageBuffer), m_ImageData,
+                                                   m_FocusView->imageMask(), 
fourierPower, weight);
             break;
 
         case TDOUBLE:
-            focusFourierPower->processFourierPower(reinterpret_cast<double 
const *>(imageBuffer), m_ImageData, filter, fourierPower,
-                                                   weight);
+            focusFourierPower->processFourierPower(reinterpret_cast<double 
const *>(imageBuffer), m_ImageData, m_FocusView->imageMask(),
+                                                   fourierPower, weight);
             break;
 
         default:
@@ -2509,7 +2558,8 @@ void Focus::setCaptureComplete()
     }
 
     captureInProgress = false;
-
+    // update the limits from the real values
+    checkMosaicMaskLimits();
 
     // Emit the whole image
     emit newImage(m_FocusView);
@@ -4004,6 +4054,7 @@ void Focus::resetButtons()
     captureB->setEnabled(enableCaptureButtons);
     resetFrameB->setEnabled(enableCaptureButtons);
     startLoopB->setEnabled(enableCaptureButtons);
+    focusAutoStarEnabled->setEnabled(enableCaptureButtons && 
focusUseFullField->isChecked() == false);
 
     if (m_Focuser && m_Focuser->isConnected())
     {
@@ -4248,8 +4299,24 @@ void Focus::toggleSubframe(bool enable)
     starSelected = false;
     starCenter   = QVector3D();
 
-    if (focusUseFullField->isChecked())
-        focusUseFullField->setChecked(!enable);
+    if (enable)
+    {
+        // sub frame focusing
+        focusAutoStarEnabled->setEnabled(true);
+        // disable focus image mask
+        focusNoMaskRB->setChecked(true);
+    }
+    else
+    {
+        // full frame focusing
+        focusAutoStarEnabled->setChecked(false);
+        focusAutoStarEnabled->setEnabled(false);
+    }
+    // update image mask controls
+    selectImageMask(m_currentImageMask);
+    // enable focus mask selection if full field is selected
+    focusRingMaskRB->setEnabled(!enable);
+    focusMosaicMaskRB->setEnabled(!enable);
 
     setUseWeights();
 }
@@ -4268,6 +4335,7 @@ void Focus::setUseWeights()
     }
     else
         focusUseWeights->setEnabled(true);
+
 }
 
 void Focus::setExposure(double value)
@@ -4812,6 +4880,7 @@ void Focus::syncSettings()
     QDoubleSpinBox *dsb = nullptr;
     QSpinBox *sb = nullptr;
     QCheckBox *cb = nullptr;
+    QRadioButton *rb = nullptr;
     QComboBox *cbox = nullptr;
     QSplitter *s = nullptr;
 
@@ -4834,6 +4903,11 @@ void Focus::syncSettings()
         key = cb->objectName();
         value = cb->isChecked();
     }
+    else if ( (rb = qobject_cast<QRadioButton*>(sender())))
+    {
+        key = rb->objectName();
+        value = rb->isChecked();
+    }
     else if ( (cbox = qobject_cast<QComboBox*>(sender())))
     {
         key = cbox->objectName();
@@ -4858,6 +4932,20 @@ void Focus::syncSettings()
     // Save to optical train specific settings as well
     
OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
     
OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::Focus, 
m_Settings);
+
+    // propagate image mask attributes
+    ImageRingMask *ringmask     = dynamic_cast<ImageRingMask 
*>(m_FocusView->imageMask().get());
+    ImageMosaicMask *mosaicmask = dynamic_cast<ImageMosaicMask 
*>(m_FocusView->imageMask().get());
+    if (ringmask != nullptr)
+    {
+        ringmask->setInnerRadius(focusFullFieldInnerRadius->value() / 100.0);
+        ringmask->setOuterRadius(focusFullFieldOuterRadius->value() / 100.0);
+    }
+    else if (mosaicmask != nullptr)
+    {
+        mosaicmask->setTileWidth(focusMosaicTileWidth->value());
+        mosaicmask->setSpace(focusMosaicSpace->value());
+    }
 }
 
 void Focus::loadGlobalSettings()
@@ -4940,9 +5028,44 @@ void Focus::loadGlobalSettings()
         else
             qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!";
     }
+    // All Radio buttons
+    for (auto &oneWidget : findChildren<QRadioButton*>())
+    {
+        key = oneWidget->objectName();
+        value = Options::self()->property(key.toLatin1());
+        if (value.isValid())
+        {
+            oneWidget->setChecked(value.toBool());
+            settings[key] = value;
+        }
+    }
+    // select focus mask type
+    ImageMaskType masktype = 
static_cast<ImageMaskType>(Options::focusMaskType());
+    selectImageMask(masktype);
+    if (masktype == FOCUS_MASK_NONE)
+        focusNoMaskRB->setChecked(true);
+    else if (masktype == FOCUS_MASK_RING)
+        focusRingMaskRB->setChecked(true);
+    else
+        focusMosaicMaskRB->setChecked(true);
+
     m_GlobalSettings = m_Settings = settings;
 }
 
+void Focus::checkMosaicMaskLimits()
+{
+    if (m_Camera == nullptr || m_Camera->isConnected() == false)
+        return;
+    ISD::CameraChip *targetChip = 
m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
+    if (targetChip == nullptr || frameSettings.contains(targetChip) == false)
+        return;
+    QVariantMap settings = frameSettings[targetChip];
+    // determine maximal square size
+    int min = std::min(settings["w"].toInt(), settings["h"].toInt());
+    // now check if the tile size is below this limit
+    focusMosaicTileWidth->setMaximum(100 * min / (3 * settings["w"].toInt()));
+}
+
 void Focus::connectSettings()
 {
     // All Combo Boxes
@@ -4951,11 +5074,11 @@ void Focus::connectSettings()
 
     // All Double Spin Boxes
     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
-        connect(oneWidget, &QDoubleSpinBox::editingFinished, this, 
&Ekos::Focus::syncSettings);
+        connect(oneWidget, 
QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, 
&Ekos::Focus::syncSettings);
 
     // All Spin Boxes
     for (auto &oneWidget : findChildren<QSpinBox*>())
-        connect(oneWidget, &QSpinBox::editingFinished, this, 
&Ekos::Focus::syncSettings);
+        connect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, 
&Ekos::Focus::syncSettings);
 
     // All Checkboxes
     for (auto &oneWidget : findChildren<QCheckBox*>())
@@ -4964,6 +5087,10 @@ void Focus::connectSettings()
     // All Splitters
     for (auto &oneWidget : findChildren<QSplitter*>())
         connect(oneWidget, &QSplitter::splitterMoved, this, 
&Ekos::Focus::syncSettings);
+    // connect mask selections
+    connect(focusNoMaskRB, &QRadioButton::toggled, this, 
&Ekos::Focus::syncImageMaskSelection);
+    connect(focusRingMaskRB, &QRadioButton::toggled, this, 
&Ekos::Focus::syncImageMaskSelection);
+    connect(focusMosaicMaskRB, &QRadioButton::toggled, this, 
&Ekos::Focus::syncImageMaskSelection);
 
     // Train combo box should NOT be synced.
     disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), 
this, &Ekos::Focus::syncSettings);
@@ -4977,11 +5104,11 @@ void Focus::disconnectSettings()
 
     // All Double Spin Boxes
     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
-        disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, 
&Ekos::Focus::syncSettings);
+        disconnect(oneWidget, 
QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, 
&Ekos::Focus::syncSettings);
 
     // All Spin Boxes
     for (auto &oneWidget : findChildren<QSpinBox*>())
-        disconnect(oneWidget, &QSpinBox::editingFinished, this, 
&Ekos::Focus::syncSettings);
+        disconnect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), 
this, &Ekos::Focus::syncSettings);
 
     // All Checkboxes
     for (auto &oneWidget : findChildren<QCheckBox*>())
@@ -4990,6 +5117,10 @@ void Focus::disconnectSettings()
     // All Splitters
     for (auto &oneWidget : findChildren<QSplitter*>())
         disconnect(oneWidget, &QSplitter::splitterMoved, this, 
&Ekos::Focus::syncSettings);
+    // All Radio Buttons
+    disconnect(focusNoMaskRB, &QRadioButton::toggled, this, 
&Ekos::Focus::syncImageMaskSelection);
+    disconnect(focusRingMaskRB, &QRadioButton::toggled, this, 
&Ekos::Focus::syncImageMaskSelection);
+    disconnect(focusMosaicMaskRB, &QRadioButton::toggled, this, 
&Ekos::Focus::syncImageMaskSelection);
 
 }
 
@@ -5057,26 +5188,12 @@ void Focus::initConnections()
     // Start continuous capture
     connect(startLoopB, &QPushButton::clicked, this, 
&Ekos::Focus::startFraming);
     // Use a subframe when capturing
-    connect(focusSubFrame, &QCheckBox::toggled, this, 
&Ekos::Focus::toggleSubframe);
+    connect(focusSubFrame, &QRadioButton::toggled, this, 
&Ekos::Focus::toggleSubframe);
     // Reset frame dimensions to default
     connect(resetFrameB, &QPushButton::clicked, this, 
&Ekos::Focus::resetFrame);
-    // Sync setting if full field setting is toggled.
-    connect(focusUseFullField, &QCheckBox::toggled, this, [&](bool toggled)
-    {
-        focusFullFieldInnerRadius->setEnabled(toggled);
-        focusFullFieldOuterRadius->setEnabled(toggled);
-        setUseWeights();
-        if (toggled)
-        {
-            focusSubFrame->setChecked(false);
-            focusAutoStarEnabled->setChecked(false);
-        }
-        else
-        {
-            // Disable the overlay
-            m_FocusView->setStarFilterRange(0, 1);
-        }
-    });
+
+    // handle frame size changes
+    connect(focusBinning, QOverload<int>::of(&QComboBox::activated), this, 
&Ekos::Focus::checkMosaicMaskLimits);
 
     // Sync settings if the temperature source selection is updated.
     connect(defaultFocusTemperatureSource, &QComboBox::currentTextChanged, 
this, &Ekos::Focus::checkTemperatureSource);
@@ -6128,7 +6245,7 @@ void Focus::focusAdvisorSetup()
 
     FAFullFieldInnerRadius = 0.0;
     FAFullFieldOuterRadius = 80.0;
-    str.append("Annulus 0%-80%\n");
+    str.append("Ring Mask 0%-80%\n");
 
     // Suspend Guilding, Guide Settle and Display Units won't affect Autofocus 
so don't set
 
@@ -6268,6 +6385,7 @@ void Focus::focusAdvisorAction()
         focusAutoStarEnabled->setChecked(FAAutoSelectStar);
         focusFullFieldInnerRadius->setValue(FAFullFieldInnerRadius);
         focusFullFieldOuterRadius->setValue(FAFullFieldOuterRadius);
+        focusRingMaskRB->setChecked(true);
         focusAdaptive->setChecked(FAAdaptiveFocus);
         focusAdaptStart->setChecked(FAAdaptStartPos);
     }
diff --git a/kstars/ekos/focus/focus.h b/kstars/ekos/focus/focus.h
index 2ad8754ce1..0e7c4bfad4 100644
--- a/kstars/ekos/focus/focus.h
+++ b/kstars/ekos/focus/focus.h
@@ -66,6 +66,7 @@ class Focus : public QWidget, public Ui::Focus
         typedef enum { FOCUS_UNITS_PIXEL, FOCUS_UNITS_ARCSEC } StarUnits;
         typedef enum { FOCUS_WALK_CLASSIC, FOCUS_WALK_FIXED_STEPS, 
FOCUS_WALK_CFZ_SHUFFLE } FocusWalk;
 
+        typedef enum { FOCUS_MASK_NONE, FOCUS_MASK_RING, FOCUS_MASK_MOSAIC } 
ImageMaskType;
         //typedef enum { FOCUSER_TEMPERATURE, OBSERVATORY_TEMPERATURE, 
NO_TEMPERATURE } TemperatureSource;
 
         /** @defgroup FocusDBusInterface Ekos DBus Interface - Focus Module
@@ -201,6 +202,12 @@ class Focus : public QWidget, public Ui::Focus
              */
         bool setFilterWheel(ISD::FilterWheel *device);
 
+        /**
+         * @brief setImageMask Select the currently active image mask filtering
+         *        the stars relevant for focusing
+         * @param newImageMask ring mask or aberration inspector style mosaic
+         */
+        void selectImageMask(const ImageMaskType newMaskType);
 
         /**
              * @brief addTemperatureSource Add temperature source to the list 
of available sources.
@@ -465,7 +472,7 @@ class Focus : public QWidget, public Ui::Focus
 
         void setVideoStreamEnabled(bool enabled);
 
-        void calculateHFR();
+        void starDetectionFinished();
         void setCurrentMeasure();
 
     signals:
@@ -599,6 +606,11 @@ class Focus : public QWidget, public Ui::Focus
          * @brief loadSettings Load setting from Options and set them 
accordingly.
          */
         void loadGlobalSettings();
+        /**
+         * @brief checkMosaicMaskLimits Check if the maximum values configured
+         * for the aberration style mosaic tile sizes fit into the CCD frame 
size.
+         */
+        void checkMosaicMaskLimits();
 
         /**
          * @brief syncSettings When checkboxes, comboboxes, or spin boxes are 
updated, save their values in the
@@ -606,6 +618,11 @@ class Focus : public QWidget, public Ui::Focus
          */
         void syncSettings();
 
+        /**
+         * @brief syncImageMaskSelection Store the current mask selection to 
the settings
+         */
+        void syncImageMaskSelection();
+
         /**
          * @brief syncControl Sync setting to widget. The value depends on the 
widget type.
          * @param settings Map of all settings
@@ -893,6 +910,8 @@ class Focus : public QWidget, public Ui::Focus
         double absMotionMin { 0 };
         /// How many iterations have we completed now in our absolute 
autofocus algorithm? We can't go forever
         int absIterations { 0 };
+        /// Current image mask
+        ImageMaskType m_currentImageMask = FOCUS_MASK_NONE;
 
         /****************************
          * Misc. variables
diff --git a/kstars/ekos/focus/focus.ui b/kstars/ekos/focus/focus.ui
index 920a217230..c8999caf74 100644
--- a/kstars/ekos/focus/focus.ui
+++ b/kstars/ekos/focus/focus.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>947</width>
-    <height>662</height>
+    <width>888</width>
+    <height>677</height>
    </rect>
   </property>
   <property name="sizePolicy">
@@ -932,90 +932,154 @@
             <number>3</number>
            </property>
            <item>
-            <layout class="QGridLayout" name="gridLayoutSettings" 
rowstretch="0,0,0,0,0" columnstretch="0,0,0,0">
-             <property name="sizeConstraint">
-              <enum>QLayout::SetDefaultConstraint</enum>
-             </property>
+            <layout class="QGridLayout" name="gridLayoutSettings" 
columnstretch="0,1,1,1,1">
              <property name="spacing">
               <number>3</number>
              </property>
-             <item row="2" column="2">
-              <widget class="QDoubleSpinBox" name="focusFullFieldInnerRadius">
-               <property name="sizePolicy">
-                <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
-                 <horstretch>0</horstretch>
-                 <verstretch>0</verstretch>
-                </sizepolicy>
+             <item row="5" column="0">
+              <widget class="QRadioButton" name="focusMosaicMaskRB">
+               <property name="toolTip">
+                
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Aberration inspector 
style mask with a 3x3 mosaic formed with tiles from the center, the corners and 
the edges.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+               </property>
+               <property name="text">
+                <string>Mosaic Mask:</string>
                </property>
+               <attribute name="buttonGroup">
+                <string notr="true">focusMaskBG</string>
+               </attribute>
+              </widget>
+             </item>
+             <item row="5" column="4">
+              <widget class="QSpinBox" name="focusMosaicSpace">
                <property name="toolTip">
-                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;During 
Full Field focusing,  this controls the size of an Annulus centred at the 
middle of the sensor to include for processing.  Set inner % to zero to include 
the centre of the sensor and set outer % to 100 to include the outer edges of 
the sensor. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Size of 
the separator between the tiles.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+               </property>
+               <property name="alignment">
+                <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
                </property>
                <property name="suffix">
-                <string> %</string>
+                <string> px</string>
                </property>
-               <property name="decimals">
+               <property name="minimum">
                 <number>1</number>
                </property>
-               <property name="singleStep">
-                <double>10.000000000000000</double>
+               <property name="value">
+                <number>5</number>
                </property>
               </widget>
              </item>
-             <item row="3" column="1">
-              <widget class="QLabel" name="label_13">
+             <item row="0" column="1" colspan="2">
+              <widget class="QCheckBox" name="focusSuspendGuiding">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
                  <horstretch>0</horstretch>
                  <verstretch>0</verstretch>
                 </sizepolicy>
                </property>
+               <property name="toolTip">
+                <string>Suspend Guiding while autofocus in progress</string>
+               </property>
                <property name="text">
-                <string>Guide Settle:</string>
+                <string>Suspend Guiding</string>
                </property>
-               <property name="alignment">
-                <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+              </widget>
+             </item>
+             <item row="4" column="0">
+              <widget class="QRadioButton" name="focusRingMaskRB">
+               <property name="toolTip">
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;During 
Full Field focusing, this controls the size of an Annulus centred at the middle 
of the sensor to include for processing. Set inner % to zero to include the 
centre of the sensor and set outer % to 100 to include the outer edges of the 
sensor. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
                </property>
-               <property name="buddy">
-                <cstring>guideSettleTime</cstring>
+               <property name="text">
+                <string>Ring Mask:</string>
+               </property>
+               <property name="checked">
+                <bool>false</bool>
                </property>
+               <attribute name="buttonGroup">
+                <string notr="true">focusMaskBG</string>
+               </attribute>
               </widget>
              </item>
-             <item row="1" column="1">
-              <widget class="QLabel" name="label_15">
+             <item row="1" column="4">
+              <widget class="QSpinBox" name="focusBoxSize">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
                  <horstretch>0</horstretch>
                  <verstretch>0</verstretch>
                 </sizepolicy>
                </property>
-               <property name="text">
-                <string>Box:</string>
+               <property name="toolTip">
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Size of 
the subframe in pixels.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
                </property>
-               <property name="alignment">
-                <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+               <property name="suffix">
+                <string> px</string>
                </property>
-               <property name="buddy">
-                <cstring>focusBoxSize</cstring>
+               <property name="minimum">
+                <number>16</number>
+               </property>
+               <property name="maximum">
+                <number>256</number>
+               </property>
+               <property name="singleStep">
+                <number>16</number>
+               </property>
+               <property name="value">
+                <number>32</number>
                </property>
               </widget>
              </item>
-             <item row="2" column="0">
-              <widget class="QCheckBox" name="focusUseFullField">
+             <item row="2" column="4">
+              <widget class="QDoubleSpinBox" name="guideSettleTime">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
                  <horstretch>0</horstretch>
                  <verstretch>0</verstretch>
                 </sizepolicy>
                </property>
+               <property name="toolTip">
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Wait 
this many seconds after autofocus completes before resuming 
guiding.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+               </property>
+               <property name="suffix">
+                <string> s</string>
+               </property>
+               <property name="maximum">
+                <double>60.000000000000000</double>
+               </property>
+              </widget>
+             </item>
+             <item row="1" column="1" colspan="2">
+              <widget class="QRadioButton" name="focusSubFrame">
+               <property name="toolTip">
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Subframe 
around the focus star during the autofocus 
procedure.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+               </property>
+               <property name="text">
+                <string>Sub Frame</string>
+               </property>
+               <property name="checked">
+                <bool>false</bool>
+               </property>
+               <attribute name="buttonGroup">
+                <string notr="true">focusFieldBG</string>
+               </attribute>
+              </widget>
+             </item>
+             <item row="1" column="0">
+              <widget class="QRadioButton" name="focusUseFullField">
                <property name="toolTip">
                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Measure 
average HFR from all stars combined in a full frame. This method defaults to 
the Centroid detection, but can use SEP detection too. Its performance 
decreases as the number of stars 
increases.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
                </property>
                <property name="text">
                 <string>Full Field</string>
                </property>
+               <property name="checked">
+                <bool>true</bool>
+               </property>
+               <attribute name="buttonGroup">
+                <string notr="true">focusFieldBG</string>
+               </attribute>
               </widget>
              </item>
-             <item row="2" column="3">
+             <item row="4" column="3" colspan="2">
               <widget class="QDoubleSpinBox" name="focusFullFieldOuterRadius">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
@@ -1024,7 +1088,10 @@
                 </sizepolicy>
                </property>
                <property name="toolTip">
-                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;During 
Full Field focusing, this controls the size of an Annulus centred at the middle 
of the sensor to include for processing. Set inner % to zero to include the 
centre of the sensor and set outer % to 100 to include the outer edges of the 
sensor. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Diameter 
of the outer circle to be excluded from focusing. The diameter is given as 
percentage of the image diagonal. Set to 100% to include the outer edges of the 
sensor. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+               </property>
+               <property name="alignment">
+                <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
                </property>
                <property name="suffix">
                 <string> %</string>
@@ -1040,24 +1107,27 @@
                </property>
               </widget>
              </item>
-             <item row="3" column="0">
-              <widget class="QCheckBox" name="focusSuspendGuiding">
+             <item row="1" column="3">
+              <widget class="QLabel" name="label_15">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
                  <horstretch>0</horstretch>
                  <verstretch>0</verstretch>
                 </sizepolicy>
                </property>
-               <property name="toolTip">
-                <string>Suspend Guiding while autofocus in progress</string>
-               </property>
                <property name="text">
-                <string>Suspend Guiding</string>
+                <string>Box:</string>
+               </property>
+               <property name="alignment">
+                <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+               </property>
+               <property name="buddy">
+                <cstring>focusBoxSize</cstring>
                </property>
               </widget>
              </item>
-             <item row="0" column="0">
-              <widget class="QCheckBox" name="focusAutoStarEnabled">
+             <item row="0" column="3" colspan="2">
+              <widget class="QCheckBox" name="useFocusDarkFrame">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
                  <horstretch>0</horstretch>
@@ -1065,18 +1135,15 @@
                 </sizepolicy>
                </property>
                <property name="toolTip">
-                <string>Automatically select the best focus star from the 
image</string>
+                <string>Use dark frames from the library.</string>
                </property>
                <property name="text">
-                <string>Auto Select Star</string>
-               </property>
-               <property name="autoRepeatDelay">
-                <number>298</number>
+                <string>Dark Frame</string>
                </property>
               </widget>
              </item>
-             <item row="4" column="1">
-              <widget class="QLabel" name="label_38">
+             <item row="2" column="3">
+              <widget class="QLabel" name="label_13">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
                  <horstretch>0</horstretch>
@@ -1084,18 +1151,18 @@
                 </sizepolicy>
                </property>
                <property name="text">
-                <string>Display Units:</string>
+                <string>Guide Settle:</string>
                </property>
                <property name="alignment">
                 <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
                </property>
                <property name="buddy">
-                <cstring>focusUnits</cstring>
+                <cstring>guideSettleTime</cstring>
                </property>
               </widget>
              </item>
-             <item row="1" column="2" colspan="2">
-              <widget class="QSpinBox" name="focusBoxSize">
+             <item row="0" column="0">
+              <widget class="QCheckBox" name="focusAutoStarEnabled">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
                  <horstretch>0</horstretch>
@@ -1103,26 +1170,42 @@
                 </sizepolicy>
                </property>
                <property name="toolTip">
-                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Size of 
the subframe in pixels.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+                <string>Automatically select the best focus star from the 
image</string>
                </property>
-               <property name="suffix">
-                <string> px</string>
+               <property name="text">
+                <string>Auto Select Star</string>
                </property>
-               <property name="minimum">
-                <number>16</number>
+               <property name="autoRepeatDelay">
+                <number>298</number>
                </property>
-               <property name="maximum">
-                <number>256</number>
+              </widget>
+             </item>
+             <item row="4" column="1" colspan="2">
+              <widget class="QDoubleSpinBox" name="focusFullFieldInnerRadius">
+               <property name="sizePolicy">
+                <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+                 <horstretch>0</horstretch>
+                 <verstretch>0</verstretch>
+                </sizepolicy>
                </property>
-               <property name="singleStep">
-                <number>16</number>
+               <property name="toolTip">
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Diameter 
of the inner circle to be excluded from focusing (e.g. a centered galaxy or 
star cluster). The diameter is given as percentage of the image diagonal. Set 
to zero to include the centre of the 
sensor.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
                </property>
-               <property name="value">
-                <number>32</number>
+               <property name="alignment">
+                <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+               </property>
+               <property name="suffix">
+                <string> %</string>
+               </property>
+               <property name="decimals">
+                <number>1</number>
+               </property>
+               <property name="singleStep">
+                <double>10.000000000000000</double>
                </property>
               </widget>
              </item>
-             <item row="4" column="2" colspan="2">
+             <item row="2" column="1" colspan="2">
               <widget class="QComboBox" name="focusUnits">
                <property name="toolTip">
                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Display 
units for HFR and FWHM.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -1139,77 +1222,74 @@
                </item>
               </widget>
              </item>
-             <item row="3" column="2" colspan="2">
-              <widget class="QDoubleSpinBox" name="guideSettleTime">
-               <property name="sizePolicy">
-                <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
-                 <horstretch>0</horstretch>
-                 <verstretch>0</verstretch>
-                </sizepolicy>
+             <item row="5" column="3">
+              <widget class="QLabel" name="focusSpacerLabel">
+               <property name="text">
+                <string>Spacer:</string>
                </property>
+              </widget>
+             </item>
+             <item row="5" column="1" colspan="2">
+              <widget class="QSpinBox" name="focusMosaicTileWidth">
                <property name="toolTip">
-                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Wait 
this many seconds after autofocus completes before resuming 
guiding.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tiles 
are squares with an edge length calculated by the given percentage of the image 
width. The percentage is limited such that the tile size does not exceed one 
third of the shorter side of the image (in most cases, it's 
height).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+               </property>
+               <property name="alignment">
+                <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
                </property>
                <property name="suffix">
-                <string> s</string>
+                <string> %</string>
                </property>
-               <property name="maximum">
-                <double>60.000000000000000</double>
+               <property name="prefix">
+                <string/>
                </property>
-              </widget>
-             </item>
-             <item row="0" column="1">
-              <widget class="QCheckBox" name="useFocusDarkFrame">
-               <property name="sizePolicy">
-                <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
-                 <horstretch>0</horstretch>
-                 <verstretch>0</verstretch>
-                </sizepolicy>
+               <property name="minimum">
+                <number>1</number>
                </property>
-               <property name="toolTip">
-                <string>Use dark frames from the library.</string>
+               <property name="maximum">
+                <number>33</number>
                </property>
-               <property name="text">
-                <string>Dark Frame</string>
+               <property name="singleStep">
+                <number>1</number>
+               </property>
+               <property name="value">
+                <number>25</number>
                </property>
               </widget>
              </item>
-             <item row="1" column="0">
-              <widget class="QCheckBox" name="focusSubFrame">
+             <item row="2" column="0">
+              <widget class="QLabel" name="label_38">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
                  <horstretch>0</horstretch>
                  <verstretch>0</verstretch>
                 </sizepolicy>
                </property>
-               <property name="toolTip">
-                <string>Subframe around the focus star during the autofocus 
procedure</string>
-               </property>
                <property name="text">
-                <string>Sub Frame</string>
+                <string>Display Units:</string>
                </property>
-               <property name="checked">
-                <bool>true</bool>
+               <property name="alignment">
+                <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+               </property>
+               <property name="buddy">
+                <cstring>focusUnits</cstring>
                </property>
               </widget>
              </item>
-             <item row="2" column="1">
-              <widget class="QLabel" name="label_10">
-               <property name="sizePolicy">
-                <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
-                 <horstretch>0</horstretch>
-                 <verstretch>0</verstretch>
-                </sizepolicy>
+             <item row="3" column="0" colspan="3">
+              <widget class="QRadioButton" name="focusNoMaskRB">
+               <property name="toolTip">
+                <string>All stars are used for focusing.</string>
                </property>
                <property name="text">
-                <string>Annulus:</string>
-               </property>
-               <property name="alignment">
-                <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+                <string>Use all stars for focusing</string>
                </property>
-               <property name="buddy">
-                <cstring>focusFullFieldInnerRadius</cstring>
+               <property name="checked">
+                <bool>true</bool>
                </property>
+               <attribute name="buttonGroup">
+                <string notr="true">focusMaskBG</string>
+               </attribute>
               </widget>
              </item>
             </layout>
@@ -3749,16 +3829,6 @@
   <tabstop>filterManagerB</tabstop>
   <tabstop>resetFrameB</tabstop>
   <tabstop>tabWidget</tabstop>
-  <tabstop>focusAutoStarEnabled</tabstop>
-  <tabstop>useFocusDarkFrame</tabstop>
-  <tabstop>focusSubFrame</tabstop>
-  <tabstop>focusBoxSize</tabstop>
-  <tabstop>focusUseFullField</tabstop>
-  <tabstop>focusFullFieldInnerRadius</tabstop>
-  <tabstop>focusFullFieldOuterRadius</tabstop>
-  <tabstop>focusSuspendGuiding</tabstop>
-  <tabstop>guideSettleTime</tabstop>
-  <tabstop>focusUnits</tabstop>
   <tabstop>focusAdaptive</tabstop>
   <tabstop>focusAdaptiveMinMove</tabstop>
   <tabstop>focusAdaptStart</tabstop>
@@ -3802,4 +3872,8 @@
  </tabstops>
  <resources/>
  <connections/>
+ <buttongroups>
+  <buttongroup name="focusFieldBG"/>
+  <buttongroup name="focusMaskBG"/>
+ </buttongroups>
 </ui>
diff --git a/kstars/ekos/focus/focusfourierpower.h 
b/kstars/ekos/focus/focusfourierpower.h
index a02d45913c..2feb189529 100644
--- a/kstars/ekos/focus/focusfourierpower.h
+++ b/kstars/ekos/focus/focusfourierpower.h
@@ -9,6 +9,7 @@
 #include <QList>
 #include "../fitsviewer/fitsstardetector.h"
 #include "fitsviewer/fitsview.h"
+#include "auxiliary/imagemask.h"
 #include "curvefit.h"
 #include "../ekos.h"
 #include <ekos_focus_debug.h>
@@ -59,7 +60,7 @@ class FocusFourierPower
 
         template <typename T>
         void processFourierPower(const T imageBuffer, const 
QSharedPointer<FITSData> &imageData,
-                                 QPair<double, double> filter, double 
*fourierPower, double *weight)
+                                 QSharedPointer<ImageMask> mask, double 
*fourierPower, double *weight)
         {
             // Initialise outputs
             *fourierPower = INVALID_STAR_MEASURE;
@@ -87,33 +88,22 @@ class FocusFourierPower
             auto skyBackground = imageData->getSkyBackground();
             auto bg = skyBackground.mean + 3.0 * skyBackground.sigma;
 
-            // Check to see if the sensor in use is being restricted by an 
annulus
-            long const sqDiagonal = (width * width + height * height) / 4;
-            long const sqInnerRadius = std::lround(sqDiagonal * (filter.first 
* filter.first) / 10000);
-            long const sqOuterRadius = std::lround(sqDiagonal * (filter.second 
* filter.second) / 10000);
-            bool const filterInUse = filter.first != 0.0 || filter.second != 
100.0;
+            uint16_t posX = 0, posY = 0;
             for (long i = 0; i < N; i++)
             {
-                if (!filterInUse)
+                if (mask.isNull() || mask->active() == false || 
mask->isVisible(posX, posY))
                     image[i * 2] = std::max(0.0, (double) imageBuffer[i] - bg);
                 else
-                {
-                    // Get pixel x, y relative to the centre of the sensor
-                    long x = i % width - width / 2;
-                    long y = i / width - height / 2;
-
-                    // Get the square radius of the pixel from the centre
-                    long sqRadius = x * x + y * y;
-
-                    // Is the pixel inside the filter? If not, set to zero.
-                    if (sqRadius > sqInnerRadius && sqRadius < sqOuterRadius)
-                        image[i * 2] = std::max(0.0, (double) imageBuffer[i] - 
bg);
-                    else
-                        image[i * 2] = 0.0;
-                }
+                    image[i * 2] = 0.0;
 
                 // Imaginary value is always zero
                 image[i * 2 + 1] = 0.0;
+
+                if (++posX == width)
+                {
+                    posX = 0;
+                    posY++;
+                }
             }
 
             // Perform FFT on all the rows
diff --git a/kstars/fitsviewer/fitsdata.cpp b/kstars/fitsviewer/fitsdata.cpp
index 46411508f9..50ce8c0d8f 100644
--- a/kstars/fitsviewer/fitsdata.cpp
+++ b/kstars/fitsviewer/fitsdata.cpp
@@ -2107,22 +2107,19 @@ QFuture<bool> FITSData::findStars(StarAlgorithm 
algorithm, const QRect &tracking
     }
 }
 
-int FITSData::filterStars(const float innerRadius, const float outerRadius)
+int FITSData::filterStars(QSharedPointer<ImageMask> mask)
 {
-    long const sqDiagonal = (long) this->width() * (long) this->width() / 4 + 
(long) this->height() * (long) this->height() / 4;
-    long const sqInnerRadius = std::lround(sqDiagonal * innerRadius * 
innerRadius);
-    long const sqOuterRadius = std::lround(sqDiagonal * outerRadius * 
outerRadius);
-
-    starCenters.erase(std::remove_if(starCenters.begin(), starCenters.end(),
-                                     [&](Edge * edge)
+    if (mask.isNull() == false)
     {
-        long const x = edge->x - this->width() / 2;
-        long const y = edge->y - this->height() / 2;
-        long const sqRadius = x * x + y * y;
-        return sqRadius < sqInnerRadius || sqOuterRadius < sqRadius;
-    }), starCenters.end());
+        starCenters.erase(std::remove_if(starCenters.begin(), 
starCenters.end(),
+                                         [&](Edge * edge)
+        {
+            return (mask->isVisible(edge->x, edge->y) == false);
+        }), starCenters.end());
+    }
 
     return starCenters.count();
+
 }
 
 double FITSData::getHFR(HFRType type)
diff --git a/kstars/fitsviewer/fitsdata.h b/kstars/fitsviewer/fitsdata.h
index 17005ab8c7..55af16881d 100644
--- a/kstars/fitsviewer/fitsdata.h
+++ b/kstars/fitsviewer/fitsdata.h
@@ -23,6 +23,7 @@
 #include "skybackground.h"
 #include "fitscommon.h"
 #include "fitsstardetector.h"
+#include "auxiliary/imagemask.h"
 
 #ifdef WIN32
 // This header must be included before fitsio.h to avoid compiler errors with 
Visual Studio
@@ -322,8 +323,8 @@ class FITSData : public QObject
         void getFloatBuffer(float *buffer, int x, int y, int w, int h) const;
         //int findSEPStars(QList<Edge*> &, const int8_t &boundary = int8_t()) 
const;
 
-        // Apply ring filter to searched stars
-        int filterStars(const float innerRadius, const float outerRadius);
+        // filter all stars that are visible through the given mask
+        int filterStars(QSharedPointer<ImageMask> mask);
 
         // Half Flux Radius
         const Edge &getSelectedHFRStar() const
diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp
index 784671a2a8..b736c1a93e 100644
--- a/kstars/fitsviewer/fitsview.cpp
+++ b/kstars/fitsviewer/fitsview.cpp
@@ -381,6 +381,9 @@ bool FITSView::loadData(const QSharedPointer<FITSData> 
&data)
 
     // Takes control of the objects passed in.
     m_ImageData = data;
+    // set the image mask geometry
+    if (m_ImageMask != nullptr)
+        m_ImageMask->setImageGeometry(data->width(), data->height());
 
     if (processData())
     {
@@ -722,16 +725,24 @@ void FITSView::ZoomToFit()
     emit zoomRubberBand(getCurrentZoom() / ZOOM_DEFAULT);
 }
 
-void FITSView::setStarFilterRange(float const innerRadius, float const 
outerRadius)
+
+
+int FITSView::filterStars()
 {
-    starFilter.innerRadius = innerRadius;
-    starFilter.outerRadius = outerRadius;
+    return ((m_ImageMask.isNull() == false
+             && m_ImageMask->active()) ? m_ImageData->filterStars(m_ImageMask) 
: m_ImageData->getStarCenters().count());
 }
 
-int FITSView::filterStars()
+void FITSView::setImageMask(ImageMask *mask)
 {
-    return starFilter.used() ? m_ImageData->filterStars(starFilter.innerRadius,
-            starFilter.outerRadius) : m_ImageData->getStarCenters().count();
+    if (m_ImageMask.isNull() == false)
+    {
+        // copy image geometry from the old mask before deleting it
+        if (mask != nullptr)
+            mask->setImageGeometry(m_ImageMask->width(), 
m_ImageMask->height());
+    }
+
+    m_ImageMask.reset(mask);
 }
 
 // isImageLarge() returns whether we use the large-image rendering strategy or 
the small-image strategy.
@@ -787,26 +798,56 @@ void FITSView::updateFrame(bool now)
             updateFrameLargeImage();
         else
             updateFrameSmallImage();
+
     }
     else
         m_UpdateFrameTimer.start();
 }
 
 
-void FITSView::updateFrameLargeImage()
+bool FITSView::initDisplayPixmap(QImage &image, float scale)
 {
-    if (!displayPixmap.convertFromImage(rawImage))
-        return;
+    ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get());
+
+    // if no mosaic should be created, simply convert the original image
+    if (mask == nullptr)
+        return displayPixmap.convertFromImage(image);
+
+    // check image geometry, sincd scaling could have changed it
+    // create the 3x3 mosaic
+    int width = mask->tileWidth() * mask->width() / 100;
+    int space = mask->space();
+    // create a new all black pixmap with mosaic size
+    displayPixmap = QPixmap((3 * width + 2 * space) * scale, (3 * width + 2 * 
space) * scale);
+    displayPixmap.fill(Qt::black);
 
     QPainter painter(&displayPixmap);
+    int pos = 0;
+    // paint tiles
+    for (QRect tile : mask->tiles())
+    {
+        const int posx = pos % 3;
+        const int posy = pos++ / 3;
+        const int tilewidth = width * scale;
+        QRectF source(tile.x() * scale, tile.y()*scale, tilewidth, tilewidth);
+        QRectF target((posx * (width + space)) * scale, (posy * (width + 
space)) * scale, width * scale, width * scale);
+        painter.drawImage(target, image, source);
+    }
+    return true;
+}
 
+void FITSView::updateFrameLargeImage()
+{
+    if (!initDisplayPixmap(rawImage, 1.0 / m_PreviewSampling))
+        return;
+    QPainter painter(&displayPixmap);
     // Possibly scale the fonts as we're drawing on the full image, not just 
the visible part of the scroll window.
     QFont font = painter.font();
     font.setPixelSize(scaleSize(FONT_SIZE));
     painter.setFont(font);
 
+    drawStarRingFilter(&painter, 1.0 / m_PreviewSampling, 
dynamic_cast<ImageRingMask *>(m_ImageMask.get()));
     drawOverlay(&painter, 1.0 / m_PreviewSampling);
-    drawStarFilter(&painter, 1.0 / m_PreviewSampling);
     m_ImageFrame->setPixmap(displayPixmap);
     m_ImageFrame->resize(((m_PreviewSampling * currentZoom) / 100.0) * 
displayPixmap.size());
 }
@@ -814,30 +855,28 @@ void FITSView::updateFrameLargeImage()
 void FITSView::updateFrameSmallImage()
 {
     QImage scaledImage = rawImage.scaled(currentWidth, currentHeight, 
Qt::KeepAspectRatio, Qt::SmoothTransformation);
-    if (!displayPixmap.convertFromImage(scaledImage))
+    if (!initDisplayPixmap(scaledImage, currentZoom / ZOOM_DEFAULT))
         return;
 
     QPainter painter(&displayPixmap);
-
-    //    if (m_PreviewSampling == 1)
-    //    {
+    // Possibly scale the fonts as we're drawing on the full image, not just 
the visible part of the scroll window.
+    QFont font = painter.font();
+    drawStarRingFilter(&painter, currentZoom / ZOOM_DEFAULT, 
dynamic_cast<ImageRingMask *>(m_ImageMask.get()));
     drawOverlay(&painter, currentZoom / ZOOM_DEFAULT);
-    drawStarFilter(&painter, currentZoom / ZOOM_DEFAULT);
-    //}
     m_ImageFrame->setPixmap(displayPixmap);
     m_ImageFrame->resize(currentWidth, currentHeight);
 }
 
-
-void FITSView::drawStarFilter(QPainter *painter, double scale)
+void FITSView::drawStarRingFilter(QPainter *painter, double scale, 
ImageRingMask *ringMask)
 {
-    if (!starFilter.used())
+    if (ringMask == nullptr || !ringMask->active())
         return;
+
     const double w = m_ImageData->width() * scale;
     const double h = m_ImageData->height() * scale;
     double const diagonal = std::sqrt(w * w + h * h) / 2;
-    int const innerRadius = std::lround(diagonal * starFilter.innerRadius);
-    int const outerRadius = std::lround(diagonal * starFilter.outerRadius);
+    int const innerRadius = std::lround(diagonal * ringMask->innerRadius());
+    int const outerRadius = std::lround(diagonal * ringMask->outerRadius());
     QPoint const center(w / 2, h / 2);
     painter->save();
     painter->setPen(QPen(Qt::blue, scaleSize(1), Qt::DashLine));
@@ -1246,15 +1285,19 @@ void FITSView::drawStarCentroid(QPainter * painter, 
double scale)
     }
 
     painter->setPen(QPen(Qt::red, scaleSize(2)));
+    ImageMosaicMask *mask = dynamic_cast<ImageMosaicMask *>(m_ImageMask.get());
 
     for (auto const &starCenter : m_ImageData->getStarCenters())
     {
         int const w  = std::round(starCenter->width) * scale;
 
+        // translate if a mosaic mask is present
+        const QPointF center = (mask == nullptr) ? QPointF(starCenter->x, 
starCenter->y) : mask->translate(QPointF(starCenter->x,
+                               starCenter->y));
         // Draw a circle around the detected star.
         // SEP coordinates are in the center of pixels, and Qt at the boundary.
-        const double xCoord = starCenter->x - 0.5;
-        const double yCoord = starCenter->y - 0.5;
+        const double xCoord = center.x() - 0.5;
+        const double yCoord = center.y() - 0.5;
         const int xc = std::round((xCoord - starCenter->width / 2.0f) * scale);
         const int yc = std::round((yCoord - starCenter->width / 2.0f) * scale);
         const int hw = w / 2;
@@ -1279,9 +1322,9 @@ void FITSView::drawStarCentroid(QPainter * painter, 
double scale)
 
             // Draw offset circle
             double factor = 15.0;
-            QPointF offsetVector = (bEdge->offset - QPointF(starCenter->x, 
starCenter->y)) * factor;
-            int const xo = std::round((starCenter->x + offsetVector.x() - 
starCenter->width / 2.0f) * scale);
-            int const yo = std::round((starCenter->y + offsetVector.y() - 
starCenter->width / 2.0f) * scale);
+            QPointF offsetVector = (bEdge->offset - QPointF(center.x(), 
center.y())) * factor;
+            int const xo = std::round((center.x() + offsetVector.x() - 
starCenter->width / 2.0f) * scale);
+            int const yo = std::round((center.y() + offsetVector.y() - 
starCenter->width / 2.0f) * scale);
             painter->setPen(QPen(Qt::red, scaleSize(2)));
             painter->drawEllipse(xo, yo, w, w);
 
diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h
index 0586462cde..84be424339 100644
--- a/kstars/fitsviewer/fitsview.h
+++ b/kstars/fitsviewer/fitsview.h
@@ -8,6 +8,7 @@
 #pragma once
 
 #include "fitscommon.h"
+#include "auxiliary/imagemask.h"
 
 #include <config-kstars.h>
 #include "stretch.h"
@@ -120,7 +121,7 @@ class FITSView : public QScrollArea
         virtual void drawOverlay(QPainter *, double scale);
 
         // Overlay objects
-        void drawStarFilter(QPainter *, double scale);
+        void drawStarRingFilter(QPainter *, double scale, ImageRingMask 
*ringMask);
         void drawStarCentroid(QPainter *, double scale);
         void drawClipping(QPainter *);
         void drawTrackingBox(QPainter *, double scale);
@@ -182,9 +183,12 @@ class FITSView : public QScrollArea
         void searchStars();
         void setStarsEnabled(bool enable);
         void setStarsHFREnabled(bool enable);
-        void setStarFilterRange(float const innerRadius, float const 
outerRadius);
         int filterStars();
 
+        // image masks
+        QSharedPointer<ImageMask> imageMask() { return m_ImageMask; }
+        void setImageMask(ImageMask *mask);
+
         // FITS Mode
         void updateMode(FITSMode fmode);
         FITSMode getMode()
@@ -356,6 +360,7 @@ class FITSView : public QScrollArea
         void doStretch(QImage *outputImage);
         double scaleSize(double size);
         bool isLargeImage();
+        bool initDisplayPixmap(QImage &image, float space);
         void updateFrameLargeImage();
         void updateFrameSmallImage();
         bool drawHFR(QPainter * painter, const QString &hfr, int x, int y);
@@ -409,15 +414,8 @@ class FITSView : public QScrollArea
         // Adaptive sampling is based on available RAM
         uint8_t m_AdaptiveSampling {1};
 
-        struct
-        {
-            bool used() const
-            {
-                return innerRadius != 0.0f || outerRadius != 1.0f;
-            }
-            float innerRadius { 0.0f };
-            float outerRadius { 1.0f };
-        } starFilter;
+        // mask for star detection
+        QSharedPointer<ImageMask> m_ImageMask;
 
         CursorMode cursorMode { selectCursor };
         bool zooming { false };
diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg
index 946fc35e3c..775569aa76 100644
--- a/kstars/kstars.kcfg
+++ b/kstars/kstars.kcfg
@@ -1894,11 +1894,6 @@
       <entry name="FocusFilter" type="String">
          <label>Default Filter Wheel filter</label>
       </entry>
-
-      <entry name="FocusAutoStarEnabled" type="Bool">
-         <label>Automatically select a star to focus.</label>
-         <default>false</default>
-      </entry>
       <entry name="UseFocusDarkFrame" type="Bool">
          <label>Take a dark frame and subtract it before running autofocus 
operation.</label>
          <default>false</default>
@@ -1916,6 +1911,10 @@
          <label>Measure average HFR from all stars combined in a full frame. 
This method defaults to the Centroid detection, but can use SEP detection too. 
Its performance decreases as the number of stars increases.</label>
          <default>false</default>
       </entry>
+      <entry name="FocusMaskType" type="Int">
+         <label>Focus mask type: 0 = All stars used when focusing, 1 = ring 
mask, 2 = 3x3 mosaic style mask</label>
+         <default>0</default>
+      </entry>
       <entry name="FocusFullFieldInnerRadius" type="Double">
          <label>Full field inner radius.</label>
          <whatsthis>During full field focusing, stars which are inside this 
percentage of the frame are filtered out of HFR calculation (default 0%). 
Detection algorithms may also have an inherent filter.</whatsthis>
@@ -1926,6 +1925,18 @@
          <whatsthis>During full field focusing, stars which are outside this 
percentage of the frame are filtered out of HFR calculation (default 100%). 
Detection algorithms may also have an inherent filter.</whatsthis>
          <default>100.0</default>
       </entry>
+      <entry name="FocusMosaicTileWidth" type="Double">
+         <label>Mosaic filter tile width in percent of the frame width.</label>
+         <default>25.0</default>
+      </entry>
+      <entry name="focusMosaicSpace" type="UInt">
+         <label>Space between the mosaic elements for the mosaic 
filter.</label>
+         <default>5</default>
+      </entry>
+      <entry name="FocusAutoStarEnabled" type="Bool">
+         <label>Automatically select a star to focus.</label>
+         <default>false</default>
+      </entry>
       <entry name="FocusSuspendGuiding" type="Bool">
          <label>Suspend guiding while autofocus in progress.</label>
          <default>true</default>

Reply via email to