Skip to content

hyfi.graphics

COLLAGE

Source code in hyfi/graphics/collage.py
class COLLAGE:
    @staticmethod
    def collage(
        images_or_uris: Union[
            List[Union[str, Path, Image.Image]], str, Path, Image.Image
        ],
        output_file: Optional[Union[str, Path]] = None,
        num_cols: Optional[int] = 3,
        num_images: Optional[int] = 12,
        collage_width: int = 1200,
        padding: int = 10,
        background_color: str = "black",
        crop_to_min_size: bool = False,
        show_filename: bool = False,
        filename_offset: Tuple[int, int] = (5, 5),
        fontname: Optional[str] = None,
        fontsize: int = 12,
        fontcolor: str = "#000",
        **kwargs,
    ) -> Optional[Collage]:
        """
        Create a collage of images.
        """

        if not isinstance(images_or_uris, list):
            images_or_uris = [images_or_uris]
        images_or_uris = [
            uri if isinstance(uri, Image.Image) else str(uri) for uri in images_or_uris
        ]

        if num_images:
            num_images = min(num_images, len(images_or_uris))
        else:
            num_images = len(images_or_uris)
        if num_images < 1:
            raise ValueError("max_images must be greater than 0")
        num_images, num_cols, nrows = GUTILs.get_grid_size(
            num_images, num_cols=num_cols
        )
        logger.info(
            "Creating collage of %d images in %d columns and %d rows",
            num_images,
            num_cols,
            nrows,
        )
        img_width = collage_width // num_cols
        collage_width = num_cols * img_width + padding * (num_cols + 1)

        # load images
        images = GUTILs.load_images(
            images_or_uris[:num_images],
            resize_to_multiple_of=None,
            crop_to_min_size=crop_to_min_size,
            max_width=img_width,
            **kwargs,
        )
        filenames = [
            os.path.basename(image_or_uri) if isinstance(image_or_uri, str) else None
            for image_or_uri in images_or_uris[:num_images]
        ]
        # convert images
        images = [
            GUTILs.print_filename_on_image(
                image,
                print_filename=show_filename,
                filename=filename,
                filename_offset=filename_offset,
                fontname=fontname,
                fontsize=fontsize,
                fontcolor=fontcolor,
            )
            for image, filename in zip(images, filenames)
        ]

        collage = COLLAGE.get_grid_of_images(
            images, num_cols, padding, background_color=background_color
        )
        if output_file:
            output_file = str(output_file)
            os.makedirs(os.path.dirname(output_file), exist_ok=True)
            collage.image.save(output_file)
            collage.filepath = output_file
            logger.info("Saved collage to %s", output_file)
        return collage

    @staticmethod
    def label_collage(
        collage: Collage,
        collage_filepath=None,
        title: Optional[str] = None,
        title_fontsize: int = 10,
        xlabel=None,
        ylabel=None,
        xticklabels=None,
        yticklabels=None,
        xlabel_fontsize=12,
        ylabel_fontsize=12,
        dpi=100,
        fg_fontcolor="white",
        bg_color="black",
        caption=None,
        **kwargs,
    ) -> Collage:
        """
        Create a collage of images.
        """
        figsize = (collage.width / dpi, collage.height / dpi)
        ncols, nrows = collage.ncols, collage.nrows

        fig = plt.figure(figsize=figsize, dpi=dpi)
        fig.patch.set_facecolor(bg_color)  # type: ignore
        plt.imshow(np.array(collage.image))
        ax = plt.gca()
        plt.grid(False)
        if xlabel is None and ylabel is None:
            plt.axis("off")
        if title is not None:
            title = "\n".join(
                sum(
                    (
                        textwrap.wrap(
                            t, width=int(collage.width / 15 * 12 / title_fontsize)
                        )
                        for t in title.split("\n")
                    ),
                    [],
                )
            )
            ax.set_title(title, fontsize=title_fontsize, color=fg_fontcolor)
        if xlabel is not None:
            # plt.xlabel(xlabel, fontdict={"fontsize": xlabel_fontsize})
            ax.set_xlabel(xlabel, fontsize=xlabel_fontsize, color=fg_fontcolor)
        if ylabel is not None:
            # plt.ylabel(ylabel, fontdict={"fontsize": ylabel_fontsize})
            ax.set_ylabel(ylabel, fontsize=ylabel_fontsize, color=fg_fontcolor)
        if xticklabels is not None:
            # get ncols number of xticks from xlim
            xlim = ax.get_xlim()
            xticks = GUTILs.get_ticks_from_lim(xlim, ncols)
            ax.set_xticks(xticks, color=fg_fontcolor)
            xticklabels = [""] + xticklabels
            ax.set_xticklabels(
                xticklabels, fontsize=xlabel_fontsize, color=fg_fontcolor
            )
        if yticklabels is not None:
            # get nrows number of yticks from ylim
            ylim = ax.get_ylim()
            yticks = GUTILs.get_ticks_from_lim(ylim, nrows)
            ax.set_yticks(yticks, color=fg_fontcolor)
            yticklabels = [""] + yticklabels
            ax.set_yticklabels(
                yticklabels, fontsize=ylabel_fontsize, color=fg_fontcolor
            )

        plt.tight_layout()
        if caption is not None:
            plt.figtext(
                0.5,
                0.01,
                caption,
                wrap=True,
                horizontalalignment="center",
                fontsize=10,
                color=fg_fontcolor,
            )

        img = GUTILs.convert_figure_to_image(fig, dpi=dpi)
        img = GUTILs.scale_image(img, max_width=collage.width)
        plt.close()

        if collage_filepath is not None:
            collage_filepath = str(collage_filepath)
            os.makedirs(os.path.dirname(collage_filepath), exist_ok=True)
            # fig.savefig(collage_filepath, dpi=dpi, bbox_inches="tight", pad_inches=0)
            img.save(collage_filepath)
            # collage_image.save(collage_filepath)
            # log.info(f"Saved collage to {collage_filepath}")

        return Collage(
            image=img,
            filepath=collage_filepath,
            width=img.width,
            height=img.height,
            ncols=ncols,
            nrows=nrows,
        )

    @staticmethod
    def get_grid_of_images(
        images: List[Image.Image],
        num_cols: int = 3,
        padding: int = 10,
        background_color: str = "black",
    ) -> Collage:
        """
        Create a grid of images.
        """
        nrows = len(images) // num_cols
        assert len(images) == nrows * num_cols
        width, height = images[0].size
        grid_width = num_cols * width + padding * (num_cols + 1)
        grid_height = nrows * height + padding * (nrows + 1)
        collage = Image.new(
            "RGB", size=(grid_width, grid_height), color=background_color
        )
        for j, image in enumerate(images):
            x = j % num_cols
            y = j // num_cols
            collage.paste(
                image, (x * width + padding * (x + 1), y * height + padding * (y + 1))
            )
        return Collage(
            image=collage,
            width=grid_width,
            height=grid_height,
            ncols=num_cols,
            nrows=nrows,
        )

    @staticmethod
    def gallery_ndarray_images(
        array: np.ndarray,
        ncols: int = 7,
    ) -> np.ndarray:
        """
        Create a gallery of images from a numpy array.
        """
        nindex, height, width, intensity = array.shape
        nrows = nindex // ncols
        assert nindex == nrows * ncols
        return (
            array.reshape(nrows, ncols, height, width, intensity)
            .swapaxes(1, 2)
            .reshape(height * nrows, width * ncols, intensity)
        )

    @staticmethod
    def make_subplot_pages_from_images(
        images: List[Union[str, Path, np.ndarray, Image.Image]],
        num_images_per_page: int,
        num_cols: int,
        num_rows: Optional[int] = None,
        output_dir: Optional[Union[str, Path]] = None,
        output_file_format: str = "collage_p{page_num}.png",
        titles: Optional[List[str]] = None,
        title_fontsize: int = 10,
        title_color: str = "black",
        figsize: Optional[Tuple[float, float]] = None,
        width_multiple: float = 4,
        height_multiple: float = 2,
        sharex: bool = True,
        sharey: bool = True,
        squeeze: bool = True,
        dpi: int = 100,
        verbose: bool = False,
    ):
        """Make subplot pages from images.

        Args:
            images: List of images to be plotted.
            num_images_per_page: Number of images per page.
            num_cols: Number of columns.
            num_rows: Number of rows. If None, it will be calculated automatically.
            output_dir: Output directory.
            output_file_format: Output file format.
            titles: List of titles for each image.
            title_fontsize: Title fontsize.
            title_color: Title color.
            figsize: Figure size.
            width_multiple: Width multiple.
            height_multiple: Height multiple.
            sharex: Share x-axis.
            sharey: Share y-axis.
            squeeze: Squeeze.
            dpi: Dots per inch.
        """
        num_images = len(images)
        num_pages = math.ceil(num_images / num_images_per_page)
        for page_num in range(num_pages):
            start_idx = page_num * num_images_per_page
            end_idx = start_idx + num_images_per_page
            page_images = images[start_idx:end_idx]
            page_titles = titles[start_idx:end_idx] if titles else None
            page_output_file = (
                Path(output_dir) / output_file_format.format(page_num=page_num)
                if output_dir
                else None
            )
            if verbose:
                logger.info(
                    f"Making page {page_num + 1}/{num_pages} with {len(page_images)} images"
                )
                logger.info(f"Page titles: {page_titles}")
                logger.info(f"Page output file: {page_output_file}")
            COLLAGE.make_subplots_from_images(
                page_images,
                num_cols=num_cols,
                num_rows=num_rows,
                output_file=page_output_file,
                titles=page_titles,
                title_fontsize=title_fontsize,
                title_color=title_color,
                figsize=figsize,
                width_multiple=width_multiple,
                height_multiple=height_multiple,
                sharex=sharex,
                sharey=sharey,
                squeeze=squeeze,
                dpi=dpi,
                verbose=verbose,
            )

    @staticmethod
    def make_subplots_from_images(
        images: List[Union[str, Path, np.ndarray, Image.Image]],
        num_cols: int,
        num_rows: Optional[int] = None,
        output_file: Optional[Union[str, Path]] = None,
        titles: Optional[List[str]] = None,
        title_fontsize: int = 10,
        title_color: str = "black",
        figsize: Optional[Tuple[float, float]] = None,
        width_multiple: float = 4,
        height_multiple: float = 2,
        sharex: bool = True,
        sharey: bool = True,
        squeeze: bool = True,
        dpi: int = 100,
        verbose: bool = False,
    ):
        """Make subplots from images.

        Args:
            images: List of images to be plotted.
            num_cols: Number of columns.
            num_rows: Number of rows. If None, it will be calculated automatically.
            output_file: Output file path.
            titles: List of titles for each image.
            title_fontsize: Title fontsize.
            title_color: Title color.
            figsize: Figure size.
            sharex: Share x-axis or not.
            sharey: Share y-axis or not.
            squeeze: Squeeze or not.
            dpi: Dots per inch.
        """
        if num_rows is None:
            num_images = len(images)
            num_rows = math.ceil(num_images / num_cols)
        if figsize is None:
            figsize = (num_cols * width_multiple, num_rows * height_multiple)
        fig, axes = plt.subplots(
            num_rows,
            num_cols,
            figsize=figsize,
            sharex=sharex,
            sharey=sharey,
            squeeze=squeeze,
        )
        for i in range(num_cols * num_rows):
            ax = (
                axes[i % num_cols]
                if num_rows == 1
                else axes[i // num_cols, i % num_cols]
            )
            if i >= len(images):
                ax.set_visible(False)
                continue
            image = images[i]
            if isinstance(image, (str, Path)):
                image = GUTILs.load_image(image)
            ax.imshow(image)
            if titles:
                ax.set_title(titles[i], fontsize=title_fontsize, color=title_color)
            ax.axis("off")
        if output_file:
            GUTILs.save_adjusted_subplots(fig, output_file, dpi=dpi, verbose=verbose)

collage(images_or_uris, output_file=None, num_cols=3, num_images=12, collage_width=1200, padding=10, background_color='black', crop_to_min_size=False, show_filename=False, filename_offset=(5, 5), fontname=None, fontsize=12, fontcolor='#000', **kwargs) staticmethod

Create a collage of images.

Source code in hyfi/graphics/collage.py
@staticmethod
def collage(
    images_or_uris: Union[
        List[Union[str, Path, Image.Image]], str, Path, Image.Image
    ],
    output_file: Optional[Union[str, Path]] = None,
    num_cols: Optional[int] = 3,
    num_images: Optional[int] = 12,
    collage_width: int = 1200,
    padding: int = 10,
    background_color: str = "black",
    crop_to_min_size: bool = False,
    show_filename: bool = False,
    filename_offset: Tuple[int, int] = (5, 5),
    fontname: Optional[str] = None,
    fontsize: int = 12,
    fontcolor: str = "#000",
    **kwargs,
) -> Optional[Collage]:
    """
    Create a collage of images.
    """

    if not isinstance(images_or_uris, list):
        images_or_uris = [images_or_uris]
    images_or_uris = [
        uri if isinstance(uri, Image.Image) else str(uri) for uri in images_or_uris
    ]

    if num_images:
        num_images = min(num_images, len(images_or_uris))
    else:
        num_images = len(images_or_uris)
    if num_images < 1:
        raise ValueError("max_images must be greater than 0")
    num_images, num_cols, nrows = GUTILs.get_grid_size(
        num_images, num_cols=num_cols
    )
    logger.info(
        "Creating collage of %d images in %d columns and %d rows",
        num_images,
        num_cols,
        nrows,
    )
    img_width = collage_width // num_cols
    collage_width = num_cols * img_width + padding * (num_cols + 1)

    # load images
    images = GUTILs.load_images(
        images_or_uris[:num_images],
        resize_to_multiple_of=None,
        crop_to_min_size=crop_to_min_size,
        max_width=img_width,
        **kwargs,
    )
    filenames = [
        os.path.basename(image_or_uri) if isinstance(image_or_uri, str) else None
        for image_or_uri in images_or_uris[:num_images]
    ]
    # convert images
    images = [
        GUTILs.print_filename_on_image(
            image,
            print_filename=show_filename,
            filename=filename,
            filename_offset=filename_offset,
            fontname=fontname,
            fontsize=fontsize,
            fontcolor=fontcolor,
        )
        for image, filename in zip(images, filenames)
    ]

    collage = COLLAGE.get_grid_of_images(
        images, num_cols, padding, background_color=background_color
    )
    if output_file:
        output_file = str(output_file)
        os.makedirs(os.path.dirname(output_file), exist_ok=True)
        collage.image.save(output_file)
        collage.filepath = output_file
        logger.info("Saved collage to %s", output_file)
    return collage

gallery_ndarray_images(array, ncols=7) staticmethod

Create a gallery of images from a numpy array.

Source code in hyfi/graphics/collage.py
@staticmethod
def gallery_ndarray_images(
    array: np.ndarray,
    ncols: int = 7,
) -> np.ndarray:
    """
    Create a gallery of images from a numpy array.
    """
    nindex, height, width, intensity = array.shape
    nrows = nindex // ncols
    assert nindex == nrows * ncols
    return (
        array.reshape(nrows, ncols, height, width, intensity)
        .swapaxes(1, 2)
        .reshape(height * nrows, width * ncols, intensity)
    )

get_grid_of_images(images, num_cols=3, padding=10, background_color='black') staticmethod

Create a grid of images.

Source code in hyfi/graphics/collage.py
@staticmethod
def get_grid_of_images(
    images: List[Image.Image],
    num_cols: int = 3,
    padding: int = 10,
    background_color: str = "black",
) -> Collage:
    """
    Create a grid of images.
    """
    nrows = len(images) // num_cols
    assert len(images) == nrows * num_cols
    width, height = images[0].size
    grid_width = num_cols * width + padding * (num_cols + 1)
    grid_height = nrows * height + padding * (nrows + 1)
    collage = Image.new(
        "RGB", size=(grid_width, grid_height), color=background_color
    )
    for j, image in enumerate(images):
        x = j % num_cols
        y = j // num_cols
        collage.paste(
            image, (x * width + padding * (x + 1), y * height + padding * (y + 1))
        )
    return Collage(
        image=collage,
        width=grid_width,
        height=grid_height,
        ncols=num_cols,
        nrows=nrows,
    )

label_collage(collage, collage_filepath=None, title=None, title_fontsize=10, xlabel=None, ylabel=None, xticklabels=None, yticklabels=None, xlabel_fontsize=12, ylabel_fontsize=12, dpi=100, fg_fontcolor='white', bg_color='black', caption=None, **kwargs) staticmethod

Create a collage of images.

Source code in hyfi/graphics/collage.py
@staticmethod
def label_collage(
    collage: Collage,
    collage_filepath=None,
    title: Optional[str] = None,
    title_fontsize: int = 10,
    xlabel=None,
    ylabel=None,
    xticklabels=None,
    yticklabels=None,
    xlabel_fontsize=12,
    ylabel_fontsize=12,
    dpi=100,
    fg_fontcolor="white",
    bg_color="black",
    caption=None,
    **kwargs,
) -> Collage:
    """
    Create a collage of images.
    """
    figsize = (collage.width / dpi, collage.height / dpi)
    ncols, nrows = collage.ncols, collage.nrows

    fig = plt.figure(figsize=figsize, dpi=dpi)
    fig.patch.set_facecolor(bg_color)  # type: ignore
    plt.imshow(np.array(collage.image))
    ax = plt.gca()
    plt.grid(False)
    if xlabel is None and ylabel is None:
        plt.axis("off")
    if title is not None:
        title = "\n".join(
            sum(
                (
                    textwrap.wrap(
                        t, width=int(collage.width / 15 * 12 / title_fontsize)
                    )
                    for t in title.split("\n")
                ),
                [],
            )
        )
        ax.set_title(title, fontsize=title_fontsize, color=fg_fontcolor)
    if xlabel is not None:
        # plt.xlabel(xlabel, fontdict={"fontsize": xlabel_fontsize})
        ax.set_xlabel(xlabel, fontsize=xlabel_fontsize, color=fg_fontcolor)
    if ylabel is not None:
        # plt.ylabel(ylabel, fontdict={"fontsize": ylabel_fontsize})
        ax.set_ylabel(ylabel, fontsize=ylabel_fontsize, color=fg_fontcolor)
    if xticklabels is not None:
        # get ncols number of xticks from xlim
        xlim = ax.get_xlim()
        xticks = GUTILs.get_ticks_from_lim(xlim, ncols)
        ax.set_xticks(xticks, color=fg_fontcolor)
        xticklabels = [""] + xticklabels
        ax.set_xticklabels(
            xticklabels, fontsize=xlabel_fontsize, color=fg_fontcolor
        )
    if yticklabels is not None:
        # get nrows number of yticks from ylim
        ylim = ax.get_ylim()
        yticks = GUTILs.get_ticks_from_lim(ylim, nrows)
        ax.set_yticks(yticks, color=fg_fontcolor)
        yticklabels = [""] + yticklabels
        ax.set_yticklabels(
            yticklabels, fontsize=ylabel_fontsize, color=fg_fontcolor
        )

    plt.tight_layout()
    if caption is not None:
        plt.figtext(
            0.5,
            0.01,
            caption,
            wrap=True,
            horizontalalignment="center",
            fontsize=10,
            color=fg_fontcolor,
        )

    img = GUTILs.convert_figure_to_image(fig, dpi=dpi)
    img = GUTILs.scale_image(img, max_width=collage.width)
    plt.close()

    if collage_filepath is not None:
        collage_filepath = str(collage_filepath)
        os.makedirs(os.path.dirname(collage_filepath), exist_ok=True)
        # fig.savefig(collage_filepath, dpi=dpi, bbox_inches="tight", pad_inches=0)
        img.save(collage_filepath)
        # collage_image.save(collage_filepath)
        # log.info(f"Saved collage to {collage_filepath}")

    return Collage(
        image=img,
        filepath=collage_filepath,
        width=img.width,
        height=img.height,
        ncols=ncols,
        nrows=nrows,
    )

make_subplot_pages_from_images(images, num_images_per_page, num_cols, num_rows=None, output_dir=None, output_file_format='collage_p{page_num}.png', titles=None, title_fontsize=10, title_color='black', figsize=None, width_multiple=4, height_multiple=2, sharex=True, sharey=True, squeeze=True, dpi=100, verbose=False) staticmethod

Make subplot pages from images.

Parameters:

Name Type Description Default
images List[Union[str, Path, ndarray, Image]]

List of images to be plotted.

required
num_images_per_page int

Number of images per page.

required
num_cols int

Number of columns.

required
num_rows Optional[int]

Number of rows. If None, it will be calculated automatically.

None
output_dir Optional[Union[str, Path]]

Output directory.

None
output_file_format str

Output file format.

'collage_p{page_num}.png'
titles Optional[List[str]]

List of titles for each image.

None
title_fontsize int

Title fontsize.

10
title_color str

Title color.

'black'
figsize Optional[Tuple[float, float]]

Figure size.

None
width_multiple float

Width multiple.

4
height_multiple float

Height multiple.

2
sharex bool

Share x-axis.

True
sharey bool

Share y-axis.

True
squeeze bool

Squeeze.

True
dpi int

Dots per inch.

100
Source code in hyfi/graphics/collage.py
@staticmethod
def make_subplot_pages_from_images(
    images: List[Union[str, Path, np.ndarray, Image.Image]],
    num_images_per_page: int,
    num_cols: int,
    num_rows: Optional[int] = None,
    output_dir: Optional[Union[str, Path]] = None,
    output_file_format: str = "collage_p{page_num}.png",
    titles: Optional[List[str]] = None,
    title_fontsize: int = 10,
    title_color: str = "black",
    figsize: Optional[Tuple[float, float]] = None,
    width_multiple: float = 4,
    height_multiple: float = 2,
    sharex: bool = True,
    sharey: bool = True,
    squeeze: bool = True,
    dpi: int = 100,
    verbose: bool = False,
):
    """Make subplot pages from images.

    Args:
        images: List of images to be plotted.
        num_images_per_page: Number of images per page.
        num_cols: Number of columns.
        num_rows: Number of rows. If None, it will be calculated automatically.
        output_dir: Output directory.
        output_file_format: Output file format.
        titles: List of titles for each image.
        title_fontsize: Title fontsize.
        title_color: Title color.
        figsize: Figure size.
        width_multiple: Width multiple.
        height_multiple: Height multiple.
        sharex: Share x-axis.
        sharey: Share y-axis.
        squeeze: Squeeze.
        dpi: Dots per inch.
    """
    num_images = len(images)
    num_pages = math.ceil(num_images / num_images_per_page)
    for page_num in range(num_pages):
        start_idx = page_num * num_images_per_page
        end_idx = start_idx + num_images_per_page
        page_images = images[start_idx:end_idx]
        page_titles = titles[start_idx:end_idx] if titles else None
        page_output_file = (
            Path(output_dir) / output_file_format.format(page_num=page_num)
            if output_dir
            else None
        )
        if verbose:
            logger.info(
                f"Making page {page_num + 1}/{num_pages} with {len(page_images)} images"
            )
            logger.info(f"Page titles: {page_titles}")
            logger.info(f"Page output file: {page_output_file}")
        COLLAGE.make_subplots_from_images(
            page_images,
            num_cols=num_cols,
            num_rows=num_rows,
            output_file=page_output_file,
            titles=page_titles,
            title_fontsize=title_fontsize,
            title_color=title_color,
            figsize=figsize,
            width_multiple=width_multiple,
            height_multiple=height_multiple,
            sharex=sharex,
            sharey=sharey,
            squeeze=squeeze,
            dpi=dpi,
            verbose=verbose,
        )

make_subplots_from_images(images, num_cols, num_rows=None, output_file=None, titles=None, title_fontsize=10, title_color='black', figsize=None, width_multiple=4, height_multiple=2, sharex=True, sharey=True, squeeze=True, dpi=100, verbose=False) staticmethod

Make subplots from images.

Parameters:

Name Type Description Default
images List[Union[str, Path, ndarray, Image]]

List of images to be plotted.

required
num_cols int

Number of columns.

required
num_rows Optional[int]

Number of rows. If None, it will be calculated automatically.

None
output_file Optional[Union[str, Path]]

Output file path.

None
titles Optional[List[str]]

List of titles for each image.

None
title_fontsize int

Title fontsize.

10
title_color str

Title color.

'black'
figsize Optional[Tuple[float, float]]

Figure size.

None
sharex bool

Share x-axis or not.

True
sharey bool

Share y-axis or not.

True
squeeze bool

Squeeze or not.

True
dpi int

Dots per inch.

100
Source code in hyfi/graphics/collage.py
@staticmethod
def make_subplots_from_images(
    images: List[Union[str, Path, np.ndarray, Image.Image]],
    num_cols: int,
    num_rows: Optional[int] = None,
    output_file: Optional[Union[str, Path]] = None,
    titles: Optional[List[str]] = None,
    title_fontsize: int = 10,
    title_color: str = "black",
    figsize: Optional[Tuple[float, float]] = None,
    width_multiple: float = 4,
    height_multiple: float = 2,
    sharex: bool = True,
    sharey: bool = True,
    squeeze: bool = True,
    dpi: int = 100,
    verbose: bool = False,
):
    """Make subplots from images.

    Args:
        images: List of images to be plotted.
        num_cols: Number of columns.
        num_rows: Number of rows. If None, it will be calculated automatically.
        output_file: Output file path.
        titles: List of titles for each image.
        title_fontsize: Title fontsize.
        title_color: Title color.
        figsize: Figure size.
        sharex: Share x-axis or not.
        sharey: Share y-axis or not.
        squeeze: Squeeze or not.
        dpi: Dots per inch.
    """
    if num_rows is None:
        num_images = len(images)
        num_rows = math.ceil(num_images / num_cols)
    if figsize is None:
        figsize = (num_cols * width_multiple, num_rows * height_multiple)
    fig, axes = plt.subplots(
        num_rows,
        num_cols,
        figsize=figsize,
        sharex=sharex,
        sharey=sharey,
        squeeze=squeeze,
    )
    for i in range(num_cols * num_rows):
        ax = (
            axes[i % num_cols]
            if num_rows == 1
            else axes[i // num_cols, i % num_cols]
        )
        if i >= len(images):
            ax.set_visible(False)
            continue
        image = images[i]
        if isinstance(image, (str, Path)):
            image = GUTILs.load_image(image)
        ax.imshow(image)
        if titles:
            ax.set_title(titles[i], fontsize=title_fontsize, color=title_color)
        ax.axis("off")
    if output_file:
        GUTILs.save_adjusted_subplots(fig, output_file, dpi=dpi, verbose=verbose)

Collage

Bases: BaseModel

Collage of images.

Source code in hyfi/graphics/collage.py
class Collage(BaseModel):
    """Collage of images."""

    image: Image.Image
    width: int
    height: int
    ncols: int
    nrows: int
    filepath: Optional[str] = None

    model_config = ConfigDict(arbitrary_types_allowed=True)

GUTILs

Image utils.

Source code in hyfi/graphics/utils.py
class GUTILs:
    """Image utils."""

    @staticmethod
    def get_grid_size(
        num_images: int,
        num_cols: Optional[int] = None,
        num_rows: Optional[int] = None,
        truncate: bool = True,
        fit_to_grid: bool = True,
    ) -> Tuple[int, int, int]:
        """Get number of rows and columns for a grid of images.

        Args:
            num_images (int): Number of images.
            num_cols (int, optional): Number of columns. Defaults to None.
            num_rows (int, optional): Number of rows. Defaults to None.
            truncate (bool, optional): Truncate number of images to fit grid. Defaults to True. If False, the number of images will be increased to fit the grid.

        Returns:
            Tuple[int, int, int]: Number of images, columns and rows.
        """
        if not num_cols and num_images % num_cols != 0 and truncate:
            num_images = (num_images // num_cols) * num_cols
        if not num_cols or num_cols > num_images or num_cols < 1:
            num_cols = num_images // 2
        num_rows = num_images // num_cols

        return num_images, num_cols, num_rows

    @staticmethod
    def scale_image(
        image: Image.Image,
        max_width: Optional[int] = None,
        max_height: Optional[int] = None,
        max_pixels: Optional[int] = None,
        scale: float = 1.0,
        resize_to_multiple_of: Optional[int] = None,
        resample: int = Image.LANCZOS,
    ) -> Image.Image:
        """Scale image to have at most `max_pixels` pixels.

        Args:
            image: PIL image.
            max_width: Maximum width.
            max_height: Maximum height.
            max_pixels: Maximum number of pixels.
            scale: Scale factor.
            resize_to_multiple_of: Resize to multiple of this value.
            resample: Resampling filter.

        Returns:
            PIL image.
        """

        w, h = image.size

        if not max_width and max_height:
            max_width = int(w * max_height / h)
        elif not max_height and max_width:
            max_height = int(h * max_width / w)
        else:
            max_width = max_width or w
            max_height = max_height or h

        if max_width and max_height:
            max_pixels = max_width * max_height

        scale = np.sqrt(max_pixels / (w * h)) if max_pixels > 0 else scale or 1.0
        max_width = int(w * scale)
        max_height = int(h * scale)
        if resize_to_multiple_of:
            max_width = (max_width // resize_to_multiple_of) * resize_to_multiple_of
            max_height = (max_height // resize_to_multiple_of) * resize_to_multiple_of

        if scale < 1.0 or w > max_width or h > max_height:
            image = image.resize((max_width, max_height), resample=resample)  # type: ignore
        return image

    @staticmethod
    def load_image_as_ndarray(
        image_or_uri: Union[str, Path, Image.Image],
    ) -> np.ndarray:
        """Load image from file or URI."""
        return np.asarray(GUTILs.load_image(image_or_uri))

    @staticmethod
    def load_image(
        image_or_uri: Union[str, Path, Image.Image],
        max_width: Optional[int] = None,
        max_height: Optional[int] = None,
        max_pixels: Optional[int] = None,
        scale: float = 1.0,
        resize_to_multiple_of: Optional[int] = None,
        crop_box: Optional[Tuple[int, int, int, int]] = None,
        mode: str = "RGB",
        **read_kwargs,
    ) -> Image.Image:
        """Load image from file or URI."""
        from PIL import Image

        if isinstance(image_or_uri, Image.Image):
            img = image_or_uri.convert(mode)
        elif Path(image_or_uri).is_file():
            img = Image.open(image_or_uri).convert(mode)
        else:
            img = Image.open(
                io.BytesIO(IOLIBs.read(image_or_uri, **read_kwargs))
            ).convert(mode)
        img = GUTILs.scale_image(
            img,
            max_width=max_width,
            max_height=max_height,
            max_pixels=max_pixels,
            scale=scale,
            resize_to_multiple_of=resize_to_multiple_of,
        )
        if crop_box is not None:
            img = img.crop(crop_box)
        return img

    @staticmethod
    def load_images(
        images_or_uris: List[Union[str, Image.Image]],
        max_width: Optional[int] = None,
        max_height: Optional[int] = None,
        max_pixels: Optional[int] = None,
        scale: float = 1.0,
        resize_to_multiple_of: Optional[int] = None,
        crop_to_min_size: bool = False,
        mode: str = "RGB",
        **kwargs,
    ) -> List[Image.Image]:
        """Load images from files or URIs."""
        imgs = [
            GUTILs.load_image(
                image_or_uri,
                max_width=max_width,
                max_height=max_height,
                max_pixels=max_pixels,
                scale=scale,
                resize_to_multiple_of=resize_to_multiple_of,
                mode=mode,
                **kwargs,
            )
            for image_or_uri in images_or_uris
        ]
        if crop_to_min_size:
            min_width = min(img.width for img in imgs)
            min_height = min(img.height for img in imgs)
            if resize_to_multiple_of is not None:
                min_width = (min_width // resize_to_multiple_of) * resize_to_multiple_of
                min_height = (
                    min_height // resize_to_multiple_of
                ) * resize_to_multiple_of
            imgs = [img.crop((0, 0, min_width, min_height)) for img in imgs]

        return imgs

    @staticmethod
    def get_image_font(
        fontname: Optional[str] = None,
        fontsize: int = 12,
    ) -> Optional[ImageFont.ImageFont]:
        """Get font for PIL image."""
        fontname, fontpath = GUTILs.get_plot_font(
            fontname=fontname,
            set_font_for_matplot=False,
        )
        return ImageFont.truetype(fontpath, fontsize) if fontpath else None

    @staticmethod
    def get_default_system_font(
        fontname: Optional[str] = None,
        fontpath: Optional[str] = None,
        lang: str = "ko",
        verbose: bool = False,
    ) -> Tuple[str, str]:
        if platform.system() == "Darwin":
            default_fontname = "AppleGothic.ttf" if lang == "ko" else "Arial.ttf"
            fontname = fontname or default_fontname
            fontpath = os.path.join("/System/Library/Fonts/Supplemental/", fontname)
        elif platform.system() == "Windows":
            default_fontname = "malgun.ttf" if lang == "ko" else "arial.ttf"
            fontname = fontname or default_fontname
            fontpath = os.path.join("c:/Windows/Fonts/", fontname)
        elif platform.system() == "Linux":
            default_fontname = "NanumGothic.ttf" if lang == "ko" else "DejaVuSans.ttf"
            fontname = fontname or default_fontname
            if fontname.lower().startswith("nanum"):
                fontpath = os.path.join("/usr/share/fonts/truetype/nanum/", fontname)
            else:
                fontpath = os.path.join("/usr/share/fonts/truetype/", fontname)
        if fontpath and not Path(fontpath).is_file():
            paths = GUTILs.find_font_file(fontname)
            fontpath = paths[0] if len(paths) > 0 else ""
        if verbose:
            logger.info(f"Font path: {fontpath}")
        return fontname, fontpath

    @staticmethod
    def get_plot_font(
        fontpath: Optional[str] = None,
        fontname: Optional[str] = None,
        lang: str = "en",
        set_font_for_matplot: bool = True,
        verbose: bool = False,
    ) -> Tuple[str, str]:
        """Get font for plot

        Args:
            fontpath: Font file path
            fontname: Font name
            lang: Language
            set_font_for_matplot: Set font for matplot
            verbose: Verbose mode

        Returns:
            Tuple of font name and font path

        """
        if fontname and not fontname.endswith(".ttf"):
            fontname += ".ttf"
        if not fontpath:
            fontname, fontpath = GUTILs.get_default_system_font(
                fontname, fontpath, lang, verbose
            )

        if fontpath and Path(fontpath).is_file():
            font_manager.fontManager.addfont(fontpath)
            fontname = font_manager.FontProperties(fname=fontpath).get_name()  # type: ignore

            if set_font_for_matplot and fontname:
                rc("font", family=fontname)
                plt.rcParams["axes.unicode_minus"] = False
                if verbose:
                    font_family = plt.rcParams["font.family"]
                    logger.info(f"font family: {font_family}")
            if verbose:
                logger.info(f"font name: {fontname}")
        else:
            logger.warning(f"Font file does not exist at {fontpath}")
            fontname = ""
            fontpath = ""
            if platform.system() == "Linux":
                font_install_help = """
                apt install fontconfig
                apt install fonts-nanum
                fc-list | grep -i nanum
                """
                print(font_install_help)
        return fontname, fontpath

    @staticmethod
    def find_font_file(query: str) -> List[str]:
        """Find font file by query string"""
        return list(
            filter(
                lambda path: query in os.path.basename(path),
                font_manager.findSystemFonts(),
            )
        )

    @staticmethod
    def convert_figure_to_image(
        fig: plt.Figure,
        dpi: int = 300,
    ):
        """Convert a Matplotlib figure to a PIL Image and return it"""
        buf = io.BytesIO()
        fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight", pad_inches=0)
        buf.seek(0)
        return Image.open(buf)

    @staticmethod
    def print_filename_on_image(
        image_or_uri,
        print_filename: bool = False,
        filename: Optional[str] = None,
        filename_offset: Tuple[float, float] = (5, 5),
        fontname: Optional[str] = None,
        fontsize: int = 12,
        fontcolor: Optional[str] = None,
    ) -> Image.Image:
        """
        Convert an image to a PIL Image.

        Args:
            image_or_uri: The image or image URI.
            print_filename: Whether to print the filename on the image.
            filename: The filename to show on the image.
            filename_offset: The offset of the filename from the top left corner.
            fontname: The font name to use for the filename.
            fontsize: The font size to use for the filename.
            fontcolor: The font color to use for the filename.

        Returns:
            The PIL Image.
        """
        img = GUTILs.load_image(image_or_uri)
        if isinstance(image_or_uri, str) and filename is None:
            filename = os.path.basename(image_or_uri)
        if print_filename and filename:
            font = GUTILs.get_image_font(fontname, fontsize)
            draw = ImageDraw.Draw(img)
            draw.text(filename_offset, filename, font=font, fill=fontcolor)

        # img = img.convert("RGB")
        # img = np.array(img)
        return img

    @staticmethod
    def get_ticks_from_lim(
        lim: Tuple[float, float],
        num_ticks: int,
    ) -> np.ndarray:
        """
        Get n evenly spaced ticks from lim.

        Args:
            lim: Tuple of (min, max) values.
            num_ticks: Number of ticks to get.

        Returns:
            Array of ticks.
        """
        ticks = np.linspace(lim[0], lim[1], num_ticks + 1)
        ticks = ticks - (ticks[1] - ticks[0]) / 2
        ticks[0] = lim[0]
        return ticks

    @staticmethod
    def save_adjusted_subplots(
        fig: plt.Figure,
        output_file: str,
        left: float = 0.1,
        right: float = 0.9,
        bottom: float = 0.1,
        top: float = 0.9,
        wspace: float = 0,
        hspace: float = 0,
        tight_layout: bool = True,
        bbox_inches: str = "tight",
        pad_inches: float = 0,
        transparent: bool = True,
        dpi: int = 300,
        verbose: bool = True,
    ):
        """Save subplots after adjusting the figure

        Args:
            fig: Figure
            output_file: Output file path
            left: Left
            right: Right
            bottom: Bottom
            top: Top
            wspace: Wspace
            hspace: Hspace
            tight_layout: Tight layout
            bbox_inches: Bbox inches
            pad_inches: Pad inches
            transparent: Transparent
            dpi: DPI
            verbose: Verbose mode
        """

        # make the figure look better
        fig.subplots_adjust(
            left=left,
            right=right,
            bottom=bottom,
            top=top,
            wspace=wspace,
            hspace=hspace,
        )
        if tight_layout:
            fig.tight_layout()

        Path(output_file).parent.mkdir(parents=True, exist_ok=True)
        fig.savefig(
            output_file,
            dpi=dpi,
            bbox_inches=bbox_inches,
            pad_inches=pad_inches,
            transparent=transparent,
        )
        if verbose:
            logger.info("Saved subplots to %s", output_file)

convert_figure_to_image(fig, dpi=300) staticmethod

Convert a Matplotlib figure to a PIL Image and return it

Source code in hyfi/graphics/utils.py
@staticmethod
def convert_figure_to_image(
    fig: plt.Figure,
    dpi: int = 300,
):
    """Convert a Matplotlib figure to a PIL Image and return it"""
    buf = io.BytesIO()
    fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight", pad_inches=0)
    buf.seek(0)
    return Image.open(buf)

find_font_file(query) staticmethod

Find font file by query string

Source code in hyfi/graphics/utils.py
@staticmethod
def find_font_file(query: str) -> List[str]:
    """Find font file by query string"""
    return list(
        filter(
            lambda path: query in os.path.basename(path),
            font_manager.findSystemFonts(),
        )
    )

get_grid_size(num_images, num_cols=None, num_rows=None, truncate=True, fit_to_grid=True) staticmethod

Get number of rows and columns for a grid of images.

Parameters:

Name Type Description Default
num_images int

Number of images.

required
num_cols int

Number of columns. Defaults to None.

None
num_rows int

Number of rows. Defaults to None.

None
truncate bool

Truncate number of images to fit grid. Defaults to True. If False, the number of images will be increased to fit the grid.

True

Returns:

Type Description
Tuple[int, int, int]

Tuple[int, int, int]: Number of images, columns and rows.

Source code in hyfi/graphics/utils.py
@staticmethod
def get_grid_size(
    num_images: int,
    num_cols: Optional[int] = None,
    num_rows: Optional[int] = None,
    truncate: bool = True,
    fit_to_grid: bool = True,
) -> Tuple[int, int, int]:
    """Get number of rows and columns for a grid of images.

    Args:
        num_images (int): Number of images.
        num_cols (int, optional): Number of columns. Defaults to None.
        num_rows (int, optional): Number of rows. Defaults to None.
        truncate (bool, optional): Truncate number of images to fit grid. Defaults to True. If False, the number of images will be increased to fit the grid.

    Returns:
        Tuple[int, int, int]: Number of images, columns and rows.
    """
    if not num_cols and num_images % num_cols != 0 and truncate:
        num_images = (num_images // num_cols) * num_cols
    if not num_cols or num_cols > num_images or num_cols < 1:
        num_cols = num_images // 2
    num_rows = num_images // num_cols

    return num_images, num_cols, num_rows

get_image_font(fontname=None, fontsize=12) staticmethod

Get font for PIL image.

Source code in hyfi/graphics/utils.py
@staticmethod
def get_image_font(
    fontname: Optional[str] = None,
    fontsize: int = 12,
) -> Optional[ImageFont.ImageFont]:
    """Get font for PIL image."""
    fontname, fontpath = GUTILs.get_plot_font(
        fontname=fontname,
        set_font_for_matplot=False,
    )
    return ImageFont.truetype(fontpath, fontsize) if fontpath else None

get_plot_font(fontpath=None, fontname=None, lang='en', set_font_for_matplot=True, verbose=False) staticmethod

Get font for plot

Parameters:

Name Type Description Default
fontpath Optional[str]

Font file path

None
fontname Optional[str]

Font name

None
lang str

Language

'en'
set_font_for_matplot bool

Set font for matplot

True
verbose bool

Verbose mode

False

Returns:

Type Description
Tuple[str, str]

Tuple of font name and font path

Source code in hyfi/graphics/utils.py
@staticmethod
def get_plot_font(
    fontpath: Optional[str] = None,
    fontname: Optional[str] = None,
    lang: str = "en",
    set_font_for_matplot: bool = True,
    verbose: bool = False,
) -> Tuple[str, str]:
    """Get font for plot

    Args:
        fontpath: Font file path
        fontname: Font name
        lang: Language
        set_font_for_matplot: Set font for matplot
        verbose: Verbose mode

    Returns:
        Tuple of font name and font path

    """
    if fontname and not fontname.endswith(".ttf"):
        fontname += ".ttf"
    if not fontpath:
        fontname, fontpath = GUTILs.get_default_system_font(
            fontname, fontpath, lang, verbose
        )

    if fontpath and Path(fontpath).is_file():
        font_manager.fontManager.addfont(fontpath)
        fontname = font_manager.FontProperties(fname=fontpath).get_name()  # type: ignore

        if set_font_for_matplot and fontname:
            rc("font", family=fontname)
            plt.rcParams["axes.unicode_minus"] = False
            if verbose:
                font_family = plt.rcParams["font.family"]
                logger.info(f"font family: {font_family}")
        if verbose:
            logger.info(f"font name: {fontname}")
    else:
        logger.warning(f"Font file does not exist at {fontpath}")
        fontname = ""
        fontpath = ""
        if platform.system() == "Linux":
            font_install_help = """
            apt install fontconfig
            apt install fonts-nanum
            fc-list | grep -i nanum
            """
            print(font_install_help)
    return fontname, fontpath

get_ticks_from_lim(lim, num_ticks) staticmethod

Get n evenly spaced ticks from lim.

Parameters:

Name Type Description Default
lim Tuple[float, float]

Tuple of (min, max) values.

required
num_ticks int

Number of ticks to get.

required

Returns:

Type Description
ndarray

Array of ticks.

Source code in hyfi/graphics/utils.py
@staticmethod
def get_ticks_from_lim(
    lim: Tuple[float, float],
    num_ticks: int,
) -> np.ndarray:
    """
    Get n evenly spaced ticks from lim.

    Args:
        lim: Tuple of (min, max) values.
        num_ticks: Number of ticks to get.

    Returns:
        Array of ticks.
    """
    ticks = np.linspace(lim[0], lim[1], num_ticks + 1)
    ticks = ticks - (ticks[1] - ticks[0]) / 2
    ticks[0] = lim[0]
    return ticks

load_image(image_or_uri, max_width=None, max_height=None, max_pixels=None, scale=1.0, resize_to_multiple_of=None, crop_box=None, mode='RGB', **read_kwargs) staticmethod

Load image from file or URI.

Source code in hyfi/graphics/utils.py
@staticmethod
def load_image(
    image_or_uri: Union[str, Path, Image.Image],
    max_width: Optional[int] = None,
    max_height: Optional[int] = None,
    max_pixels: Optional[int] = None,
    scale: float = 1.0,
    resize_to_multiple_of: Optional[int] = None,
    crop_box: Optional[Tuple[int, int, int, int]] = None,
    mode: str = "RGB",
    **read_kwargs,
) -> Image.Image:
    """Load image from file or URI."""
    from PIL import Image

    if isinstance(image_or_uri, Image.Image):
        img = image_or_uri.convert(mode)
    elif Path(image_or_uri).is_file():
        img = Image.open(image_or_uri).convert(mode)
    else:
        img = Image.open(
            io.BytesIO(IOLIBs.read(image_or_uri, **read_kwargs))
        ).convert(mode)
    img = GUTILs.scale_image(
        img,
        max_width=max_width,
        max_height=max_height,
        max_pixels=max_pixels,
        scale=scale,
        resize_to_multiple_of=resize_to_multiple_of,
    )
    if crop_box is not None:
        img = img.crop(crop_box)
    return img

load_image_as_ndarray(image_or_uri) staticmethod

Load image from file or URI.

Source code in hyfi/graphics/utils.py
@staticmethod
def load_image_as_ndarray(
    image_or_uri: Union[str, Path, Image.Image],
) -> np.ndarray:
    """Load image from file or URI."""
    return np.asarray(GUTILs.load_image(image_or_uri))

load_images(images_or_uris, max_width=None, max_height=None, max_pixels=None, scale=1.0, resize_to_multiple_of=None, crop_to_min_size=False, mode='RGB', **kwargs) staticmethod

Load images from files or URIs.

Source code in hyfi/graphics/utils.py
@staticmethod
def load_images(
    images_or_uris: List[Union[str, Image.Image]],
    max_width: Optional[int] = None,
    max_height: Optional[int] = None,
    max_pixels: Optional[int] = None,
    scale: float = 1.0,
    resize_to_multiple_of: Optional[int] = None,
    crop_to_min_size: bool = False,
    mode: str = "RGB",
    **kwargs,
) -> List[Image.Image]:
    """Load images from files or URIs."""
    imgs = [
        GUTILs.load_image(
            image_or_uri,
            max_width=max_width,
            max_height=max_height,
            max_pixels=max_pixels,
            scale=scale,
            resize_to_multiple_of=resize_to_multiple_of,
            mode=mode,
            **kwargs,
        )
        for image_or_uri in images_or_uris
    ]
    if crop_to_min_size:
        min_width = min(img.width for img in imgs)
        min_height = min(img.height for img in imgs)
        if resize_to_multiple_of is not None:
            min_width = (min_width // resize_to_multiple_of) * resize_to_multiple_of
            min_height = (
                min_height // resize_to_multiple_of
            ) * resize_to_multiple_of
        imgs = [img.crop((0, 0, min_width, min_height)) for img in imgs]

    return imgs

print_filename_on_image(image_or_uri, print_filename=False, filename=None, filename_offset=(5, 5), fontname=None, fontsize=12, fontcolor=None) staticmethod

Convert an image to a PIL Image.

Parameters:

Name Type Description Default
image_or_uri

The image or image URI.

required
print_filename bool

Whether to print the filename on the image.

False
filename Optional[str]

The filename to show on the image.

None
filename_offset Tuple[float, float]

The offset of the filename from the top left corner.

(5, 5)
fontname Optional[str]

The font name to use for the filename.

None
fontsize int

The font size to use for the filename.

12
fontcolor Optional[str]

The font color to use for the filename.

None

Returns:

Type Description
Image

The PIL Image.

Source code in hyfi/graphics/utils.py
@staticmethod
def print_filename_on_image(
    image_or_uri,
    print_filename: bool = False,
    filename: Optional[str] = None,
    filename_offset: Tuple[float, float] = (5, 5),
    fontname: Optional[str] = None,
    fontsize: int = 12,
    fontcolor: Optional[str] = None,
) -> Image.Image:
    """
    Convert an image to a PIL Image.

    Args:
        image_or_uri: The image or image URI.
        print_filename: Whether to print the filename on the image.
        filename: The filename to show on the image.
        filename_offset: The offset of the filename from the top left corner.
        fontname: The font name to use for the filename.
        fontsize: The font size to use for the filename.
        fontcolor: The font color to use for the filename.

    Returns:
        The PIL Image.
    """
    img = GUTILs.load_image(image_or_uri)
    if isinstance(image_or_uri, str) and filename is None:
        filename = os.path.basename(image_or_uri)
    if print_filename and filename:
        font = GUTILs.get_image_font(fontname, fontsize)
        draw = ImageDraw.Draw(img)
        draw.text(filename_offset, filename, font=font, fill=fontcolor)

    # img = img.convert("RGB")
    # img = np.array(img)
    return img

save_adjusted_subplots(fig, output_file, left=0.1, right=0.9, bottom=0.1, top=0.9, wspace=0, hspace=0, tight_layout=True, bbox_inches='tight', pad_inches=0, transparent=True, dpi=300, verbose=True) staticmethod

Save subplots after adjusting the figure

Parameters:

Name Type Description Default
fig Figure

Figure

required
output_file str

Output file path

required
left float

Left

0.1
right float

Right

0.9
bottom float

Bottom

0.1
top float

Top

0.9
wspace float

Wspace

0
hspace float

Hspace

0
tight_layout bool

Tight layout

True
bbox_inches str

Bbox inches

'tight'
pad_inches float

Pad inches

0
transparent bool

Transparent

True
dpi int

DPI

300
verbose bool

Verbose mode

True
Source code in hyfi/graphics/utils.py
@staticmethod
def save_adjusted_subplots(
    fig: plt.Figure,
    output_file: str,
    left: float = 0.1,
    right: float = 0.9,
    bottom: float = 0.1,
    top: float = 0.9,
    wspace: float = 0,
    hspace: float = 0,
    tight_layout: bool = True,
    bbox_inches: str = "tight",
    pad_inches: float = 0,
    transparent: bool = True,
    dpi: int = 300,
    verbose: bool = True,
):
    """Save subplots after adjusting the figure

    Args:
        fig: Figure
        output_file: Output file path
        left: Left
        right: Right
        bottom: Bottom
        top: Top
        wspace: Wspace
        hspace: Hspace
        tight_layout: Tight layout
        bbox_inches: Bbox inches
        pad_inches: Pad inches
        transparent: Transparent
        dpi: DPI
        verbose: Verbose mode
    """

    # make the figure look better
    fig.subplots_adjust(
        left=left,
        right=right,
        bottom=bottom,
        top=top,
        wspace=wspace,
        hspace=hspace,
    )
    if tight_layout:
        fig.tight_layout()

    Path(output_file).parent.mkdir(parents=True, exist_ok=True)
    fig.savefig(
        output_file,
        dpi=dpi,
        bbox_inches=bbox_inches,
        pad_inches=pad_inches,
        transparent=transparent,
    )
    if verbose:
        logger.info("Saved subplots to %s", output_file)

scale_image(image, max_width=None, max_height=None, max_pixels=None, scale=1.0, resize_to_multiple_of=None, resample=Image.LANCZOS) staticmethod

Scale image to have at most max_pixels pixels.

Parameters:

Name Type Description Default
image Image

PIL image.

required
max_width Optional[int]

Maximum width.

None
max_height Optional[int]

Maximum height.

None
max_pixels Optional[int]

Maximum number of pixels.

None
scale float

Scale factor.

1.0
resize_to_multiple_of Optional[int]

Resize to multiple of this value.

None
resample int

Resampling filter.

LANCZOS

Returns:

Type Description
Image

PIL image.

Source code in hyfi/graphics/utils.py
@staticmethod
def scale_image(
    image: Image.Image,
    max_width: Optional[int] = None,
    max_height: Optional[int] = None,
    max_pixels: Optional[int] = None,
    scale: float = 1.0,
    resize_to_multiple_of: Optional[int] = None,
    resample: int = Image.LANCZOS,
) -> Image.Image:
    """Scale image to have at most `max_pixels` pixels.

    Args:
        image: PIL image.
        max_width: Maximum width.
        max_height: Maximum height.
        max_pixels: Maximum number of pixels.
        scale: Scale factor.
        resize_to_multiple_of: Resize to multiple of this value.
        resample: Resampling filter.

    Returns:
        PIL image.
    """

    w, h = image.size

    if not max_width and max_height:
        max_width = int(w * max_height / h)
    elif not max_height and max_width:
        max_height = int(h * max_width / w)
    else:
        max_width = max_width or w
        max_height = max_height or h

    if max_width and max_height:
        max_pixels = max_width * max_height

    scale = np.sqrt(max_pixels / (w * h)) if max_pixels > 0 else scale or 1.0
    max_width = int(w * scale)
    max_height = int(h * scale)
    if resize_to_multiple_of:
        max_width = (max_width // resize_to_multiple_of) * resize_to_multiple_of
        max_height = (max_height // resize_to_multiple_of) * resize_to_multiple_of

    if scale < 1.0 or w > max_width or h > max_height:
        image = image.resize((max_width, max_height), resample=resample)  # type: ignore
    return image

MOTION

Source code in hyfi/graphics/motion.py
class MOTION:
    @staticmethod
    def make_gif(
        image_filepaths=None,
        filename_patterns: Optional[str] = None,
        base_dir: Optional[str] = None,
        output_file: Optional[str] = None,
        duration: int = 100,
        loop: int = 0,
        width: int = 0,
        optimize: bool = True,
        quality: int = 50,
        display_to_notebook: bool = False,
        force: bool = False,
        **kwargs,
    ) -> Optional[str]:
        """
        Create a GIF from a list of images or a list of filenames.
        """
        output_file = output_file or ""
        if os.path.exists(output_file) and not force:
            logger.info("Skipping GIF creation, already exists: %s", output_file)
            logger.info("If you want to re-create the GIF, set force=True")
        else:
            if image_filepaths is None and filename_patterns:
                image_filepaths = sorted(
                    IOLIBs.get_filepaths(filename_patterns, base_dir=base_dir)
                )
            if not image_filepaths:
                logger.warning("no images found")
                return
            if frames := GUTILs.load_images(image_filepaths):
                frame_one = frames[0]
                frame_one.save(
                    output_file,
                    format="GIF",
                    append_images=frames,
                    save_all=True,
                    duration=duration,
                    loop=loop,
                    optimize=optimize,
                    quality=quality,
                )
                logger.info("Saved GIF to %s", output_file)
            else:
                logger.warning("No frames found for %s", filename_patterns)

        if display_to_notebook and os.path.exists(output_file):
            NBs.display_image(data=IOLIBs.read(output_file), width=width)

        return output_file

    @staticmethod
    def extract_frames(
        input_video_file: str,
        every_nth_frame: int,
        ouput_dir: str,
        frame_filename_pattern: str = "%04d.jpg",
        ffmpeg_path: str = "/usr/bin/ffmpeg",
    ):
        """
        Extract frames from a video.

        Args:
            input_video_file (str): Path to the video.
            every_nth_frame (int): Extract every nth frame.
            ouput_dir (str): Path to the output directory.
            frame_filename_pattern (str, optional): Frame filename pattern. Defaults to "%04d.jpg".
        """
        logger.info(
            "Exporting Video Frames (every %sth frame) to %s",
            every_nth_frame,
            ouput_dir,
        )
        try:
            for f in Path(f"{ouput_dir}").glob("*.jpg"):
                f.unlink()
        except FileNotFoundError:
            logger.info("No video frames found in %s", ouput_dir)
        vf = f"select=not(mod(n\\,{every_nth_frame}))"

        if not os.path.exists(ffmpeg_path):
            ffmpeg_path = "ffmpeg"
        if os.path.exists(input_video_file):
            subprocess.run(
                [
                    ffmpeg_path,
                    "-i",
                    f"{input_video_file}",
                    "-vf",
                    f"{vf}",
                    "-vsync",
                    "vfr",
                    "-q:v",
                    "2",
                    "-loglevel",
                    "error",
                    "-stats",
                    f"{ouput_dir}/{frame_filename_pattern}",
                ],
                stdout=subprocess.PIPE,
            ).stdout.decode("utf-8")
        else:
            logger.warning(
                "WARNING!\n\nVideo not found: %s.\nPlease check your video path.",
                input_video_file,
            )

    @staticmethod
    def create_video(
        input_image_dir: str,
        video_output_file: str,
        input_url: str,
        fps: int,
        start_number: int,
        vframes: int,
        force: bool = False,
    ) -> Optional[str]:
        """
        Create a video from a list of images.

        Args:
            input_image_dir (str): Base directory.
            video_path (str): Path to the video.
            input_url (str): Input URL.
            fps (int): Frames per second.
            start_number (int): Start number.
            vframes (int): Number of frames.
            force (bool, optional): Force video creation. Defaults to False.

        Raises:
            FileNotFoundError: If the video is not found.

        """

        logger.info("Creating video from %s", input_url)
        if os.path.exists(video_output_file) and not force:
            logger.info(
                "Skipping video creation, already exists: %s", video_output_file
            )
            logger.info("If you want to re-create the video, set force=True")
            return video_output_file

        cmd = [
            "ffmpeg",
            "-y",
            "-vcodec",
            "png",
            "-r",
            str(fps),
            "-start_number",
            str(start_number),
            "-i",
            input_url,
            "-frames:v",
            str(vframes),
            "-c:v",
            "libx264",
            "-vf",
            f"fps={fps}",
            "-pix_fmt",
            "yuv420p",
            "-crf",
            "17",
            "-preset",
            "veryslow",
            video_output_file,
        ]

        process = subprocess.Popen(
            cmd,
            cwd=f"{input_image_dir}",
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        _, stderr = process.communicate()
        if process.returncode != 0:
            logger.error(stderr)
            raise RuntimeError(stderr)
        else:
            logger.info("Video created successfully and saved to %s", video_output_file)

        return video_output_file

create_video(input_image_dir, video_output_file, input_url, fps, start_number, vframes, force=False) staticmethod

Create a video from a list of images.

Parameters:

Name Type Description Default
input_image_dir str

Base directory.

required
video_path str

Path to the video.

required
input_url str

Input URL.

required
fps int

Frames per second.

required
start_number int

Start number.

required
vframes int

Number of frames.

required
force bool

Force video creation. Defaults to False.

False

Raises:

Type Description
FileNotFoundError

If the video is not found.

Source code in hyfi/graphics/motion.py
@staticmethod
def create_video(
    input_image_dir: str,
    video_output_file: str,
    input_url: str,
    fps: int,
    start_number: int,
    vframes: int,
    force: bool = False,
) -> Optional[str]:
    """
    Create a video from a list of images.

    Args:
        input_image_dir (str): Base directory.
        video_path (str): Path to the video.
        input_url (str): Input URL.
        fps (int): Frames per second.
        start_number (int): Start number.
        vframes (int): Number of frames.
        force (bool, optional): Force video creation. Defaults to False.

    Raises:
        FileNotFoundError: If the video is not found.

    """

    logger.info("Creating video from %s", input_url)
    if os.path.exists(video_output_file) and not force:
        logger.info(
            "Skipping video creation, already exists: %s", video_output_file
        )
        logger.info("If you want to re-create the video, set force=True")
        return video_output_file

    cmd = [
        "ffmpeg",
        "-y",
        "-vcodec",
        "png",
        "-r",
        str(fps),
        "-start_number",
        str(start_number),
        "-i",
        input_url,
        "-frames:v",
        str(vframes),
        "-c:v",
        "libx264",
        "-vf",
        f"fps={fps}",
        "-pix_fmt",
        "yuv420p",
        "-crf",
        "17",
        "-preset",
        "veryslow",
        video_output_file,
    ]

    process = subprocess.Popen(
        cmd,
        cwd=f"{input_image_dir}",
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    _, stderr = process.communicate()
    if process.returncode != 0:
        logger.error(stderr)
        raise RuntimeError(stderr)
    else:
        logger.info("Video created successfully and saved to %s", video_output_file)

    return video_output_file

extract_frames(input_video_file, every_nth_frame, ouput_dir, frame_filename_pattern='%04d.jpg', ffmpeg_path='/usr/bin/ffmpeg') staticmethod

Extract frames from a video.

Parameters:

Name Type Description Default
input_video_file str

Path to the video.

required
every_nth_frame int

Extract every nth frame.

required
ouput_dir str

Path to the output directory.

required
frame_filename_pattern str

Frame filename pattern. Defaults to "%04d.jpg".

'%04d.jpg'
Source code in hyfi/graphics/motion.py
@staticmethod
def extract_frames(
    input_video_file: str,
    every_nth_frame: int,
    ouput_dir: str,
    frame_filename_pattern: str = "%04d.jpg",
    ffmpeg_path: str = "/usr/bin/ffmpeg",
):
    """
    Extract frames from a video.

    Args:
        input_video_file (str): Path to the video.
        every_nth_frame (int): Extract every nth frame.
        ouput_dir (str): Path to the output directory.
        frame_filename_pattern (str, optional): Frame filename pattern. Defaults to "%04d.jpg".
    """
    logger.info(
        "Exporting Video Frames (every %sth frame) to %s",
        every_nth_frame,
        ouput_dir,
    )
    try:
        for f in Path(f"{ouput_dir}").glob("*.jpg"):
            f.unlink()
    except FileNotFoundError:
        logger.info("No video frames found in %s", ouput_dir)
    vf = f"select=not(mod(n\\,{every_nth_frame}))"

    if not os.path.exists(ffmpeg_path):
        ffmpeg_path = "ffmpeg"
    if os.path.exists(input_video_file):
        subprocess.run(
            [
                ffmpeg_path,
                "-i",
                f"{input_video_file}",
                "-vf",
                f"{vf}",
                "-vsync",
                "vfr",
                "-q:v",
                "2",
                "-loglevel",
                "error",
                "-stats",
                f"{ouput_dir}/{frame_filename_pattern}",
            ],
            stdout=subprocess.PIPE,
        ).stdout.decode("utf-8")
    else:
        logger.warning(
            "WARNING!\n\nVideo not found: %s.\nPlease check your video path.",
            input_video_file,
        )

make_gif(image_filepaths=None, filename_patterns=None, base_dir=None, output_file=None, duration=100, loop=0, width=0, optimize=True, quality=50, display_to_notebook=False, force=False, **kwargs) staticmethod

Create a GIF from a list of images or a list of filenames.

Source code in hyfi/graphics/motion.py
@staticmethod
def make_gif(
    image_filepaths=None,
    filename_patterns: Optional[str] = None,
    base_dir: Optional[str] = None,
    output_file: Optional[str] = None,
    duration: int = 100,
    loop: int = 0,
    width: int = 0,
    optimize: bool = True,
    quality: int = 50,
    display_to_notebook: bool = False,
    force: bool = False,
    **kwargs,
) -> Optional[str]:
    """
    Create a GIF from a list of images or a list of filenames.
    """
    output_file = output_file or ""
    if os.path.exists(output_file) and not force:
        logger.info("Skipping GIF creation, already exists: %s", output_file)
        logger.info("If you want to re-create the GIF, set force=True")
    else:
        if image_filepaths is None and filename_patterns:
            image_filepaths = sorted(
                IOLIBs.get_filepaths(filename_patterns, base_dir=base_dir)
            )
        if not image_filepaths:
            logger.warning("no images found")
            return
        if frames := GUTILs.load_images(image_filepaths):
            frame_one = frames[0]
            frame_one.save(
                output_file,
                format="GIF",
                append_images=frames,
                save_all=True,
                duration=duration,
                loop=loop,
                optimize=optimize,
                quality=quality,
            )
            logger.info("Saved GIF to %s", output_file)
        else:
            logger.warning("No frames found for %s", filename_patterns)

    if display_to_notebook and os.path.exists(output_file):
        NBs.display_image(data=IOLIBs.read(output_file), width=width)

    return output_file