Repository navigation
Delete scaling keywords from header when unsigned int data converted to another type #5053
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,23 +99,35 @@ def __init__(self, data=None, header=None, do_not_scale_image_data=False, | |
| self._uint = uint | ||
| self._scale_back = scale_back | ||
|
|
||
| # Keep track of whether BZERO/BSCALE were set from the header so that | ||
| # values for self._orig_bzero and self._orig_bscale can be set | ||
| # properly, if necessary, once the data has been set. | ||
| bzero_in_header = 'BZERO' in self._header | ||
| bscale_in_header = 'BSCALE' in self._header | ||
| self._bzero = self._header.get('BZERO', 0) | ||
| self._bscale = self._header.get('BSCALE', 1) | ||
|
|
||
| # Save off other important values from the header needed to interpret | ||
| # the image data | ||
| self._axes = [self._header.get('NAXIS' + str(axis + 1), 0) | ||
| for axis in range(self._header.get('NAXIS', 0))] | ||
| self._bitpix = self._header.get('BITPIX', 8) | ||
|
|
||
| # Not supplying a default for BITPIX makes sense because BITPIX | ||
| # is either in the header or should be determined from the dtype of | ||
| # the data (which occurs when the data is set). | ||
| self._bitpix = self._header.get('BITPIX') | ||
| self._gcount = self._header.get('GCOUNT', 1) | ||
| self._pcount = self._header.get('PCOUNT', 0) | ||
| self._blank = None if ignore_blank else self._header.get('BLANK') | ||
| self._verify_blank() | ||
|
|
||
| self._orig_bitpix = self._bitpix | ||
| self._orig_blank = self._header.get('BLANK') | ||
|
|
||
| # These get set again below, but need to be set to sensible defaults | ||
| # here. | ||
| self._orig_bzero = self._bzero | ||
| self._orig_bscale = self._bscale | ||
| self._orig_blank = self._header.get('BLANK') | ||
|
|
||
| # Set the name attribute if it was provided (if this is an ImageHDU | ||
| # this will result in setting the EXTNAME keyword of the header as | ||
|
|
@@ -135,9 +147,31 @@ def __init__(self, data=None, header=None, do_not_scale_image_data=False, | |
| self._data_needs_rescale = True | ||
| return | ||
| else: | ||
| # Setting data will set set _bitpix, _bzero, and _bscale to the | ||
| # appropriate BITPIX for the data, and always sets _bzero=0 and | ||
| # _bscale=1. | ||
| self.data = data | ||
| self.update_header() | ||
|
|
||
| # Check again for BITPIX/BSCALE/BZERO in case they changed when the | ||
| # data was assigned. This can happen, for example, if the input | ||
| # data is an unsigned int numpy array. | ||
| self._bitpix = self._header.get('BITPIX') | ||
|
|
||
| # Do not provide default values for BZERO and BSCALE here because | ||
| # the keywords will have been deleted in the header if appropriate | ||
| # after scaling. We do not want to put them back in if they | ||
| # should not be there. | ||
| self._bzero = self._header.get('BZERO') | ||
| self._bscale = self._header.get('BSCALE') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, (and I'm not saying it should be done for this PR, but sooner rather than later) this should be inverted. What's going on is that in some places bzero/bscale are being handled through the header. For example Basically what I'm saying is the header should be ignored as early as possible when it comes to managing the state of these attributes. The bzero and bscale should be managed only through the attributes self._bzero and self._bzero should at all times contain the values that should go in the header if the header were updated to reflect the current data (which it should be when self.data is set). For unsigned ints this means
Forgive me if all of the above is obvious to you already. I'm just trying to sort out my own thoughts. This PR looks like a step in the right direction, but there's still a bit of a mess involving management of these attributes that makes the code more complicated and confusing than it needs to be. Not saying that needs to be addressed right this second.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not at all -- what I was missing was some sense of what the internal behavior should be, this is very helpful. |
||
|
|
||
| # Handle case where there was no BZERO/BSCALE in the initial header | ||
| # but there should be a BSCALE/BZERO now that the data has been set. | ||
| if not bzero_in_header: | ||
| self._orig_bzero = self._bzero | ||
| if bscale_in_header: | ||
| self._orig_bscale = self._bscale | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes sense. |
||
|
|
||
| @classmethod | ||
| def match_header(cls, header): | ||
| """ | ||
|
|
@@ -217,8 +251,10 @@ def data(self, data): | |
| return | ||
| else: | ||
| self._data_replaced = True | ||
| was_unsigned = _is_pseudo_unsigned(self.__dict__['data'].dtype) | ||
| else: | ||
| self._data_replaced = True | ||
| was_unsigned = False | ||
|
|
||
| if data is not None and not isinstance(data, np.ndarray): | ||
| # Try to coerce the data into a numpy array--this will work, on | ||
|
|
@@ -233,10 +269,11 @@ def data(self, data): | |
| self._modified = True | ||
|
|
||
| if isinstance(data, np.ndarray): | ||
| # Set new values of bitpix, bzero, and bscale now, but wait to | ||
| # revise original values until header is updated. | ||
| self._bitpix = DTYPE2BITPIX[data.dtype.name] | ||
| self._orig_bitpix = self._bitpix | ||
| self._bscale = self._orig_bscale = 1 | ||
| self._bzero = self._orig_bzero = 0 | ||
| self._bscale = 1 | ||
| self._bzero = 0 | ||
| self._blank = None | ||
| self._axes = list(data.shape) | ||
| self._axes.reverse() | ||
|
|
@@ -245,7 +282,26 @@ def data(self, data): | |
| else: | ||
| raise ValueError('not a valid data array') | ||
|
|
||
| # Update the header, including adding BZERO/BSCALE if new data is | ||
| # unsigned. Does not change the values of self._bitpix, | ||
| # self._orig_bitpix, etc. | ||
| self.update_header() | ||
| if (data is not None and was_unsigned): | ||
| self._update_header_scale_info(data.dtype) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If anything, for unsigned data BZERO and BSCALE should be kept in the header, because that's the only way the unsigned ints can be meaningfully represented in FITS. So in that case keeping them in the header accurately reflects the data (the only reason they are deleted in the generic case is that once the scale factors are applied to the data, they are no longer needed in the header to represent the data unless the data is scaled back before writing).
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right; this ends up deleting those keywords only when:
So BZERO and BSCALE are present when the data is unsigned but are removed when the data is set to something whose type is not unsigned. |
||
|
|
||
| # Keep _orig_bitpix as it was until header update is done, then | ||
| # set it, to allow easier handling of the case of unsigned | ||
| # integer data being converted to something else. Setting these here | ||
| # is needed only for the case do_not_scale_image_data=True when | ||
| # setting the data to unsigned int. | ||
|
|
||
| # If necessary during initialization, i.e. if BSCALE and BZERO were | ||
| # not in the header but the data was unsigned, the attributes below | ||
| # will be update in __init__. | ||
| self._orig_bitpix = self._bitpix | ||
| self._orig_bscale = self._bscale | ||
| self._orig_bzero = self._bzero | ||
|
|
||
|
|
||
| # returning the data signals to lazyproperty that we've already handled | ||
| # setting self.__dict__['data'] | ||
|
|
@@ -313,11 +369,30 @@ def update_header(self): | |
| if 'BLANK' in self._header: | ||
| self._blank = self._header['BLANK'] | ||
|
|
||
| # Add BSCALE/BZERO to header if data is unsigned int. | ||
| self._update_uint_scale_keywords() | ||
|
|
||
| self._modified = False | ||
|
|
||
| def _update_header_scale_info(self, dtype=None): | ||
| """ | ||
| Delete BSCALE/BZERO from header if necessary. | ||
| """ | ||
|
|
||
| # Note that _dtype_for_bitpix determines the dtype based on the | ||
| # "original" values of bitpix, bscale, and bzero, stored in | ||
| # self._orig_bitpix, etc. It contains the logic for determining which | ||
| # special cases of BZERO/BSCALE, if any, are auto-detected as following | ||
| # the FITS unsigned int convention. | ||
|
|
||
| # Added original_was_unsigned with the intent of facilitating the | ||
| # special case of do_not_scale_image_data=True and uint=True | ||
| # eventually. | ||
| if self._dtype_for_bitpix() is not None: | ||
| original_was_unsigned = self._dtype_for_bitpix().kind == 'u' | ||
| else: | ||
| original_was_unsigned = False | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like you were going to do something interesting here but haven't gotten around to it yet. Maybe something to do with my comment here? Also regarding |
||
|
|
||
| if (self._do_not_scale_image_data or | ||
| (self._orig_bzero == 0 and self._orig_bscale == 1)): | ||
| return | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks to me like
update_header()already gets called when settingself.data. See https://github.com/mwcraig/astropy/blob/0627686c4c422c7f183e552d70d320a0f1540a5f/astropy/io/fits/hdu/image.py#L288So this can probably be removed (if somehow things break unless it's called twice then that would be something to look into as well).
Basically most of this business of setting
self._bitpixshouldn't be happening here in__init__at all, and should just happen when settingself.data =...(since that can happen outside of__init__as well).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, passes locally, will fix in separate PR addressing the remaining comments too.