diff --git a/docs/add_text.ipynb b/docs/add_text.ipynb new file mode 100644 index 0000000..9748fdd --- /dev/null +++ b/docs/add_text.ipynb @@ -0,0 +1,148 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Adding text on images" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import stackview\n", + "from skimage.io import imread\n", + "from skimage.filters import gaussian\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "image = imread('data/blobs.tif')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll apply Gaussian blur with sigma values from 1 to 10 and store the results in lists." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated 10 blurred images\n", + "Sigma labels: ['sigma=1', 'sigma=2', 'sigma=3', 'sigma=4', 'sigma=5', 'sigma=6', 'sigma=7', 'sigma=8', 'sigma=9', 'sigma=10']\n" + ] + } + ], + "source": [ + "# Initialize lists to store results\n", + "blurred_images = []\n", + "sigma_labels = []\n", + "\n", + "# Apply Gaussian blur with sigma values from 1 to 10\n", + "for sigma in range(1, 11):\n", + " blurred_image = gaussian(image, sigma=sigma, preserve_range=True)\n", + " blurred_images.append(blurred_image)\n", + " sigma_labels.append(f\"sigma={sigma}\")\n", + "\n", + "print(f\"Generated {len(blurred_images)} blurred images\")\n", + "print(f\"Sigma labels: {sigma_labels}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, we add text to images and show them afterwards." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(254, 256)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "images = stackview.add_text(blurred_images, sigma_labels, position=\"bottom-left\")\n", + "images[0].shape" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "stackview.animate(images)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/setup.py b/setup.py index e0bb8b7..81b650e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="stackview", - version="0.19.1", + version="0.19.2", license="BSD-3-Clause", author="Robert Haase", author_email="robert.haase@uni-leipzig.de", @@ -15,7 +15,7 @@ url="https://github.com/haesleinhuepf/stackview/", packages=setuptools.find_packages(), include_package_data=True, - install_requires=["numpy!=1.19.4", "ipycanvas", "ipywidgets", "scikit-image", "ipyevents", "toolz", "matplotlib", "ipykernel", "imageio", "ipympl", "wordcloud"], + install_requires=["numpy!=1.19.4", "ipycanvas", "ipywidgets", "scikit-image", "ipyevents", "toolz", "matplotlib", "ipykernel", "imageio", "ipympl", "wordcloud", "scipy"], python_requires='>=3.6', classifiers=[ "Programming Language :: Python :: 3", diff --git a/stackview/__init__.py b/stackview/__init__.py index 8096842..9e0f879 100644 --- a/stackview/__init__.py +++ b/stackview/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.19.1" +__version__ = "0.19.2" from ._static_view import jupyter_displayable_output, insight from ._utilities import merge_rgb @@ -27,3 +27,5 @@ from ._histogram import histogram from ._blend import blend from ._zoom import zoom +from ._add_text import add_text +from ._plot_profile import plot_profile diff --git a/stackview/_add_text.py b/stackview/_add_text.py new file mode 100644 index 0000000..5fad893 --- /dev/null +++ b/stackview/_add_text.py @@ -0,0 +1,114 @@ +import numpy as np +from typing import List, Union + +def add_text( + images: Union[List[np.ndarray], np.ndarray], + texts: List[str], + font_size: int = 16, + text_color: str = 'white', + bg_color: str = 'black', + position: str = 'top', + colormap: str = 'Greys_r' +) -> List[np.ndarray]: + """ + Burn text onto images using matplotlib. + + Parameters: + ----------- + images : list of np.ndarray or np.ndarray + List of images or n-dimensional array where first dimension is images + texts : list of str + List of text strings to burn onto each image + font_size : int + Font size for the text (default: 16) + text_color : str + Color of the text (default: 'white') + position : str + Text position: 'top', 'bottom', 'center', 'top-left', 'top-right', + 'bottom-left', 'bottom-right', 'center-left', 'center-right' (default: 'top') + colormap : str + Colormap to use for displaying the image (default: 'Greys_r') + + Returns: + -------- + list of np.ndarray + List of images with text burned in + """ + import matplotlib.pyplot as plt + from matplotlib.backends.backend_agg import FigureCanvasAgg + + # Convert to list if numpy array + if isinstance(images, np.ndarray): + images = [images[i] for i in range(images.shape[0])] + + if len(images) != len(texts): + raise ValueError(f"Number of images ({len(images)}) must match number of texts ({len(texts)})") + + result_images = [] + + for img, text in zip(images, texts): + # Create figure with exact image size + h, w = img.shape[:2] + dpi = 100 + fig = plt.figure(figsize=(w/dpi, h/dpi), dpi=dpi) + ax = fig.add_axes([0, 0, 1, 1]) + ax.axis('off') + + # Display the image + ax.imshow(img, cmap=colormap) + + # Determine text position + # Default to center + x_pos, y_pos = 0.5, 0.5 + ha, va = 'center', 'center' + + # Vertical positioning + if 'top' in position: + y_pos, va = 0.95, 'top' + elif 'bottom' in position: + y_pos, va = 0.05, 'bottom' + + # Horizontal positioning + if 'left' in position: + x_pos, ha = 0.05, 'left' + elif 'right' in position: + x_pos, ha = 0.95, 'right' + + # Add text with background + px = 1.0 / w + for delta_x, delta_y in [(-px, -px), (px, -px), (-px, px), (px, px)]: + ax.text( + x_pos+delta_x, y_pos+delta_y, text, + transform=ax.transAxes, + fontsize=font_size, + color=bg_color, + ha=ha, + va=va + ) + ax.text( + x_pos, y_pos, text, + transform=ax.transAxes, + fontsize=font_size, + color=text_color, + ha=ha, + va=va + ) + + # Convert figure to numpy array + canvas = FigureCanvasAgg(fig) + canvas.draw() + buf = canvas.buffer_rgba() + result = np.asarray(buf) + + # Convert RGBA to RGB if original was RGB + if img.ndim == 3 and img.shape[2] == 3: + result = result[:, :, :3] + # Convert back to single-channel if original was single-channel + elif img.ndim == 2 or img.shape[-1] not in [3, 4]: + result = result[:, :, 0] + result = result.astype(img.dtype) + + result_images.append(result) + plt.close(fig) + + return result_images \ No newline at end of file diff --git a/stackview/_bia_bob_plugins.py b/stackview/_bia_bob_plugins.py index 2aceb26..e7c00ea 100644 --- a/stackview/_bia_bob_plugins.py +++ b/stackview/_bia_bob_plugins.py @@ -36,6 +36,14 @@ def list_bia_bob_plugins(): * Allows switching between multiple images and displaying them with a slider. stackview.switch(images:list) + * Add bounding boxes to an image. + bounding_boxes = [ {'x':5, 'y':5, 'width':10, 'height':15} ] + stackview.add_bounding_boxes(image, bounding_boxes) + + * Add text to images at specified positions. + texts = ["Sample Text"] # List of texts for each image + stackview.add_text([image], texts, position='top-left') + * Allows plotting a scatterplot of a pandas dataframe while interactively choosing the columns and using a lasso tool for selecting data points stackview.scatterplot(dataframe, column_x, column_y, selection_column) diff --git a/stackview/_plot_profile.py b/stackview/_plot_profile.py new file mode 100644 index 0000000..4012951 --- /dev/null +++ b/stackview/_plot_profile.py @@ -0,0 +1,73 @@ +def plot_profile(image, point1, point2, linecolor='orange'): + """ + Extract intensity along a line between two points and create a combined figure. + + Parameters: + ----------- + image : numpy.ndarray + Input image (2D grayscale or 3D RGB) + point1 : tuple + First point coordinates (x, y) + point2 : tuple + Second point coordinates (x, y) + linecolor : str + Color for the line and points on the image and the intensity profile plot + + Returns: + -------- + numpy.ndarray + Combined figure as a numpy array (RGB image) + """ + import numpy as np + import matplotlib.pyplot as plt + from scipy import ndimage + from matplotlib.backends.backend_agg import FigureCanvasAgg + + x1, y1 = point1 + x2, y2 = point2 + + # Calculate number of points along the line + length = int(np.hypot(x2 - x1, y2 - y1)) + + # Generate coordinates along the line + x_coords = np.linspace(x1, x2, length) + y_coords = np.linspace(y1, y2, length) + + # Extract intensity values along the line + if image.ndim == 2: # Grayscale image + intensities = ndimage.map_coordinates(image, [y_coords, x_coords], order=1) + else: # RGB image - use average of channels or first channel + intensities = ndimage.map_coordinates( + np.mean(image, axis=2), [y_coords, x_coords], order=1 + ) + + # Create figure with two subplots + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) + + # Plot the image with the line + if image.ndim == 2: + ax1.imshow(image, cmap='gray') + else: + ax1.imshow(image) + + ax1.plot([x1, x2], [y1, y2], '-', color=linecolor, linewidth=2, label='Profile Line') + ax1.plot([x1, x2], [y1, y2], 'o', color=linecolor, markersize=8) + ax1.axis('off') + + # Plot the intensity profile + ax2.plot(intensities, linewidth=2, color=linecolor) + ax2.set_xlabel('Distance along line (pixels)') + ax2.set_ylabel('Intensity') + ax2.grid(True, alpha=0.3) + + plt.subplots_adjust(left=0, right=1, bottom=0, top=1, wspace=0.05) + + # Convert figure to numpy array + canvas = FigureCanvasAgg(fig) + canvas.draw() + buf = canvas.buffer_rgba() + result = np.asarray(buf) + + plt.close(fig) # Close figure to prevent displaying + + return result \ No newline at end of file