Skip to content

Commit

Permalink
Merge pull request #898 from aaronwmorris/dev
Browse files Browse the repository at this point in the history
New 16-bit CLAHE contrast enhance
  • Loading branch information
aaronwmorris authored Aug 18, 2023
2 parents 1abf556 + 47178f0 commit eb06c9f
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 37 deletions.
1 change: 1 addition & 0 deletions indi_allsky/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class IndiAllSkyConfigBase(object):
"DAYTIME_TIMELAPSE" : True,
"DAYTIME_CONTRAST_ENHANCE" : False,
"NIGHT_CONTRAST_ENHANCE" : False,
"CONTRAST_ENHANCE_16BIT" : False,
"CLAHE_CLIPLIMIT" : 3.0,
"CLAHE_GRIDSIZE" : 8,
"NIGHT_SUN_ALT_DEG" : -6.0,
Expand Down
8 changes: 8 additions & 0 deletions indi_allsky/filetransfer/boto3_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
#from datetime import timedelta
import socket
import time
import urllib3.exceptions
from botocore.client import Config
import boto3
import botocore.exceptions
import boto3.exceptions
import logging

Expand Down Expand Up @@ -121,6 +123,12 @@ def put(self, *args, **kwargs):
raise ConnectionFailure(str(e)) from e
except ConnectionRefusedError as e:
raise ConnectionFailure(str(e)) from e
except botocore.exceptions.ConnectTimeoutError as e:
raise ConnectionFailure(str(e)) from e
except urllib3.exceptions.ReadTimeoutError as e:
raise ConnectionFailure(str(e)) from e
except botocore.exceptions.ReadTimeoutError as e:
raise ConnectionFailure(str(e)) from e
except boto3.exceptions.S3UploadFailedError as e:
raise TransferFailure(str(e)) from e

Expand Down
1 change: 1 addition & 0 deletions indi_allsky/flask/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2035,6 +2035,7 @@ class IndiAllskyConfigForm(FlaskForm):
DAYTIME_TIMELAPSE = BooleanField('Daytime Timelapse')
DAYTIME_CONTRAST_ENHANCE = BooleanField('Daytime Contrast Enhance')
NIGHT_CONTRAST_ENHANCE = BooleanField('Night time Contrast Enhance')
CONTRAST_ENHANCE_16BIT = BooleanField('16-bit Contrast Enhance')
CLAHE_CLIPLIMIT = FloatField('CLAHE Clip Limit', validators=[CLAHE_CLIPLIMIT_validator])
CLAHE_GRIDSIZE = IntegerField('CLAHE Grid Size', validators=[CLAHE_GRIDSIZE_validator])
NIGHT_SUN_ALT_DEG = FloatField('Sun altitude', validators=[NIGHT_SUN_ALT_DEG_validator])
Expand Down
22 changes: 21 additions & 1 deletion indi_allsky/flask/templates/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,8 @@
<div class="col-sm-8">Enable daytime timelapse generation</div>
</div>

<hr />

<div class="form-group row">
<div class="col-sm-2">
{{ form_config.DAYTIME_CONTRAST_ENHANCE.label }}
Expand Down Expand Up @@ -487,7 +489,7 @@
<div id="CLAHE_CLIPLIMIT-error" class="invalid-feedback text-danger" style="display: none;"></div>
</div>
<div class="col-sm-8">
<a href="https://docs.opencv.org/4.x/d5/daf/tutorial_py_histogram_equalization.html" target="_blank">OpenCV CLAHE Tutorial</a>
<div>Increase value for more contrast</div>
</div>
</div>

Expand All @@ -500,6 +502,23 @@
<div id="CLAHE_GRIDSIZE-error" class="invalid-feedback text-danger" style="display: none;"></div>
</div>
<div class="col-sm-8">
<div><a href="https://docs.opencv.org/4.x/d5/daf/tutorial_py_histogram_equalization.html" target="_blank">OpenCV CLAHE Tutorial</a></div>
</div>
</div>

<div class="form-group row">
<div class="col-sm-2">
{{ form_config.CONTRAST_ENHANCE_16BIT.label }}
</div>
<div class="col-sm-2">
<div class="form-switch">
{{ form_config.CONTRAST_ENHANCE_16BIT(class='form-check-input') }}
<div id="CONTRAST_ENHANCE_16BIT-error" class="invalid-feedback text-danger" style="display: none;"></div>
</div>
</div>
<div class="col-sm-8">
<div>Perform CLAHE in 16-bit mode (early in order of operations)</div>
<div><span class="badge rounded-pill bg-warning text-dark">Warning</span> Requires minimum 2GB of RAM (for 16-bit mode)</div>
</div>
</div>

Expand Down Expand Up @@ -3041,6 +3060,7 @@
'DAYTIME_TIMELAPSE',
'DAYTIME_CONTRAST_ENHANCE',
'NIGHT_CONTRAST_ENHANCE',
'CONTRAST_ENHANCE_16BIT',
'IMAGE_STRETCH__MODE1_ENABLE',
'IMAGE_STRETCH__SPLIT',
'IMAGE_STRETCH__MOONMODE',
Expand Down
2 changes: 2 additions & 0 deletions indi_allsky/flask/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,7 @@ def get_context(self):
'DAYTIME_TIMELAPSE' : self.indi_allsky_config.get('DAYTIME_TIMELAPSE', True),
'DAYTIME_CONTRAST_ENHANCE' : self.indi_allsky_config.get('DAYTIME_CONTRAST_ENHANCE', False),
'NIGHT_CONTRAST_ENHANCE' : self.indi_allsky_config.get('NIGHT_CONTRAST_ENHANCE', False),
'CONTRAST_ENHANCE_16BIT' : self.indi_allsky_config.get('CONTRAST_ENHANCE_16BIT', False),
'CLAHE_CLIPLIMIT' : self.indi_allsky_config.get('CLAHE_CLIPLIMIT', 3.0),
'CLAHE_GRIDSIZE' : self.indi_allsky_config.get('CLAHE_GRIDSIZE', 8),
'NIGHT_SUN_ALT_DEG' : self.indi_allsky_config.get('NIGHT_SUN_ALT_DEG', -6.0),
Expand Down Expand Up @@ -1298,6 +1299,7 @@ def dispatch_request(self):
self.indi_allsky_config['DAYTIME_TIMELAPSE'] = bool(request.json['DAYTIME_TIMELAPSE'])
self.indi_allsky_config['DAYTIME_CONTRAST_ENHANCE'] = bool(request.json['DAYTIME_CONTRAST_ENHANCE'])
self.indi_allsky_config['NIGHT_CONTRAST_ENHANCE'] = bool(request.json['NIGHT_CONTRAST_ENHANCE'])
self.indi_allsky_config['CONTRAST_ENHANCE_16BIT'] = bool(request.json['CONTRAST_ENHANCE_16BIT'])
self.indi_allsky_config['CLAHE_CLIPLIMIT'] = float(request.json['CLAHE_CLIPLIMIT'])
self.indi_allsky_config['CLAHE_GRIDSIZE'] = int(request.json['CLAHE_GRIDSIZE'])
self.indi_allsky_config['NIGHT_SUN_ALT_DEG'] = float(request.json['NIGHT_SUN_ALT_DEG'])
Expand Down
109 changes: 73 additions & 36 deletions indi_allsky/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,15 @@ def processImage(self, i_dict):
self.image_processor.stretch()


if self.config.get('CONTRAST_ENHANCE_16BIT'):
if not self.night_v.value and self.config['DAYTIME_CONTRAST_ENHANCE']:
# Contrast enhancement during the day
self.image_processor.contrast_clahe_16bit()
elif self.night_v.value and self.config['NIGHT_CONTRAST_ENHANCE']:
# Contrast enhancement during night
self.image_processor.contrast_clahe_16bit()


self.image_processor.convert_16bit_to_8bit()


Expand Down Expand Up @@ -537,12 +546,13 @@ def processImage(self, i_dict):
self.image_processor.saturation_adjust()


if not self.night_v.value and self.config['DAYTIME_CONTRAST_ENHANCE']:
# Contrast enhancement during the day
self.image_processor.contrast_clahe()
elif self.night_v.value and self.config['NIGHT_CONTRAST_ENHANCE']:
# Contrast enhancement during night
self.image_processor.contrast_clahe()
if not self.config.get('CONTRAST_ENHANCE_16BIT'):
if not self.night_v.value and self.config['DAYTIME_CONTRAST_ENHANCE']:
# Contrast enhancement during the day
self.image_processor.contrast_clahe()
elif self.night_v.value and self.config['NIGHT_CONTRAST_ENHANCE']:
# Contrast enhancement during night
self.image_processor.contrast_clahe()


self.image_processor.colorize()
Expand Down Expand Up @@ -957,36 +967,9 @@ def export_raw_image(self, i_ref, jpeg_exif=None):
# nothing to scale
scaled_data = data
elif i_ref['image_bitpix'] == 16:
if max_bit_depth == 8:
logger.info('Upscaling data from 8 to 16 bit')
scaled_data = numpy.left_shift(data, 8)
elif max_bit_depth == 9:
logger.info('Upscaling data from 9 to 16 bit')
scaled_data = numpy.left_shift(data, 7)
elif max_bit_depth == 10:
logger.info('Upscaling data from 10 to 16 bit')
scaled_data = numpy.left_shift(data, 6)
elif max_bit_depth == 11:
logger.info('Upscaling data from 11 to 16 bit')
scaled_data = numpy.left_shift(data, 5)
elif max_bit_depth == 12:
logger.info('Upscaling data from 12 to 16 bit')
scaled_data = numpy.left_shift(data, 4)
elif max_bit_depth == 13:
logger.info('Upscaling data from 13 to 16 bit')
scaled_data = numpy.left_shift(data, 3)
elif max_bit_depth == 14:
logger.info('Upscaling data from 14 to 16 bit')
scaled_data = numpy.left_shift(data, 2)
elif max_bit_depth == 15:
logger.info('Upscaling data from 15 to 16 bit')
scaled_data = numpy.left_shift(data, 1)
elif max_bit_depth == 16:
# nothing to scale
scaled_data = data
else:
# assume 16 bit
scaled_data = data
logger.info('Upscaling data from %d to 16 bit', max_bit_depth)
shift_factor = 16 - max_bit_depth
scaled_data = numpy.left_shift(data, shift_factor)
else:
raise Exception('Unsupported bit depth')

Expand Down Expand Up @@ -2562,6 +2545,8 @@ def contrast_clahe(self):
# disable processing in focus mode
return

logger.info('Performing CLAHE contrast enhance')

clip_limit = self.config.get('CLAHE_CLIPLIMIT', 3.0)
grid_size = self.config.get('CLAHE_GRIDSIZE', 8)

Expand All @@ -2585,6 +2570,54 @@ def contrast_clahe(self):
self.image = cv2.cvtColor(new_lab, cv2.COLOR_LAB2BGR)


def contrast_clahe_16bit(self):
if self.focus_mode:
# disable processing in focus mode
return

logger.info('Performing 16-bit CLAHE contrast enhance')

clip_limit = self.config.get('CLAHE_CLIPLIMIT', 3.0)
grid_size = self.config.get('CLAHE_GRIDSIZE', 8)


### ohhhh, contrasty
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(grid_size, grid_size))


if len(self.image.shape) == 2:
# mono
self.image = clahe.apply(self.image)
return


if self._max_bit_depth == 8:
numpy_dtype = numpy.uint8
else:
numpy_dtype = numpy.uint16


max_value = 2 ** self._max_bit_depth

# float32 normalized values
norm_image = (self.image / max_value).astype(numpy.float32)


# color, apply to luminance
# cvtColor() only accepts uint8 and normalized float32
lab = cv2.cvtColor(norm_image, cv2.COLOR_BGR2LAB)

# clahe only accepts uint8 and uint16
# luminance is a float between 0-100, which needs to be remapped ot a 16bit int
cl_u16 = clahe.apply((lab[:, :, 0] * 650).astype(numpy_dtype)) # a little less than 65535 / 100

# map luminance back to 0-100
lab[:, :, 0] = (cl_u16 / 656).astype(numpy.float32) # trying to prevent artifiacts

# convert back to uint8 or uint16
self.image = (cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) * max_value).astype(numpy_dtype)


def colorize(self):
if len(self.image.shape) == 3:
# already color
Expand Down Expand Up @@ -3470,6 +3503,10 @@ def _processLabelComment(self, line):


def stretch(self):
if self.focus_mode:
# disable processing in focus mode
return

stretched_image, is_stretched = self._stretch.main(self.image, self.max_bit_depth)


Expand Down

0 comments on commit eb06c9f

Please sign in to comment.