Exemple #1
0
class ImagePropertiesTable(ImageGeometryTable):
    """Properties table for images, with geometry and additional statistical information.
    Allows automatic handling of tags in filenames, e.g., ZxYxX_u16be.
    """
    @atable.column_function([
        atable.ColumnProperties(name="sample_min", label="Min sample value"),
        atable.ColumnProperties(name="sample_max", label="Max sample value")
    ])
    def set_sample_extrema(self, file_path, row):
        array = load_array_bsq(file_or_path=file_path,
                               image_properties_row=row).flatten()
        row["sample_min"], row["sample_max"] = array.min(), array.max()
        if row["float"] == False:
            assert row["sample_min"] == int(row["sample_min"])
            assert row["sample_max"] == int(row["sample_max"])
            row["sample_min"] = int(row["sample_min"])
            row["sample_max"] = int(row["sample_max"])

    @atable.column_function("dynamic_range_bits", label="Dynamic range (bits)")
    def set_dynamic_range_bits(self, file_path, row):
        if row["float"] is True:
            range_len = 8 * row["bytes_per_sample"]
        else:
            range_len = int(row["sample_max"]) - int(row["sample_min"])
        assert range_len >= 0, (file_path, row["sample_max"],
                                row["sample_min"], range_len)
        row[_column_name] = max(1, math.ceil(math.log2(range_len + 1)))

    @atable.column_function("entropy_1B_bps",
                            label="Entropy (bps, 1-byte samples)",
                            plot_min=0,
                            plot_max=8)
    def set_file_1B_entropy(self, file_path, row):
        """Return the zero-order entropy of the data in file_path (1-byte samples are assumed)
        """
        row[_column_name] = entropy(
            np.fromfile(file_path, dtype="uint8").flatten())

    @atable.column_function("entropy_2B_bps",
                            label="Entropy (bps, 2-byte samples)",
                            plot_min=0,
                            plot_max=16)
    def set_file_2B_entropy(self, file_path, row):
        """Set the zero-order entropy of the data in file_path (2-byte samples are assumed)
        if bytes_per_sample is a multiple of 2, otherwise the column is set to -1
        """
        if row["bytes_per_sample"] % 2 != 0:
            row[_column_name] = -1
        else:
            row[_column_name] = entropy(
                np.fromfile(file_path, dtype=np.uint16).flatten())

    @atable.column_function(
        [f"byte_value_{s}" for s in ["min", "max", "avg", "std"]])
    def set_byte_value_extrema(self, file_path, row):
        contents = np.fromfile(file_path, dtype="uint8")
        row["byte_value_min"] = contents.min()
        row["byte_value_max"] = contents.max()
        row["byte_value_avg"] = contents.mean()
        row["byte_value_std"] = contents.std()
Exemple #2
0
class ImageGeometryTable(sets.FilePropertiesTable):
    """Basic properties table for images, including geometry.
    Allows automatic handling of tags in filenames, e.g., ZxYxX_u16be.
    """
    @atable.column_function("bytes_per_sample",
                            label="Bytes per sample",
                            plot_min=0)
    def set_bytes_per_sample(self, file_path, row):
        if any(s in file_path for s in ("u8be", "u8le", "s8be", "s8le")):
            row[_column_name] = 1
        elif any(s in file_path
                 for s in ("u16be", "u16le", "s16be", "s16le", "f16")):
            row[_column_name] = 2
        elif any(s in file_path
                 for s in ("u32be", "u32le", "s32be", "s32le", "f32")):
            row[_column_name] = 4
        elif any(s in file_path for s in ("f64")):
            row[_column_name] = 8
        else:
            raise sets.UnkownPropertiesException(
                f"Unknown {_column_name} for {file_path}")

    @atable.column_function("float", label="Floating point data?")
    def set_float(self, file_path, row):
        if any(s in file_path for s in ("u8be", "u8le", "s8be", "s8le",
                                        "u16be", "u16le", "s16be", "s16le",
                                        "u32be", "u32le", "s32be", "s32le")):
            row[_column_name] = False
        elif any(s in file_path for s in ("f16", "f32", "f64")):
            row[_column_name] = True
        else:
            raise sets.UnkownPropertiesException(
                f"Unknown {_column_name} from {file_path}")

    @atable.column_function("signed", label="Signed samples")
    def set_signed(self, file_path, row):
        if any(s in file_path
               for s in ("u8be", "u16be", "u16le", "u32be", "u32le")):
            row[_column_name] = False
        elif any(s in file_path for s in ("s8be", "s16be", "s16le", "s32be",
                                          "s32le", "f16", "f32", "f64")):
            row[_column_name] = True
        else:
            raise sets.UnkownPropertiesException(
                f"Unknown {_column_name} for {file_path}")

    @atable.column_function("big_endian", label="Big endian?")
    def set_big_endian(self, file_path, row):
        if any(s in file_path
               for s in ("u8be", "u16be", "u32be", "s8be", "s16be", "s32be")):
            row[_column_name] = True
        elif any(s in file_path for s in ("u8le", "u16le", "u32le", "s8le",
                                          "s16le", "s32le")):
            row[_column_name] = False
        elif any(s in file_path for s in ("f16", "f32", "f64")):
            row[_column_name] = True
        else:
            raise sets.UnkownPropertiesException(
                f"Unknown {_column_name} for {file_path}")

    @atable.column_function("samples", label="Sample count", plot_min=0)
    def set_samples(self, file_path, row):
        """Set the number of samples in the image
        """
        assert row["size_bytes"] % row["bytes_per_sample"] == 0
        row[_column_name] = row["size_bytes"] // row["bytes_per_sample"]

    @atable.column_function([
        atable.ColumnProperties(name="width", label="Width", plot_min=1),
        atable.ColumnProperties(name="height", label="Height", plot_min=1),
        atable.ColumnProperties(name="component_count",
                                label="Components",
                                plot_min=1),
    ])
    def set_image_geometry(self, file_path, row):
        """Obtain the image's geometry (width, height and number of components)
        based on the filename tags (and possibly its size)
        """
        file_path_to_geometry_dict(file_path=file_path, existing_dict=row)
class CompressionExperiment(experiment.Experiment):
    """This class allows seamless execution of compression experiments.

    In the functions decorated with @atable,column_function, the row argument
    contains two magic properties, compression_results and decompression_results.
    These give access to the :class:`CompressionResults` and :class:`DecompressionResults`
    instances resulting respectively from compressing and decompressing
    according to the row index parameters. The paths referenced in the compression
    and decompression results are valid while the row is being processed, and
    are disposed of afterwards.
    Also, the image_info_row attribute gives access to the image metainformation
    (e.g., geometry)
    """
    default_file_properties_table_class = enb.isets.ImagePropertiesTable
    check_lossless = True

    class RowWrapper:
        """Rows passed as arguments to the table column functions of CompressionExperiment
        subclasses are of this type. This allows accessing the compression_results and
        decompression_results properties (see the CompressionResults and DecompressionResults classes), 
        which automatically compress and decompress
        the image with the appropriate codec. Row names are set and retrieved normally
        with a dict-like syntax.
        """

        def __init__(self, file_path, codec, image_info_row, row):
            self.file_path = file_path
            self.codec = codec
            self.image_info_row = image_info_row
            self.row = row
            self._compression_results = None
            self._decompression_results = None

        @property
        def compression_results(self):
            """Perform the actual compression experiment for the selected row.
            """
            if self._compression_results is None:
                _, tmp_compressed_path = tempfile.mkstemp(
                    dir=options.base_tmp_dir,
                    prefix=f"compressed_{os.path.basename(self.file_path)}_")
                try:
                    measured_times = []

                    if options.verbose > 1:
                        print(f"[E]xecuting compression {self.codec.name} on {self.file_path} "
                              f"[{options.repetitions} times]")
                    for repetition_index in range(options.repetitions):
                        if options.verbose > 2:
                            print(f"[E]xecuting compression {self.codec.name} on {self.file_path} "
                                  f"[rep{repetition_index + 1}/{options.repetitions}]")
                        time_before = time.time()
                        self._compression_results = self.codec.compress(original_path=self.file_path,
                                                                        compressed_path=tmp_compressed_path,
                                                                        original_file_info=self.image_info_row)

                        if not os.path.isfile(tmp_compressed_path) \
                                or os.path.getsize(tmp_compressed_path) == 0:
                            raise CompressionException(
                                original_path=self.file_path, compressed_path=tmp_compressed_path,
                                file_info=self.image_info_row,
                                output=f"Compression didn't produce a file (or it was empty) {self.file_path}")

                        wall_compression_time = time.time() - time_before
                        if self._compression_results is None:
                            if options.verbose > 2:
                                print(f"[W]arning: codec {self.codec.name} did not report execution times. "
                                      f"Using wall clock instead (might be inaccurate)")
                            self._compression_results = self.codec.compression_results_from_paths(
                                original_path=self.file_path, compressed_path=tmp_compressed_path)
                            self._compression_results.compression_time_seconds = wall_compression_time

                        measured_times.append(self._compression_results.compression_time_seconds)
                        if repetition_index < options.repetitions - 1:
                            os.remove(tmp_compressed_path)

                    self._compression_results.compression_time_seconds = sum(measured_times) / len(measured_times)
                except Exception as ex:
                    os.remove(tmp_compressed_path)
                    raise ex

            return self._compression_results

        @property
        def decompression_results(self):
            """Perform the actual decompression experiment for the selected row.
            """
            if self._decompression_results is None:
                _, tmp_reconstructed_path = tempfile.mkstemp(
                    prefix=f"reconstructed_{os.path.basename(self.file_path)}",
                    dir=options.base_tmp_dir)
                try:
                    measured_times = []
                    if options.verbose > 1:
                        print(f"[E]xecuting decompression {self.codec.name} on {self.file_path} "
                              f"[{options.repetitions} times]")
                    for repetition_index in range(options.repetitions):
                        if options.verbose > 2:
                            print(f"[E]xecuting decompression {self.codec.name} on {self.file_path} "
                                  f"[rep{repetition_index + 1}/{options.repetitions}]")

                        time_before = time.time()
                        self._decompression_results = self.codec.decompress(
                            compressed_path=self.compression_results.compressed_path,
                            reconstructed_path=tmp_reconstructed_path,
                            original_file_info=self.image_info_row)

                        wall_decompression_time = time.time() - time_before
                        if self._decompression_results is None:
                            if options.verbose > 2:
                                print(f"[W]arning: codec {self.codec.name} did not report execution times. "
                                      f"Using wall clock instead (might be inaccurate)")
                            self._decompression_results = self.codec.decompression_results_from_paths(
                                compressed_path=self.compression_results.compressed_path,
                                reconstructed_path=tmp_reconstructed_path)
                            self._decompression_results.decompression_time_seconds = wall_decompression_time

                        if not os.path.isfile(tmp_reconstructed_path) or os.path.getsize(
                                self._decompression_results.reconstructed_path) == 0:
                            raise CompressionException(
                                original_path=self.compression_results.file_path,
                                compressed_path=self.compression_results.compressed_path,
                                file_info=self.image_info_row,
                                output=f"Decompression didn't produce a file (or it was empty)"
                                       f" {self.compression_results.file_path}")

                        measured_times.append(self._decompression_results.decompression_time_seconds)
                        if repetition_index < options.repetitions - 1:
                            os.remove(tmp_reconstructed_path)
                    self._decompression_results.decompression_time_seconds = sum(measured_times) / len(measured_times)
                except Exception as ex:
                    os.remove(tmp_reconstructed_path)
                    raise ex

            return self._decompression_results

        @property
        def numpy_dtype(self):
            """Get the numpy dtype corresponding to the original image's data format
            """
            return isets.iproperties_row_to_numpy_dtype(self.image_info_row)

        def __getitem__(self, item):
            return self.row[item]

        def __setitem__(self, key, value):
            self.row[key] = value

        def __delitem__(self, key):
            del self.row[key]

        def __contains__(self, item):
            return item in self.row

        def __del__(self):
            if self._compression_results is not None:
                try:
                    os.remove(self._compression_results.compressed_path)
                except OSError:
                    pass
            if self._decompression_results is not None:
                try:
                    os.remove(self._decompression_results.reconstructed_path)
                except OSError:
                    pass

    def __init__(self, codecs,
                 dataset_paths=None,
                 csv_experiment_path=None,
                 csv_dataset_path=None,
                 dataset_info_table=None,
                 overwrite_file_properties=False,
                 parallel_dataset_property_processing=None,
                 reconstructed_dir_path=None,
                 compressed_copy_dir_path=None):
        """
        :param codecs: list of :py:class:`AbstractCodec` instances. Note that
          codecs are compatible with the interface of :py:class:`ExperimentTask`.
        :param dataset_paths: list of paths to the files to be used as input for compression.
          If it is None, this list is obtained automatically from the configured
          base dataset dir.
        :param csv_experiment_path: if not None, path to the CSV file giving persistence
          support to this experiment.
          If None, it is automatically determined within options.persistence_dir.
        :param csv_dataset_path: if not None, path to the CSV file given persistence
          support to the dataset file properties.
          If None, it is automatically determined within options.persistence_dir.
        :param dataset_info_table: if not None, it must be a ImagePropertiesTable instance or
          subclass instance that can be used to obtain dataset file metainformation,
          and/or gather it from csv_dataset_path. If None, a new ImagePropertiesTable
          instance is created and used for this purpose.
        :param overwrite_file_properties: if True, file properties are recomputed before starting
          the experiment. Useful for temporary and/or random datasets. Note that overwrite
          control for the experiment results themselves is controlled in the call
          to get_df
        :param parallel_dataset_property_processing: if not None, it determines whether file properties
          are to be obtained in parallel. If None, it is given by not options.sequential.
        :param reconstructed_dir_path: if not None, a directory where reconstructed images are
          to be stored.
        :param compressed_copy_dir_path: if not None, it gives the directory where a copy of the compressed images.
          is to be stored. If may not be generated for images for which all columns are known
        """
        table_class = type(dataset_info_table) if dataset_info_table is not None \
            else self.default_file_properties_table_class
        csv_dataset_path = csv_dataset_path if csv_dataset_path is not None \
            else os.path.join(options.persistence_dir, f"{table_class.__name__}_persistence.csv")
        imageinfo_table = dataset_info_table if dataset_info_table is not None \
            else table_class(csv_support_path=csv_dataset_path)

        csv_dataset_path = csv_dataset_path if csv_dataset_path is not None \
            else f"{dataset_info_table.__class__.__name__}_persistence.csv"
        super().__init__(tasks=codecs,
                         dataset_paths=dataset_paths,
                         csv_experiment_path=csv_experiment_path,
                         csv_dataset_path=csv_dataset_path,
                         dataset_info_table=imageinfo_table,
                         overwrite_file_properties=overwrite_file_properties,
                         parallel_dataset_property_processing=parallel_dataset_property_processing)
        self.reconstructed_dir_path = reconstructed_dir_path
        self.compressed_copy_dir_path = compressed_copy_dir_path

    @property
    def codecs(self):
        """:return: an iterable of defined codecs
        """
        return self.tasks_by_name.values()

    @codecs.setter
    def codecs(self, new_codecs):
        self.tasks_by_name = collections.OrderedDict({
            codec.name: codec for codec in new_codecs})

    @property
    def codecs_by_name(self):
        """Alias for :py:attr:`tasks_by_name`
        """
        return self.tasks_by_name

    def process_row(self, index, column_fun_tuples, row, overwrite, fill):
        file_path, codec_name = index
        codec = self.codecs_by_name[codec_name]
        image_info_row = self.dataset_table_df.loc[indices_to_internal_loc(file_path)]
        row_wrapper = self.RowWrapper(
            file_path=file_path, codec=codec,
            image_info_row=image_info_row,
            row=row)
        result = super().process_row(index=index, column_fun_tuples=column_fun_tuples,
                                     row=row_wrapper, overwrite=overwrite, fill=fill)

        if isinstance(result, Exception):
            return result

        if self.compressed_copy_dir_path:
            output_compressed_path = os.path.join(
                self.compressed_copy_dir_path,
                codec.name,
                os.path.basename(os.path.dirname(file_path)), os.path.basename(file_path))
            os.makedirs(os.path.dirname(output_compressed_path), exist_ok=True)
            if options.verbose > 1:
                print(f"[C]opying {file_path} into {output_compressed_path}")
            shutil.copy(row_wrapper.compression_results.compressed_path, output_compressed_path)

        if self.reconstructed_dir_path is not None:
            output_reconstructed_path = os.path.join(
                self.reconstructed_dir_path,
                codec.name,
                os.path.basename(os.path.dirname(file_path)), os.path.basename(file_path))
            os.makedirs(os.path.dirname(output_reconstructed_path), exist_ok=True)
            if options.verbose > 1:
                print(f"[C]opying {row_wrapper.compression_results.compressed_path} into {output_reconstructed_path}")
            shutil.copy(row_wrapper.decompression_results.reconstructed_path,
                        output_reconstructed_path)

            if image_info_row["component_count"] == 3:
                rendered_path = f"{output_reconstructed_path}.png"
                if not os.path.exists(rendered_path) or options.force:
                    array = isets.load_array_bsq(file_or_path=row_wrapper.decompression_results.reconstructed_path,
                                                 image_properties_row=image_info_row).astype(np.int)
                    if options.reconstructed_size is not None:
                        width, height, _ = array.shape
                        array = array[
                                width // 2 - options.reconstructed_size // 2:width // 2 + options.reconstructed_size // 2,
                                height // 2 - options.reconstructed_size // 2:height // 2 + options.reconstructed_size // 2,
                                :]
                    cmin = array.min()
                    cmax = array.max()
                    array = np.round((255 * (array.astype(np.int) - cmin) / (cmax - cmin))).astype("uint8")
                    if options.verbose > 1:
                        print(f"[R]endering {rendered_path}")

                    numpngw.imwrite(rendered_path, array.swapaxes(0, 1))

            else:
                full_array = isets.load_array_bsq(
                    file_or_path=row_wrapper.decompression_results.reconstructed_path,
                    image_properties_row=image_info_row).astype(np.int)
                if options.reconstructed_size is not None:
                    width, height, _ = full_array.shape
                    full_array = full_array[
                                 width // 2 - options.reconstructed_size // 2:width // 2 + options.reconstructed_size // 2,
                                 height // 2 - options.reconstructed_size // 2:height // 2 + options.reconstructed_size // 2,
                                 :]
                for i, rendered_path in enumerate(f"{output_reconstructed_path}_component{i}.png"
                                                  for i in range(image_info_row['component_count'])):
                    if not os.path.exists(rendered_path) or options.force:
                        array = full_array[:, :, i].squeeze().swapaxes(0, 1)
                        cmin = array.min()
                        cmax = array.max()
                        array = np.round((255 * (array - cmin) / (cmax - cmin))).astype("uint8")
                        if options.verbose > 1:
                            print(f"[R]endering {rendered_path}")
                        numpngw.imwrite(rendered_path, array)

        return row

    @atable.column_function("compressed_size_bytes", label="Compressed data size (Bytes)", plot_min=0)
    def set_compressed_data_size(self, index, row):
        row[_column_name] = os.path.getsize(row.compression_results.compressed_path)

    @atable.column_function([
        atable.ColumnProperties(name="compression_ratio", label="Compression ratio", plot_min=0),
        atable.ColumnProperties(name="lossless_reconstruction", label="Lossless?"),
        atable.ColumnProperties(name="compression_time_seconds", label="Compression time (s)", plot_min=0),
        atable.ColumnProperties(name="decompression_time_seconds", label="Decompression time (s)", plot_min=0),
        atable.ColumnProperties(name="repetitions", label="Number of compression/decompression repetitions",
                                plot_min=0),
        atable.ColumnProperties(name="compressed_file_sha256", label="Compressed file's SHA256")
    ])
    def set_comparison_results(self, index, row):
        """Perform a compression-decompression cycle and store the comparison results
        """
        file_path, codec_name = index
        row.image_info_row = self.dataset_table_df.loc[indices_to_internal_loc(file_path)]
        assert row.compression_results.compressed_path == row.decompression_results.compressed_path
        assert row.image_info_row["bytes_per_sample"] * row.image_info_row["samples"] \
               == os.path.getsize(row.compression_results.original_path)
        hasher = hashlib.sha256()
        with open(row.compression_results.compressed_path, "rb") as compressed_file:
            hasher.update(compressed_file.read())
        compressed_file_sha256 = hasher.hexdigest()

        row["lossless_reconstruction"] = filecmp.cmp(row.compression_results.original_path,
                                                     row.decompression_results.reconstructed_path)
        assert row.compression_results.compression_time_seconds is not None
        row["compression_time_seconds"] = row.compression_results.compression_time_seconds
        assert row.decompression_results.decompression_time_seconds is not None
        row["decompression_time_seconds"] = row.decompression_results.decompression_time_seconds
        row["repetitions"] = options.repetitions
        row["compression_ratio"] = os.path.getsize(row.compression_results.original_path) / row["compressed_size_bytes"]
        row["compressed_file_sha256"] = compressed_file_sha256

    @atable.column_function("bpppc", label="Compressed data rate (bpppc)", plot_min=0)
    def set_bpppc(self, index, row):
        row[_column_name] = 8 * row["compressed_size_bytes"] / row.image_info_row["samples"]

    @atable.column_function("compression_ratio_dr", label="Compression ratio", plot_min=0)
    def set_compression_ratio_dr(self, index, row):
        """Set the compression ratio calculated based on the dynamic range of the
        input samples, as opposed to 8*bytes_per_sample.
        """
        row[_column_name] = (row.image_info_row["dynamic_range_bits"] * row.image_info_row["samples"]) \
                            / (8 * row["compressed_size_bytes"])

    @atable.column_function(
        [atable.ColumnProperties(name="compression_efficiency_1byte_entropy",
                                 label="Compression efficiency (1B entropy)", plot_min=0),
         atable.ColumnProperties(name="compression_efficiency_2byte_entropy",
                                 label="Compression efficiency (2B entropy)", plot_min=0)])
    def set_efficiency(self, index, row):
        compression_efficiency_1byte_entropy = \
            row.image_info_row["entropy_1B_bps"] * row.image_info_row["size_bytes"] \
            / (row["compressed_size_bytes"] * 8)
        compression_efficiency_2byte_entropy = \
            row.image_info_row["entropy_2B_bps"] * (row.image_info_row["size_bytes"] / 2) \
            / (row["compressed_size_bytes"] * 8)
        row["compression_efficiency_1byte_entropy"] = compression_efficiency_1byte_entropy
        row["compression_efficiency_2byte_entropy"] = compression_efficiency_2byte_entropy
class ImageGeometryTable(sets.FilePropertiesTable):
    """Basic properties table for images, including geometry.
    Allows automatic handling of tags in filenames, e.g., ZxYxX_u16be.
    """
    dataset_files_extension = "raw"

    # Data type columns

    @atable.column_function("bytes_per_sample",
                            label="Bytes per sample",
                            plot_min=0)
    def set_bytes_per_sample(self, file_path, row):
        if any(s in file_path for s in ("u8be", "u8le", "s8be", "s8le")):
            row[_column_name] = 1
        elif any(s in file_path
                 for s in ("u16be", "u16le", "s16be", "s16le", "f16")):
            row[_column_name] = 2
        elif any(s in file_path
                 for s in ("u32be", "u32le", "s32be", "s32le", "f32")):
            row[_column_name] = 4
        elif any(s in file_path for s in ("f64")):
            row[_column_name] = 8
        else:
            raise sets.UnkownPropertiesException(
                f"{self.__class__.__name__}: unknown {_column_name} for {file_path}"
            )

    @atable.column_function("float", label="Floating point data?")
    def set_float(self, file_path, row):
        if any(s in os.path.basename(file_path)
               for s in ("u8be", "u8le", "s8be", "s8le", "u16be", "u16le",
                         "s16be", "s16le", "u32be", "u32le", "s32be",
                         "s32le")):
            row[_column_name] = False
        elif any(s in os.path.basename(file_path)
                 for s in ("f16", "f32", "f64")):
            row[_column_name] = True
        else:
            enb.logger.debug(
                f"Unknown {_column_name} from {file_path}. Setting to False.")
            row[_column_name] = False

    @atable.column_function("signed", label="Signed samples")
    def set_signed(self, file_path, row):
        if any(s in file_path
               for s in ("u8be", "u16be", "u16le", "u32be", "u32le")):
            row[_column_name] = False
        elif any(s in file_path for s in ("s8be", "s16be", "s16le", "s32be",
                                          "s32le", "f16", "f32", "f64")):
            row[_column_name] = True
        else:
            enb.logger.debug(
                f"Unknown {_column_name} for {file_path}. Setting to False.")
            row[_column_name] = False

    @atable.column_function("big_endian", label="Big endian?")
    def set_big_endian(self, file_path, row):
        if any(s in file_path
               for s in ("u8be", "u16be", "u32be", "s8be", "s16be", "s32be")):
            row[_column_name] = True
        elif any(s in file_path for s in ("u8le", "u16le", "u32le", "s8le",
                                          "s16le", "s32le")):
            row[_column_name] = False
        elif any(s in file_path for s in ("f16", "f32", "f64")):
            row[_column_name] = True
        else:
            enb.logger.debug(
                f"Unknown {_column_name} for {file_path}. Setting to False.")
            row[_column_name] = False

    @atable.column_function("dtype", label="Numpy dtype")
    def set_column_dtype(self, file_path, row):
        """Set numpy's dtype string
        """
        if row["float"]:
            row[_column_name] = f"f{8 * row['bytes_per_sample']}"
        else:
            row[_column_name] = f"{'>' if row['big_endian'] else '<'}{'i' if row['signed'] else 'u'}{row['bytes_per_sample']}"

    @atable.column_function("type_name",
                            label="Type name usable in file names")
    def set_type_name(self, file_path, row):
        """Set the type name usable in file names
        """
        if row["float"]:
            row[_column_name] = f"f{8 * row['bytes_per_sample']}"
        else:
            row[_column_name] = f"{'s' if row['signed'] else 'u'}{8 * row['bytes_per_sample']}{'be' if row['big_endian'] else 'le'}"

    # Image dimension columns

    @atable.column_function("samples", label="Sample count", plot_min=0)
    def set_samples(self, file_path, row):
        """Set the number of samples in the image
        """
        assert row["size_bytes"] % row["bytes_per_sample"] == 0
        row[_column_name] = row["size_bytes"] // row["bytes_per_sample"]

    @atable.column_function([
        atable.ColumnProperties(name="width", label="Width", plot_min=1),
        atable.ColumnProperties(name="height", label="Height", plot_min=1),
        atable.ColumnProperties(name="component_count",
                                label="Components",
                                plot_min=1),
    ])
    def set_image_geometry(self, file_path, row):
        """Obtain the image's geometry (width, height and number of components)
        based on the filename tags (and possibly its size)
        """
        file_path_to_geometry_dict(file_path=file_path, existing_dict=row)
class DefinitionModesTable(atable.ATable):
    """Example ATable subclass that exemplifies multiple ways of defining columns.
    """
    def column_a(self, index, row):
        """Methods that start with column_* are automatically recognized as column definitions.
        The returned value is the value set into the appropriate column of row.
        """
        return 1

    def column_b(self, index, row):
        """Previously defined column values for this row can be used in other columns.
        Columns are computed in the order they are defined."""
        return row["a"] + 1

    @atable.column_function("c")
    def set_column_c(self, index, row):
        """The @enb.atable.column_function decorator can be used to explicitly mark class methods as
        column functions, i.e., functions that set one or more column functions.

        Functions decorated with @enb.atable.column_function must explicitly edit the row parameter
        to update the column function being set.
        """
        row["c"] = row["b"] + 1

    @atable.column_function("d")
    def set_column_d(self, index, row):
        """The _column_name is automatically be defined before calling a decorated (or column_*) function.
        This way, the function code needs not change if the column name is renamed.
        """
        row[_column_name] = row["c"] + 1

    def ignore_column_f(self, index, row):
        """Any number of methods can be defined normally in the ATable subclass.
        These are not invoked automatically by enb.
        """
        raise Exception("This method is never invoked")

    @atable.column_function([
        atable.ColumnProperties("e"),
        atable.ColumnProperties("f",
                                label="label for f",
                                plot_min=0,
                                plot_max=10)
    ])
    def set_multiple_colums(self, index, row):
        """Multiple columns can be set with the same decorated column function
        (not with undecorated column_* methods).

        The @enb.atable.column_function decorator accepts a list of atable.ColumnProperties instances,
        one per column being set by this function.

        Check out the documentation for atable.ColumnProperties, as it allows providing hints
        for plotting any column individually.
        See https://miguelinux314.github.io/experiment-notebook/using_analyzer_subclasses.html
        for more details.
        """
        row["e"] = row["d"] + 1
        row["f"] = row["e"] + 1

    @atable.column_function(
        atable.ColumnProperties("constant_one", plot_min=1, plot_max=1),
        "constant_zero",
        atable.ColumnProperties("first_ten_numbers", has_iterable_values=True),
        atable.ColumnProperties("ascii_table", has_dict_values=True),
    )
    def set_mixed_type_columns(self, index, row):
        row["constant_one"] = 1
        row["constant_zero"] = 0
        row["first_ten_numbers"] = list(range(10))
        row["ascii_table"] = {l: ord(l) for l in string.ascii_letters}