Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions indi_allsky/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ class IndiAllSkyConfigBase(object):
"ADU_FOV_DIV" : 4,
"DETECT_STARS" : True,
"DETECT_STARS_THOLD" : 0.6,
"DETECT_STARS_METHOD" : "template",
"DETECT_STARS_SEP_THOLD" : 5.0,
"DETECT_STARS_SEP_MAX_RADIUS" : 20,
"DETECT_METEORS" : False,
"DETECT_METEORS_THOLD" : 125,
"DETECT_MASK" : "",
Expand Down
30 changes: 30 additions & 0 deletions indi_allsky/flask/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,28 @@ def DETECT_STARS_THOLD_validator(form, field):
raise ValidationError('Threshold must be 1.0 or less')


def DETECT_STARS_SEP_THOLD_validator(form, field):
if not isinstance(field.data, (int, float)):
raise ValidationError('Please enter valid number')

if field.data < 0.5:
raise ValidationError('Sigma must be 0.5 or greater')

if field.data > 50.0:
raise ValidationError('Sigma must be 50.0 or less')


def DETECT_STARS_SEP_MAX_RADIUS_validator(form, field):
if not isinstance(field.data, int):
raise ValidationError('Please enter a valid integer')

if field.data < 1:
raise ValidationError('Max radius must be 1 or greater')

if field.data > 500:
raise ValidationError('Max radius must be 500 or less')


def DETECT_METEORS_THOLD_validator(form, field):
if not isinstance(field.data, int):
raise ValidationError('Please enter valid number')
Expand Down Expand Up @@ -4398,8 +4420,16 @@ class IndiAllskyConfigForm(FlaskForm):
ADU_ROI_X2 = IntegerField('ADU ROI x2', validators=[ADU_ROI_validator])
ADU_ROI_Y2 = IntegerField('ADU ROI y2', validators=[ADU_ROI_validator])
ADU_FOV_DIV = SelectField('ADU FoV', choices=ADU_FOV_DIV_choices, validators=[ADU_FOV_DIV_validator])
DETECT_STARS_METHOD_choices = (
('template', 'Template Match'),
('sep', 'SEP (Source Extractor)'),
)

DETECT_STARS = BooleanField('Star Detection')
DETECT_STARS_THOLD = FloatField('Star Detection Threshold', validators=[DataRequired(), DETECT_STARS_THOLD_validator])
DETECT_STARS_METHOD = SelectField('Star Detection Method', choices=DETECT_STARS_METHOD_choices, validators=[DataRequired()])
DETECT_STARS_SEP_THOLD = FloatField('SEP Sigma Threshold', validators=[DataRequired(), DETECT_STARS_SEP_THOLD_validator], widget=NumberInput(step=0.5))
DETECT_STARS_SEP_MAX_RADIUS = IntegerField('SEP Max Star Radius', validators=[DataRequired(), DETECT_STARS_SEP_MAX_RADIUS_validator])
DETECT_METEORS = BooleanField('Meteor Detection')
DETECT_METEORS_THOLD = IntegerField('Meteor Detection Threshold', validators=[DataRequired(), DETECT_METEORS_THOLD_validator])
DETECT_MASK = StringField('Detection Mask', validators=[DETECT_MASK_validator])
Expand Down
65 changes: 64 additions & 1 deletion indi_allsky/flask/templates/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -3863,7 +3863,40 @@ <h2 class="accordion-header">
{{ form_config.DETECT_STARS_THOLD(class='form-control bg-secondary') }}
<div id="DETECT_STARS_THOLD-error" class="invalid-feedback text-danger" style="display: none;"></div>
</div>
<div class="col-sm-8">0.5 - 0.6 is a good value to start</div>
<div class="col-sm-8">0.5 - 0.6 is a good value to start (template method only)</div>
</div>

<div class="form-group row">
<div class="col-sm-2">
{{ form_config.DETECT_STARS_METHOD.label(class='col-form-label') }}
</div>
<div class="col-sm-2">
{{ form_config.DETECT_STARS_METHOD(class='form-select bg-secondary') }}
<div id="DETECT_STARS_METHOD-error" class="invalid-feedback text-danger" style="display: none;"></div>
</div>
<div class="col-sm-8">Template match uses a synthetic star pattern; SEP (Source Extractor) estimates the sky background and detects real sources</div>
</div>

<div class="form-group row" id="DETECT_STARS_SEP_THOLD_row">
<div class="col-sm-2">
{{ form_config.DETECT_STARS_SEP_THOLD.label(class='col-form-label') }}
</div>
<div class="col-sm-2">
{{ form_config.DETECT_STARS_SEP_THOLD(class='form-control bg-secondary') }}
<div id="DETECT_STARS_SEP_THOLD-error" class="invalid-feedback text-danger" style="display: none;"></div>
</div>
<div class="col-sm-8">Sigma above background noise: 3 (faint stars) &rarr; 10 (bright stars only). Default: 5.0 (SEP method only)</div>
</div>

<div class="form-group row" id="DETECT_STARS_SEP_MAX_RADIUS_row">
<div class="col-sm-2">
{{ form_config.DETECT_STARS_SEP_MAX_RADIUS.label(class='col-form-label') }}
</div>
<div class="col-sm-2">
{{ form_config.DETECT_STARS_SEP_MAX_RADIUS(class='form-control bg-secondary') }}
<div id="DETECT_STARS_SEP_MAX_RADIUS-error" class="invalid-feedback text-danger" style="display: none;"></div>
</div>
<div class="col-sm-8">Maximum semi-major axis (pixels) for a valid star detection — larger sources are rejected as bloomed stars or artifacts. Default: 20 (SEP method only)</div>
</div>

<hr>
Expand Down Expand Up @@ -9884,6 +9917,9 @@ <h2 class="accordion-header">
'SQM_ROI_Y2',
'SQM_FOV_DIV',
'DETECT_STARS_THOLD',
'DETECT_STARS_METHOD',
'DETECT_STARS_SEP_THOLD',
'DETECT_STARS_SEP_MAX_RADIUS',
'DETECT_METEORS_THOLD',
'DETECT_MASK',
'LOGO_OVERLAY',
Expand Down Expand Up @@ -10976,6 +11012,30 @@ <h2 class="accordion-header">
});


// Star detection method
function applyDetectStarsMethod(val) {
if (val === 'sep') {
$('#DETECT_STARS_THOLD').attr({'readonly': true, 'disabled': true});
$('#DETECT_STARS_THOLD').addClass('bg-dark text-secondary');
$('#DETECT_STARS_SEP_THOLD').removeAttr('readonly disabled');
$('#DETECT_STARS_SEP_THOLD').removeClass('bg-dark text-secondary');
$('#DETECT_STARS_SEP_MAX_RADIUS').removeAttr('readonly disabled');
$('#DETECT_STARS_SEP_MAX_RADIUS').removeClass('bg-dark text-secondary');
} else {
$('#DETECT_STARS_THOLD').removeAttr('readonly disabled');
$('#DETECT_STARS_THOLD').removeClass('bg-dark text-secondary');
$('#DETECT_STARS_SEP_THOLD').attr({'readonly': true, 'disabled': true});
$('#DETECT_STARS_SEP_THOLD').addClass('bg-dark text-secondary');
$('#DETECT_STARS_SEP_MAX_RADIUS').attr({'readonly': true, 'disabled': true});
$('#DETECT_STARS_SEP_MAX_RADIUS').addClass('bg-dark text-secondary');
}
}

$('#DETECT_STARS_METHOD').on('change', function() {
applyDetectStarsMethod($(this).val());
});


// Night timelapse settings
$('#TIMELAPSE__USE_NIGHT_CONFIG').on('change', function() {
if ($('#TIMELAPSE__USE_NIGHT_CONFIG').prop('checked')) {
Expand Down Expand Up @@ -11327,6 +11387,9 @@ <h2 class="accordion-header">
$('#SHARPEN_AMOUNT_DAY').addClass('bg-dark text-secondary');
}

// Star detection method - apply initial state
applyDetectStarsMethod($('#DETECT_STARS_METHOD').val());

// Denoise dependent fields - grey out on page load (night)
if (!$('#IMAGE_DENOISE').val()) {
$('#IMAGE_DENOISE_STRENGTH').attr({'readonly': true, 'disabled': true});
Expand Down
6 changes: 6 additions & 0 deletions indi_allsky/flask/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,9 @@ def get_context(self):
'SQM_FOV_DIV' : str(self.indi_allsky_config.get('SQM_FOV_DIV', 4)), # string in form, int in config
'DETECT_STARS' : self.indi_allsky_config.get('DETECT_STARS', True),
'DETECT_STARS_THOLD' : self.indi_allsky_config.get('DETECT_STARS_THOLD', 0.6),
'DETECT_STARS_METHOD' : self.indi_allsky_config.get('DETECT_STARS_METHOD', 'template'),
'DETECT_STARS_SEP_THOLD' : self.indi_allsky_config.get('DETECT_STARS_SEP_THOLD', 5.0),
'DETECT_STARS_SEP_MAX_RADIUS' : self.indi_allsky_config.get('DETECT_STARS_SEP_MAX_RADIUS', 20),
'DETECT_METEORS' : self.indi_allsky_config.get('DETECT_METEORS', False),
'DETECT_METEORS_THOLD' : self.indi_allsky_config.get('DETECT_METEORS_THOLD', 125),
'DETECT_MASK' : self.indi_allsky_config.get('DETECT_MASK', ''),
Expand Down Expand Up @@ -3340,6 +3343,9 @@ def dispatch_request(self):
self.indi_allsky_config['SQM_FOV_DIV'] = int(request.json['SQM_FOV_DIV'])
self.indi_allsky_config['DETECT_STARS'] = bool(request.json['DETECT_STARS'])
self.indi_allsky_config['DETECT_STARS_THOLD'] = float(request.json['DETECT_STARS_THOLD'])
self.indi_allsky_config['DETECT_STARS_METHOD'] = str(request.json['DETECT_STARS_METHOD'])
self.indi_allsky_config['DETECT_STARS_SEP_THOLD'] = float(request.json['DETECT_STARS_SEP_THOLD'])
self.indi_allsky_config['DETECT_STARS_SEP_MAX_RADIUS'] = int(request.json['DETECT_STARS_SEP_MAX_RADIUS'])
self.indi_allsky_config['DETECT_METEORS'] = bool(request.json['DETECT_METEORS'])
self.indi_allsky_config['DETECT_METEORS_THOLD'] = int(request.json['DETECT_METEORS_THOLD'])
self.indi_allsky_config['DETECT_MASK'] = str(request.json['DETECT_MASK'])
Expand Down
6 changes: 5 additions & 1 deletion indi_allsky/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,11 @@ def post_init(self):
self._adu_mask_dict = self._detection_mask_dict # reuse detection mask for ADU mask (if defined)

self._sqm = IndiAllskySqm(self.config, self.gain_av, mask=self._detection_mask_dict)
self._stars_detect = IndiAllSkyStars(self.config, mask=self._detection_mask_dict)
if self.config.get('DETECT_STARS_METHOD', 'template') == 'sep':
from .starsSep import IndiAllSkyStarsSEP
self._stars_detect = IndiAllSkyStarsSEP(self.config, mask=self._detection_mask_dict)
else:
self._stars_detect = IndiAllSkyStars(self.config, mask=self._detection_mask_dict)
self._lineDetect = IndiAllskyDetectLines(self.config, mask=self._detection_mask_dict)
self._draw = IndiAllSkyDraw(self.config, mask=self._detection_mask_dict)

Expand Down
126 changes: 126 additions & 0 deletions indi_allsky/starsSep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import time
from pathlib import Path
import cv2
import numpy
import sep
import logging


logger = logging.getLogger('indi_allsky')


class IndiAllSkyStarsSEP(object):
"""Star detection using SEP (Source Extractor Python).

Provides the same interface as IndiAllSkyStars so it can be used as a
drop-in replacement in the processing pipeline. SEP estimates and
subtracts the sky background before extracting sources, which makes it
significantly more robust on real sky images than template matching.
"""

def __init__(self, config, mask=None):
self.config = config

self._sqm_mask_dict = mask

self._star_mask_dict = dict()
for binning in self._sqm_mask_dict.keys():
self._star_mask_dict[binning] = None

self._detectionThreshold = self.config.get('DETECT_STARS_SEP_THOLD', 5.0)

if self.config['IMAGE_FOLDER']:
self.image_dir = Path(self.config['IMAGE_FOLDER']).absolute()
else:
self.image_dir = Path(__file__).parent.parent.joinpath('html', 'images').absolute()


def detectObjects(self, original_data, binning):
if isinstance(self._star_mask_dict[binning], type(None)):
self._generateStarMask(original_data, binning)

# build SEP mask: nonzero = excluded (inverse of indi-allsky convention)
indi_mask = self._star_mask_dict[binning]
if indi_mask is not None:
sep_mask = (indi_mask == 0).astype(numpy.uint8)
else:
sep_mask = None

if len(original_data.shape) == 2:
grey = original_data
else:
grey = cv2.cvtColor(original_data, cv2.COLOR_BGR2GRAY)

data = numpy.ascontiguousarray(grey.astype(numpy.float32))

sep_start = time.time()

bkg = sep.Background(data, mask=sep_mask)
data_sub = data - bkg

try:
objects = sep.extract(data_sub, self._detectionThreshold, err=bkg.globalrms, mask=sep_mask)
except Exception as e:
logger.error('SEP extraction error: %s', e)
objects = []

sep_elapsed_s = time.time() - sep_start

# reject bloomed/saturated sources — their semi-major axis is much
# larger than a real point-source star on this sensor
max_star_radius = self.config.get('DETECT_STARS_SEP_MAX_RADIUS', 20)
objects = [obj for obj in objects if obj['a'] <= max_star_radius]

logger.info('Detected %d stars in %0.4f s', len(objects), sep_elapsed_s)

blobs = [(float(obj['x']), float(obj['y'])) for obj in objects]

self._drawCircles(original_data, objects)

return blobs


def _generateStarMask(self, img, binning):
logger.info('Generating mask based on SQM_ROI')

if not isinstance(self._sqm_mask_dict[binning], type(None)):
self._star_mask_dict[binning] = self._sqm_mask_dict[binning]
return

image_height, image_width = img.shape[:2]

mask = numpy.zeros((image_height, image_width), dtype=numpy.uint8)

sqm_roi = self.config.get('SQM_ROI', [])

try:
x1 = int(sqm_roi[0] / binning)
y1 = int(sqm_roi[1] / binning)
x2 = int(sqm_roi[2] / binning)
y2 = int(sqm_roi[3] / binning)
except IndexError:
logger.warning('Using central ROI for star detection')
sqm_fov_div = self.config.get('SQM_FOV_DIV', 4)
x1 = int((image_width / 2) - (image_width / sqm_fov_div))
y1 = int((image_height / 2) - (image_height / sqm_fov_div))
x2 = int((image_width / 2) + (image_width / sqm_fov_div))
y2 = int((image_height / 2) + (image_height / sqm_fov_div))

cv2.rectangle(mask, (x1, y1), (x2, y2), 255, cv2.FILLED)

self._star_mask_dict[binning] = mask


def _drawCircles(self, img, objects):
if not self.config.get('DETECT_DRAW'):
return

color_bgr = list(self.config['TEXT_PROPERTIES']['FONT_COLOR'])
color_bgr.reverse()

logger.info('Draw circles around objects')
for obj in objects:
cx = int(obj['x'])
cy = int(obj['y'])
r = max(4, int(obj['a'] * 3))
cv2.circle(img, (cx, cy), r, tuple(color_bgr), thickness=1)