class FrameC18n(object): def __init__(self, path, bin_min_exp=-4, bin_max_exp=2, num_bins=None): if not num_bins: num_bins = 1 + bin_max_exp - bin_min_exp self._path = path self._img_size = self._path.stat().st_size self._image_input = ImageInput.open(str(self._path)) if self._image_input is None: # TODO check that the problem *is* actually the file is not there raise FileNotFoundError( f"could not read image `{self._path}': {oiio.geterror()}") roi = self._image_input.spec().roi # n.b. ROI xend and yend are range-style 'one beyond the end' values self._x = roi.xbegin self._width = roi.xend - roi.xbegin self._y = roi.ybegin self._height = roi.yend - roi.ybegin self._overall_registers = Registers( f"registers for entire image", "overall", self._image_input.spec().channelnames) self.octants = {} for octant_key in Octant.keys(): self.octants[octant_key] = Octant(self._image_input.spec(), octant_key, bin_min_exp, bin_max_exp, num_bins) def tally(self): img_array = self._image_input.read_image() self._overall_registers.tally(img_array, np.full(img_array.shape[0:2], True)) for octant in self.octants.values(): octant.tally(img_array) def add_to_columns(self, columns): """Append the information in the frame c18n to a Pandas DataFrame Parameters ---------- columns : dict """ self._overall_registers.add_to_columns(columns) for octant in self.octants: self.octants[octant].add_to_columns(columns) def summarize(self, indent_level=0): summary = '' summary += "overall image statistics:\n" summary += self._overall_registers.summarize(indent_level + 1) for octant in self.octants.values(): if samples_in_octant := octant.samples_in_octant: summary += f"{' '*indent_level}statistics for {octant} ({samples_in_octant} samples):\n" summary += octant.summarize(indent_level + 1) return summary
class Octant(object): """ Class to hold spatial and statistical distribution of pixel values in a Cartesian octant. Parameters ---------- img_spec : OpenImageIO ImageSpec Describes the image to be analyzed octant_key : tuple of booleans Indicates whether an octant axis represents, in the space of pixel values, distance along the negative extent of that axis. (All octant data is stored with spatial and statistical data transformed to all-positive values; for the spatial data, the _to_first_octant_scalars attribute provides for transformatipon back to the original coordinate system. bin_min_exp : int Base-10 exponent of the largest negative value considered to not be 'overflow'. See documentation of the LogBins class for the gory details. bin_max_exp : int Base-10 exponent of the tinyest negative value considered to not be 'underflow'. See documentaionm of the LogBinds class for the gory details. num_bins : int Number of bins into which pixel values will be mapped """ @staticmethod def keys(): keys = [] for has_blue_neg in (False, True): for has_green_neg in (False, True): for has_red_neg in (False, True): keys.append((has_red_neg, has_green_neg, has_blue_neg)) return keys def _name(self, channel_names, spacer): name_pieces = [] negativities_and_names = zip(self._octant_key, channel_names) for negativity, name in negativities_and_names: name_pieces += [f"{name}{'-' if negativity else '+'}"] return spacer.join(name_pieces) def name(self): return self._name(['red', 'green', 'blue'], ', ') def label(self): return f"oct_{self._name(['r', 'g', 'b'], '')}" def _ix_for_octant(self, img_array): ix = np.full(img_array.shape[:2], True, dtype=np.bool) for chan, axis_negative_in_octant in enumerate(self._octant_key): chan_in_octant_ix = img_array[ ..., chan] < 0 if axis_negative_in_octant else img_array[..., chan] >= 0 np.logical_and(ix, chan_in_octant_ix, ix) return ix @staticmethod def _log10_edges(min_exp, max_exp, num_bins=None): if not num_bins: num_bins = 1 + max_exp - min_exp zero_anchor = np.array([0]) edge = 10**(np.linspace(min_exp, max_exp, num_bins, dtype=np.dtype('half'))) max_anchor = np.array([np.finfo('half').max]) edge = np.hstack([zero_anchor, edge, max_anchor]) return edge def __init__(self, img_spec, octant_key, bin_min_exp, bin_max_exp, num_bins): self._img_spec = img_spec self._octant_key = octant_key self._to_first_octant_scalars = [-1 if e else 1 for e in octant_key] self._edge = self._log10_edges(bin_min_exp, bin_max_exp, num_bins=num_bins) self.samples_in_octant = 0 self.hist3d = np.zeros((num_bins, num_bins, num_bins), dtype=np.uint) self._label = self.label() register_desc = f"registers for octant {self._label}" self._registers = Registers(register_desc, self._label, self._img_spec.channelnames) def _bin(self, img_array, octant_ix): bins = [self._edge] * 3 img_in_octant = img_array[octant_ix] img_in_octant *= self._to_first_octant_scalars self.hist3d, _ = np.histogramdd(img_in_octant, bins) def tally(self, img_array): octant_ix = self._ix_for_octant(img_array) self.samples_in_octant = np.sum(octant_ix) self._bin(img_array, octant_ix) self._registers.tally(img_array, octant_ix) def add_to_columns(self, columns): self._registers.add_to_columns(columns) def summarize(self, indent_level=0): return self._registers.summarize(indent_level)