Skip to content

Commit 0b40678

Browse files
authored
Merge pull request #1001 from aaronwmorris/dev
BREAKING CHANGE: Detection masks utilized prior to geometry changes
2 parents 3ac45eb + a65236d commit 0b40678

9 files changed

Lines changed: 303 additions & 18 deletions

File tree

Lines changed: 3 additions & 0 deletions
Loading

indi_allsky/flask/templates/base.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
<a href="{{ url_for('indi_allsky.image_processing_view') }}" class="dropdown-item">
8181
<img src="{{ url_for('indi_allsky.static', filename='svg/magic.svg') }}" width="16" height="16" alt="Log"><span class="ms-1 d-none d-sm-inline">Process FITS</span> </a>
8282
</li>
83+
<li>
84+
<a href="{{ url_for('indi_allsky.mask_view') }}" class="dropdown-item">
85+
<img src="{{ url_for('indi_allsky.static', filename='svg/layers.svg') }}" width="16" height="16" alt="Log"><span class="ms-1 d-none d-sm-inline">Mask Base</span> </a>
86+
</li>
8387
<li>
8488
<a href="{{ url_for('indi_allsky.log_view') }}" class="dropdown-item">
8589
<img src="{{ url_for('indi_allsky.static', filename='svg/terminal-fill.svg') }}" width="16" height="16" alt="Log"><span class="ms-1 d-none d-sm-inline">Log</span> </a>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{% extends 'base.html' %}
2+
3+
{% block title %}indi-allsky: Mask Base{% endblock %}
4+
5+
{% block head %}
6+
<meta charset="UTF-8">
7+
<style>
8+
#mask_img {
9+
width: 100%;
10+
height: auto;
11+
}
12+
</style>
13+
<script>
14+
var url = '{{ mask_image_uri }}';
15+
</script>
16+
{% endblock %}
17+
18+
{% block content %}
19+
<div class="row">
20+
<div class="col-12 text-center" style="font-size:10px">
21+
This image is the original camera output. It is not rotated, flipped, or cropped.
22+
</div>
23+
</div>
24+
25+
<div class="row">
26+
<div class="col-1"></div>
27+
<div class="col-10">
28+
<img id="mask_img">
29+
</div>
30+
</div>
31+
32+
<div class="row">
33+
<div class="col-12 text-center" style="font-size:10px">
34+
<span>Generated: {{ mask_date }}</span>
35+
</div>
36+
</div>
37+
38+
<div class="row">
39+
<div class="col-12 text-center">
40+
<span id="mask_download"></span>
41+
</div>
42+
</div>
43+
<script>
44+
function init() {
45+
$('#mask_img').attr('src', url + '?' + new Date().getTime());
46+
47+
$('#mask_download').html(
48+
$('<a />', {
49+
'href' : $('#mask_img').attr('src'),
50+
'rel' : 'noopener noreferrer',
51+
'download' : 'mask_base.png',
52+
}).html(
53+
$('<span />', {
54+
'text' : 'Download Mask Base',
55+
'class' : "badge pill bg-info text-dark",
56+
})
57+
)
58+
);
59+
}
60+
61+
$( document ).ready(function() {
62+
init();
63+
});
64+
65+
</script>
66+
67+
{% endblock %}

indi_allsky/flask/views.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,29 @@ def dispatch_request(self):
247247
return redirect(url_for('indi_allsky.index_view'))
248248

249249

250+
class MaskView(TemplateView):
251+
def get_context(self):
252+
context = super(MaskView, self).get_context()
253+
254+
mask_image_uri = Path('images/mask_base.png')
255+
256+
context['mask_image_uri'] = str(mask_image_uri)
257+
258+
259+
image_dir = Path(self.indi_allsky_config['IMAGE_FOLDER']).absolute()
260+
mask_image_p = image_dir.joinpath(mask_image_uri.name)
261+
262+
if mask_image_p.exists():
263+
mask_mtime = mask_image_p.stat().st_mtime
264+
mask_mtime_dt = datetime.fromtimestamp(mask_mtime)
265+
context['mask_date'] = mask_mtime_dt.strftime('%Y-%m-%d %H:%M:%S')
266+
else:
267+
context['mask_date'] = ''
268+
269+
270+
return context
271+
272+
250273
class CamerasView(TemplateView):
251274
def get_context(self):
252275
context = super(CamerasView, self).get_context()
@@ -4731,6 +4754,7 @@ def images_folder(path):
47314754
bp_allsky.add_url_rule('/darks', view_func=DarkFramesView.as_view('darks_view', template_name='darks.html'))
47324755
bp_allsky.add_url_rule('/processing', view_func=ImageProcessingView.as_view('image_processing_view', template_name='imageprocessing.html'))
47334756
bp_allsky.add_url_rule('/js/processing', view_func=JsonImageProcessingView.as_view('js_image_processing_view'))
4757+
bp_allsky.add_url_rule('/mask', view_func=MaskView.as_view('mask_view', template_name='mask.html'))
47344758

47354759
bp_allsky.add_url_rule('/public', view_func=PublicIndexView.as_view('public_index_view')) # redirect
47364760

indi_allsky/image.py

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def __init__(
8989
self.name = 'Image-{0:d}'.format(idx)
9090

9191
self.config = config
92+
9293
self.error_q = error_q
9394
self.image_q = image_q
9495
self.upload_q = upload_q
@@ -140,12 +141,15 @@ def __init__(
140141

141142
self.filename_t = 'ccd{0:d}_{1:s}.{2:s}'
142143

144+
self.generate_mask_base = True
145+
143146
self.target_adu_found = False
144147
self.current_adu_target = 0
145148
self.hist_adu = []
146149

147150
self.sqm_value = 0
148151

152+
self.image_count = 0
149153
self.metadata_count = 0
150154

151155
self.image_processor = ImageProcessor(
@@ -340,6 +344,9 @@ def processImage(self, i_dict):
340344
filename_p.unlink() # original file is no longer needed
341345

342346

347+
self.image_count += 1
348+
349+
343350
# use original value if not defined
344351
libcamera_black_level = image_data.get('libcamera_black_level', libcamera_black_level)
345352

@@ -494,6 +501,28 @@ def processImage(self, i_dict):
494501
adu, adu_average = self.calculate_exposure(adu, exposure)
495502

496503

504+
# generate a new mask base once the target ADU is found
505+
# this should only only fire once per restart
506+
if self.generate_mask_base and self.target_adu_found:
507+
self.generate_mask_base = False
508+
self.write_mask_base_img(self.image_processor.image)
509+
510+
511+
# line detection
512+
if self.night_v.value and self.config.get('DETECT_METEORS'):
513+
self.image_processor.detectLines()
514+
515+
516+
# star detection
517+
if self.night_v.value and self.config.get('DETECT_STARS', True):
518+
self.image_processor.detectStars()
519+
520+
521+
# additional draw code
522+
if self.config.get('DETECT_DRAW'):
523+
self.image_processor.drawDetections()
524+
525+
497526
if self.config.get('IMAGE_ROTATE'):
498527
self.image_processor.rotate_90()
499528

@@ -512,21 +541,6 @@ def processImage(self, i_dict):
512541
self.image_processor.flip_h()
513542

514543

515-
# line detection
516-
if self.night_v.value and self.config.get('DETECT_METEORS'):
517-
self.image_processor.detectLines()
518-
519-
520-
# star detection
521-
if self.night_v.value and self.config.get('DETECT_STARS', True):
522-
self.image_processor.detectStars()
523-
524-
525-
# additional draw code
526-
if self.config.get('DETECT_DRAW'):
527-
self.image_processor.drawDetections()
528-
529-
530544
# crop
531545
if self.config.get('IMAGE_CROP_ROI'):
532546
self.image_processor.crop_image()
@@ -1127,6 +1141,31 @@ def export_raw_image(self, i_ref, jpeg_exif=None):
11271141
self._miscUpload.s3_upload_raw(raw_entry, raw_metadata)
11281142

11291143

1144+
def write_mask_base_img(self, data):
1145+
logger.info('Generating new mask base')
1146+
f_tmpfile = tempfile.NamedTemporaryFile(mode='w+b', delete=False, suffix='.png')
1147+
f_tmpfile.close()
1148+
1149+
tmpfile_name = Path(f_tmpfile.name)
1150+
1151+
1152+
cv2.imwrite(str(tmpfile_name), data, [cv2.IMWRITE_PNG_COMPRESSION, self.config['IMAGE_FILE_COMPRESSION']['png']])
1153+
1154+
mask_file = self.image_dir.joinpath('mask_base.png')
1155+
1156+
try:
1157+
mask_file.unlink()
1158+
except FileNotFoundError:
1159+
pass
1160+
1161+
1162+
shutil.copy2(str(tmpfile_name), str(mask_file))
1163+
mask_file.chmod(0o644)
1164+
1165+
1166+
tmpfile_name.unlink()
1167+
1168+
11301169
def write_img(self, data, i_ref, camera, jpeg_exif=None):
11311170
f_tmpfile = tempfile.NamedTemporaryFile(mode='w+b', delete=False, suffix='.{0}'.format(self.config['IMAGE_FILE_TYPE']))
11321171
f_tmpfile.close()

indi_allsky/maskProcessing.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
### This is a mini image processor for masks
2+
### Masks are pre-rotation/flip/cropping and need these operations to be applied to processed images
3+
4+
#import time
5+
import cv2
6+
import logging
7+
8+
9+
logger = logging.getLogger('indi_allsky')
10+
11+
12+
class MaskProcessor(object):
13+
def __init__(
14+
self,
15+
config,
16+
bin_v,
17+
):
18+
19+
self.config = config
20+
self.bin_v = bin_v
21+
22+
self._image = None
23+
24+
25+
@property
26+
def image(self):
27+
return self._image
28+
29+
@image.setter
30+
def image(self, new_image):
31+
self._image = new_image
32+
33+
34+
def rotate_90(self):
35+
try:
36+
rotate_enum = getattr(cv2, self.config['IMAGE_ROTATE'])
37+
except AttributeError:
38+
logger.error('Unknown rotation option: %s', self.config['IMAGE_ROTATE'])
39+
return
40+
41+
self.image = cv2.rotate(self.image, rotate_enum)
42+
43+
44+
def rotate_angle(self):
45+
angle = self.config.get('IMAGE_ROTATE_ANGLE')
46+
47+
#rotate_start = time.time()
48+
49+
height, width = self.image.shape[:2]
50+
center_x = int(width / 2)
51+
center_y = int(height / 2)
52+
53+
rot = cv2.getRotationMatrix2D((center_x, center_y), int(angle), 1.0)
54+
55+
abs_cos = abs(rot[0, 0])
56+
abs_sin = abs(rot[0, 1])
57+
58+
bound_w = int(height * abs_sin + width * abs_cos)
59+
bound_h = int(height * abs_cos + width * abs_sin)
60+
61+
rot[0, 2] += bound_w / 2 - center_x
62+
rot[1, 2] += bound_h / 2 - center_y
63+
64+
self.image = cv2.warpAffine(self.image, rot, (bound_w, bound_h))
65+
66+
rot_height, rot_width = self.image.shape[:2]
67+
mod_height = rot_height % 2
68+
mod_width = rot_width % 2
69+
70+
if mod_height or mod_width:
71+
# width and height needs to be divisible by 2 for timelapse
72+
crop_height = rot_height - mod_height
73+
crop_width = rot_width - mod_width
74+
75+
self.image = self.image[
76+
0:crop_height,
77+
0:crop_width,
78+
]
79+
80+
81+
#processing_elapsed_s = time.time() - rotate_start
82+
#logger.warning('Rotation in %0.4f s', processing_elapsed_s)
83+
84+
85+
def flip(self, cv2_axis):
86+
self.image = cv2.flip(self.image, cv2_axis)
87+
88+
89+
def flip_v(self):
90+
self.flip(0)
91+
92+
93+
def flip_h(self):
94+
self.flip(1)
95+
96+
97+
def crop_image(self):
98+
# divide the coordinates by binning value
99+
x1 = int(self.config['IMAGE_CROP_ROI'][0] / self.bin_v.value)
100+
y1 = int(self.config['IMAGE_CROP_ROI'][1] / self.bin_v.value)
101+
x2 = int(self.config['IMAGE_CROP_ROI'][2] / self.bin_v.value)
102+
y2 = int(self.config['IMAGE_CROP_ROI'][3] / self.bin_v.value)
103+
104+
105+
self.image = self.image[
106+
y1:y2,
107+
x1:x2,
108+
]
109+
110+
#new_height, new_width = self.image.shape[:2]
111+
#logger.info('New cropped size: %d x %d', new_width, new_height)
112+
113+

indi_allsky/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__version__ = "7.5"
1+
__version__ = "7.6"
22
__config_level__ = "20231018.0"
33

0 commit comments

Comments
 (0)