Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add images tutorial #1470

Merged
merged 36 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f10d266
add work in progress version of the images tutorial
weiglszonja May 4, 2022
2caa64c
Merge branch 'dev' into docs/add_images_tutorial
weiglszonja May 17, 2022
cd662a4
rearrange tutorial
weiglszonja May 17, 2022
24440e9
add newline at end of file
weiglszonja May 17, 2022
db98a4e
Merge branch 'dev' into docs/add_images_tutorial
bendichter Jul 13, 2022
b6e7ccb
Apply suggestions from code review
weiglszonja Jul 13, 2022
8241383
change unit to n.a.
weiglszonja Jul 13, 2022
c17d451
change image dtype to uint8
weiglszonja Jul 13, 2022
d819f66
fix incorrect import from PIL
weiglszonja Jul 13, 2022
e939e4d
mention expected color channel order in imageseries
weiglszonja Jul 13, 2022
d523176
fix image resolution values to be floats
weiglszonja Jul 13, 2022
14a59dd
add io section
weiglszonja Jul 14, 2022
bda80c2
remove images section from file basics tutorial
weiglszonja Jul 14, 2022
9f521a8
demonstrate external_file mode with ImageSeries
weiglszonja Jul 14, 2022
9a8cab9
demonstrate external_file mode with ImageSeries
weiglszonja Jul 14, 2022
06a7f7a
Merge branch 'dev' into docs/add_images_tutorial
bendichter Jul 14, 2022
ce3e24b
Apply suggestions from code review
weiglszonja Jul 14, 2022
f578ae5
Apply suggestions from code review
weiglszonja Jul 14, 2022
b013721
apply suggestions from code review
weiglszonja Jul 14, 2022
17bb733
Apply suggestions from code review
weiglszonja Jul 15, 2022
78fda5e
Update docs/gallery/domain/images.py
oruebel Jul 15, 2022
52d5aae
add dandiarchive as external link
weiglszonja Jul 15, 2022
ab6ef2a
changes based on code review
weiglszonja Jul 15, 2022
0fefe7f
Merge remote-tracking branch 'origin/docs/add_images_tutorial' into d…
weiglszonja Jul 15, 2022
ee79f52
Update docs/gallery/domain/images.py
weiglszonja Jul 15, 2022
27fbd51
Merge branch 'docs/add_images_tutorial' of github.com:NeurodataWithou…
weiglszonja Jul 15, 2022
40fcdf1
shorten dandi external link
weiglszonja Jul 15, 2022
e158df1
Merge branch 'dev' into docs/add_images_tutorial
bendichter Jul 15, 2022
bf94a9c
flake8
weiglszonja Jul 15, 2022
2c70d43
Apply suggestions from code review
weiglszonja Jul 18, 2022
786c798
extend external files description
weiglszonja Jul 18, 2022
694c39a
flake8
weiglszonja Jul 18, 2022
9b92b50
Merge branch 'dev' into docs/add_images_tutorial
weiglszonja Jul 18, 2022
789ab8a
update CHANGELOG.md
weiglszonja Jul 18, 2022
2a479a5
add thumbnail image
weiglszonja Jul 18, 2022
b44598e
Merge branch 'dev' into docs/add_images_tutorial
oruebel Jul 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## Documentation and tutorial enhancements:
- Support explicit ordering of sphinx gallery tutorials in the docs. @oruebel (#1504), @bdichter (#1495)
- Add developer guide on how to create a new tutorial. @oruebel (#1504)
- Add images tutorial. @weiglszonja (#1470)

## PyNWB 2.1.0 (July 6, 2022)

Expand Down
278 changes: 278 additions & 0 deletions docs/gallery/domain/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
"""
.. _images:

Images
==================================

This tutorial will demonstrate the usage of the :py:mod:`pynwb.image` module for adding
images to an :py:class:`~pynwb.file.NWBFile`.

Image data can be a collection of individual images or movie segments (as a movie is simply a series of images),
about the subject, the environment, the presented stimuli, or other parts
related to the experiment. This tutorial focuses in particular on the usage of:

* :py:class:`~pynwb.image.OpticalSeries` for series of images that were presented as stimulus
* :py:class:`~pynwb.image.ImageSeries`, for series of images (movie segments);
* :py:class:`~pynwb.image.GrayscaleImage`, :py:class:`~pynwb.image.RGBImage`, :py:class:`~pynwb.image.RGBAImage`, for static images;

The following examples will reference variables that may not be defined within the block they are used in. For
clarity, we define them here:
"""
# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_image_data.png'
from datetime import datetime
from dateutil import tz

import numpy as np
from PIL import Image

from pynwb import NWBFile, NWBHDF5IO
from pynwb.base import Images
from pynwb.image import RGBAImage, RGBImage, GrayscaleImage, OpticalSeries, ImageSeries

# Define file paths used in the tutorial
import os
nwbfile_path = os.path.abspath("images_tutorial.nwb")
moviefiles_path = [
oruebel marked this conversation as resolved.
Show resolved Hide resolved
os.path.abspath("image/file_1.tiff"),
os.path.abspath("image/file_2.tiff"),
os.path.abspath("image/file_3.tiff"),
]

####################
# Create an NWB File
# ------------------
#
# Create an :py:class:`~pynwb.file.NWBFile` object with the required fields
# (``session_description``, ``identifier``, ``session_start_time``) and additional metadata.

session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific"))

nwbfile = NWBFile(
session_description="Mouse exploring an open field", # required
identifier="Mouse5_Day3", # required
session_start_time=session_start_time, # required
session_id="session_1234", # optional
experimenter="My Name", # optional
lab="My Lab Name", # optional
institution="University of My Institution", # optional
related_publications="DOI:10.1016/j.neuron.2016.12.011", # optional
)

nwbfile

####################
#
# .. seealso::
# You can learn more about the :py:class:`~pynwb.file.NWBFile` format in the :ref:`basics` tutorial.
#
# OpticalSeries: Storing series of images as stimuli
# --------------------------------------------------
#
# :py:class:`~pynwb.image.OpticalSeries` is for time series of images that were presented
# to the subject as stimuli.
# We will create an :py:class:`~pynwb.image.OpticalSeries` object with the name
# ``"StimulusPresentation"`` representing what images were shown to the subject and at what times.
#
# Image data can be stored either in the HDF5 file or as an external image file.
# For this tutorial, we will use fake image data with shape of ``('time', 'x', 'y', 'RGB') = (200, 50, 50, 3)``.
# As in all :py:class:`~pynwb.base.TimeSeries`, the first dimension is time.
# The second and third dimensions represent x and y.
# The fourth dimension represents the RGB value (length of 3) for color images.
weiglszonja marked this conversation as resolved.
Show resolved Hide resolved
#
# NWB differentiates between acquired data and data that was presented as stimulus.
# We can add it to the :py:class:`~pynwb.file.NWBFile` object as stimulus data using
# the :py:meth:`~pynwb.file.NWBFile.add_stimulus` method.
#

image_data = np.random.randint(low=0, high=255, size=(200, 50, 50, 3), dtype=np.uint8)
optical_series = OpticalSeries(
name="StimulusPresentation", # required
distance=0.7, # required
field_of_view=[0.2, 0.3, 0.7], # required
orientation="lower left", # required
data=image_data,
unit="n.a.",
format="raw",
starting_frame=[0.0],
rate=1.0,
comments="no comments",
description="The images presented to the subject as stimuli",
)

nwbfile.add_stimulus(timeseries=optical_series)

####################
# ImageSeries: Storing series of images as acquisition
# ----------------------------------------------------
#
# :py:class:`~pynwb.image.ImageSeries` is a general container for time series of images acquired during
# the experiment. Image data can be stored either in the HDF5 file or as an external image file.
# When color images are stored in the HDF5 file the color channel order is expected to be RGB.
#
# We can add raw data to the :py:class:`~pynwb.file.NWBFile` object as *acquisition* using
# the :py:meth:`~pynwb.file.NWBFile.add_acquisition` method.
#

image_data = np.random.randint(low=0, high=255, size=(200, 50, 50, 3), dtype=np.uint8)
behavior_images = ImageSeries(
name="ImageSeries",
data=image_data,
description="Image data of an animal moving in environment.",
unit="n.a.",
format="raw",
rate=1.0,
starting_time=0.0,
)

nwbfile.add_acquisition(behavior_images)

####################
# External Files
# ^^^^^^^^^^^^^^
#
# External files (e.g. video files of the behaving animal) can be added to the :py:class:`~pynwb.file.NWBFile` by creating
# an :py:class:`~pynwb.image.ImageSeries` object using the :py:attr:`~pynwb.image.ImageSeries.external_file` attribute that specifies the
# path to the external file(s) on disk. The file(s) path must be relative to the path of the NWB file.
# Either ``external_file`` or ``data`` must be specified, but not both.
#
# If the sampling rate is constant, use :py:attr:`~pynwb.base.TimeSeries.rate` and :py:attr:`~pynwb.base.TimeSeries.starting_time` to specify time.
# For irregularly sampled recordings, use :py:attr:`~pynwb.base.TimeSeries.timestamps` to specify time for each sample image.
#
# Each external image may contain one or more consecutive frames of the full :py:class:`~pynwb.image.ImageSeries`.
# The :py:attr:`~pynwb.image.ImageSeries.starting_frame` attribute serves as an index to indicate which frame
# each file contains.
# For example, if the ``external_file`` dataset has three paths to files and the first and the second file have 2 frames,
# and the third file has 3 frames, then this attribute will have values `[0, 2, 4]`.
oruebel marked this conversation as resolved.
Show resolved Hide resolved

external_file = [
os.path.relpath(movie_path, nwbfile_path) for movie_path in moviefiles_path
]
# We have 3 movie files each containing multiple frames. We here need to specify the timestamp for each frame.
timestamps = [0.0, 0.04, 0.07, 0.1, 0.14, 0.16, 0.21]
oruebel marked this conversation as resolved.
Show resolved Hide resolved
behavior_external_file = ImageSeries(
name="ExternalFiles",
description="Behavior video of animal moving in environment.",
unit="n.a.",
external_file=external_file,
format="external",
starting_frame=[0, 2, 4],
timestamps=timestamps,
)

nwbfile.add_acquisition(behavior_external_file)

####################
oruebel marked this conversation as resolved.
Show resolved Hide resolved
# .. note::
# See the :dandi:`External Links in NWB and DANDI </2022/03/03/external-links-organize.html>`
# guidelines of the :dandi:`DANDI <>` data archive for best practices on how to organize
# external files, e.g., movies and images.
#
# Static images
# -------------
weiglszonja marked this conversation as resolved.
Show resolved Hide resolved
#
# Static images can be stored in an :py:class:`~pynwb.file.NWBFile` object by creating an
# :py:class:`~pynwb.image.RGBAImage`, :py:class:`~pynwb.image.RGBImage` or
# :py:class:`~pynwb.image.GrayscaleImage` object with the image data.
#
# .. note::
# All basic image types :py:class:`~pynwb.image.RGBAImage`, :py:class:`~pynwb.image.RGBImage`, and
# :py:class:`~pynwb.image.GrayscaleImage` provide the optional: 1) ``description`` parameter to include a
# text description about the image and 2) ``resolution`` parameter to specify the *pixels / cm* resolution
# of the image.
#
# RGBAImage: for color images with transparency
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#
# :py:class:`~pynwb.image.RGBAImage` is for storing data of color image with transparency.
# :py:attr:`~pynwb.image.RGBAImage.data` must be 3D where the first and second dimensions
# represent x and y. The third dimension has length 4 and represents the RGBA value.
#

img = Image.open("docs/source/figures/logo_pynwb.png") # an example image

rgba_logo = RGBAImage(
name="pynwb_RGBA_logo",
data=np.array(img),
resolution=70.0, # in pixels / cm
description="RGBA version of the PyNWB logo.",
)

####################
# RGBImage: for color images
# ^^^^^^^^^^^^^^^^^^^^^^^^^^
#
# :py:class:`~pynwb.image.RGBImage` is for storing data of RGB color image.
# :py:attr:`~pynwb.image.RGBImage.data` must be 3D where the first and second dimensions
# represent x and y. The third dimension has length 3 and represents the RGB value.
#

rgb_logo = RGBImage(
name="pynwb_RGB_logo",
data=np.array(img.convert("RGB")),
resolution=70.0,
description="RGB version of the PyNWB logo.",
)

####################
# GrayscaleImage: for grayscale images
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#
# :py:class:`~pynwb.image.GrayscaleImage` is for storing grayscale image data.
# :py:attr:`~pynwb.image.GrayscaleImage.data` must be 2D where the first and second dimensions
# represent x and y.
#

gs_logo = GrayscaleImage(
name="pynwb_Grayscale_logo",
data=np.array(img.convert("L")),
description="Grayscale version of the PyNWB logo.",
resolution=35.433071,
)

####################
# Images: a container for images
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#
# Add the images to an :py:class:`~pynwb.base.Images` container
# that accepts any of these image types.

images = Images(
name="logo_images",
images=[rgb_logo, rgba_logo, gs_logo],
description="A collection of logo images presented to the subject.",
)

nwbfile.add_acquisition(images)

####################
# Writing the images to an NWB File
# ---------------------------------------
# As demonstrated in the :ref:`basic_writing` tutorial, we will use :py:class:`~pynwb.NWBHDF5IO`
# to write the file.

with NWBHDF5IO(nwbfile_path, "w") as io:
io.write(nwbfile)

####################
# Reading and accessing data
# --------------------------
#
# To read the NWB file, use another :py:class:`~pynwb.NWBHDF5IO` object,
# and use the :py:meth:`~pynwb.NWBHDF5IO.read` method to retrieve an
# :py:class:`~pynwb.file.NWBFile` object.
#
# We can access the data added as acquisition to the NWB File by indexing ``nwbfile.acquisition``
# with the name of the :py:class:`~pynwb.image.ImageSeries` object "ImageSeries".
#
# We can also access :py:class:`~pynwb.image.OpticalSeries` data that was added to the NWB File
# as stimuli by indexing ``nwbfile.stimulus`` with the name of the :py:class:`~pynwb.image.OpticalSeries`
# object "StimulusPresentation".
# Data arrays are read passively from the file.
# Accessing the ``data`` attribute of the :py:class:`~pynwb.image.OpticalSeries` object
# does not read the data values into memory, but returns an HDF5 object that can be indexed to read data.
# Use the ``[:]`` operator to read the entire data array into memory.

with NWBHDF5IO(nwbfile_path, "r") as io:
read_nwbfile = io.read()
print(read_nwbfile.acquisition["ImageSeries"])
print(read_nwbfile.stimulus["StimulusPresentation"].data[:])
39 changes: 0 additions & 39 deletions docs/gallery/general/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,6 @@
from pynwb.epoch import TimeIntervals
from pynwb.file import Subject
from pynwb.behavior import SpatialSeries, Position
from pynwb.image import RGBAImage, RGBImage, GrayscaleImage
from pynwb.base import Images
from PIL import Image
from datetime import datetime
from dateutil import tz

Expand Down Expand Up @@ -459,7 +456,6 @@
print(read_nwbfile.processing["behavior"]["Position"]["SpatialSeries"])

####################

# .. _reuse_timestamps:
#
# Reusing timestamps
Expand Down Expand Up @@ -594,41 +590,6 @@

nwbfile.add_time_intervals(sleep_stages)

####################
# Images
# ------
#
# You can store static images within the NWB file as well. These can be images of the subject, the environment, stimuli,
# or really anything.
#
# .. note::
# All basic image types :py:class:`~pynwb.image.RGBAImage`, :py:class:`~pynwb.image.RGBImage`, and
# :py:class:`~pynwb.image.GrayscaleImage` provide the optional: 1) ``description`` parameter to include a
# text description about the image and 2) ``resolution`` parameter to specify the *pixels / cm* resolution
# of the image.

img = Image.open("docs/source/figures/logo_nwb.png") # an example image

# you can store an RGBA image
rgba_logo = RGBAImage(name="RGBA_logo", data=np.array(img))

# or RGB
rgb_logo = RGBImage(name="RGB_logo", data=np.array(img.convert("RGB")))

# or Grayscale. Here with the optional description and resolution specified.
gs_logo = GrayscaleImage(
name="Grayscale_logo",
data=np.array(img.convert("L")),
description="Grayscale version of the NWB logo",
resolution=35.433071,
)

# Images is a container that accepts any of these image types
images = Images(name="logo_images", images=[rgb_logo, rgba_logo, gs_logo])

# Finally, do not forget to add the images object to the nwb file.
nwbfile.processing["behavior"].add(images)

####################
# Now we overwrite the file with all of the data

Expand Down
3 changes: 2 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ def __call__(self, filename):
'incf_collection': ('https://training.incf.org/collection/%s', ''),
'nwb_extension': ('https://github.com/nwb-extensions/%s', ''),
'pynwb': ('https://github.com/NeurodataWithoutBorders/pynwb/%s', ''),
'nwb_overview': ('https://nwb-overview.readthedocs.io/en/latest/%s', '')}
'nwb_overview': ('https://nwb-overview.readthedocs.io/en/latest/%s', ''),
'dandi': ('https://www.dandiarchive.org/%s', '')}

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.