Skip to content

Commit 6a9c8de

Browse files
authored
Merge pull request #238 from mkcor/use-imageio
Replace `skimage.io` with `imageio.v3` for reading/writing images.
2 parents 56d6102 + 3a9dbf0 commit 6a9c8de

10 files changed

Lines changed: 250 additions & 188 deletions

_episodes/02-image-basics.md

Lines changed: 106 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ questions:
77
objectives:
88
- "Define the terms bit, byte, kilobyte, megabyte, etc."
99
- "Explain how a digital image is composed of pixels."
10+
- "Recommend using imageio (resp. skimage) for I/O (resp. image processing) tasks."
1011
- "Explain how images are stored in NumPy arrays."
1112
- "Explain the left-hand coordinate system used in digital images."
1213
- "Explain the RGB additive colour model used in digital images."
@@ -91,13 +92,14 @@ First, the necessary imports:
9192

9293
~~~
9394
"""
94-
* Python libraries for learning and performing image processing.*
95+
* Python libraries for learning and performing image processing.
96+
*
9597
"""
9698
import numpy as np
97-
import skimage.io
98-
import skimage.viewer
9999
import matplotlib.pyplot as plt
100100
import ipympl
101+
import imageio.v3 as iio
102+
import skimage
101103
~~~
102104
{: .language-python}
103105

@@ -116,8 +118,8 @@ import ipympl
116118
>
117119
> ~~~
118120
> import skimage # form 1, load whole skimage library
119-
> import skimage.io # form 2, load skimage.io module only
120-
> from skimage.io import imread # form 3, load only the imread function
121+
> import skimage.draw # form 2, load skimage.draw module only
122+
> from skimage.draw import disk # form 3, load only the disk function
121123
> import numpy as np # form 4, load all of numpy into an object called np
122124
> ~~~
123125
> {: .language-python }
@@ -127,30 +129,32 @@ import ipympl
127129
> > In the example above, form 1 loads the entire `skimage` library into the
128130
> > program as an object.
129131
> > Individual modules of the library are then available within that object,
130-
> > e.g. to access the `imread` function used in the example above,
131-
> > you would write `skimage.io.imread()`.
132+
> > e.g., to access the `disk` function used in [the drawing episode]({{ page.root }}{% link _episodes/04-drawing.md %}),
133+
> > you would write `skimage.draw.disk()`.
132134
> >
133-
> > Form 2 loads only the `io` module of `skimage` into the program.
135+
> > Form 2 loads only the `draw` module of `skimage` into the program.
134136
> > When we run the code,
135137
> > the program will take less time and use less memory
136138
> > because we will not load the whole `skimage` library.
137139
> > The syntax needed to use the module remains unchanged:
138-
> > to access the `imread` function,
140+
> > to access the `disk` function,
139141
> > we would use the same function call as given for form 1.
140142
> >
141143
> > To further reduce the time and memory requirements for your program,
142144
> > form 3 can be used to import only a specific function/class from a library/module.
143145
> > Unlike the other forms, when this approach is used,
144146
> > the imported function or class can be called by its name only,
145-
> > without prefacing it with the name of the module/library from which it was loaded,
146-
> > i.e., `imread()` instead of `skimage.io.imread()` using the example above.
147+
> > without prefixing it with the name of the module/library from which it was loaded,
148+
> > i.e., `disk()` instead of `skimage.draw.disk()` using the example above.
147149
> > One hazard of this form is that importing like this will overwrite any
148150
> > object with the same name that was defined/imported earlier in the program,
149-
> > i.e., the example above would replace any existing object called `imread`
150-
> > with the `imread` function from `skimage.io`.
151+
> > i.e., the example above would replace any existing object called `disk`
152+
> > with the `disk` function from `skimage.draw`.
151153
> >
152154
> > Finally, the `as` keyword can be used when importing,
153155
> > to define a name to be used as shorthand for the library/module being imported.
156+
> > This name is referred to as an alias. Typically, using an alias (such as
157+
> > `np` for the NumPy library) saves us a little typing.
154158
> > You may see `as` combined with any of the other first three forms of `import` statement.
155159
> >
156160
> > Which form is used often depends on
@@ -171,11 +175,28 @@ more efficiently run commands later in the session.
171175
172176
With that taken care of,
173177
let's load our image data from disk using
174-
the `imread` function from the `skimage.io` library and display it using
175-
the `imshow` function from the `matplotlib` library.
178+
the `imread` function from the `imageio.v3` module and display it using
179+
the `imshow` function from the `matplotlib.pyplot` module.
180+
`imageio` is a Python library for reading and writing image data.
181+
`imageio.v3` is specifying that we want to use version 3 of `imageio`. This
182+
version has the benefit of supporting nD (multidimensional) image data
183+
natively (think of volumes, movies).
184+
185+
> ## Why not use `skimage.io.imread()`
186+
>
187+
> The `skimage` library has its own function to read an image,
188+
> so you might be asking why we don't use it here.
189+
> Actually, `skimage.io.imread()` uses `iio.imread()` internally when loading an image into Python.
190+
> It is certainly something you may use as you see fit in your own code.
191+
> In this lesson, we use the `imageio` library to read or write (save) images,
192+
> while `skimage` is dedicated to performing operations on the images.
193+
> Using `imageio` gives us more flexibility, especially when it comes to
194+
> handling metadata.
195+
>
196+
{: .callout}
176197
177198
~~~
178-
image = skimage.io.imread(fname="data/eight.tif")
199+
image = iio.imread(uri="data/eight.tif")
179200
plt.imshow(image)
180201
~~~
181202
{: .language-python}
@@ -198,7 +219,7 @@ the bulk of a picture file is just arrays of numeric information that,
198219
when interpreted according to a certain rule set,
199220
become recognizable as an image to us.
200221
Our image of an eight is no exception,
201-
and `skimage.io` stored that image data in an array of arrays making
222+
and `imageio.v3` stored that image data in an array of arrays making
202223
a 5 x 3 matrix of 15 pixels.
203224
We can demonstrate that by calling on the shape property of our image variable
204225
and see the matrix by printing our image variable to the screen.
@@ -237,7 +258,7 @@ column labeled 1.
237258
Using array slicing, we can then address and assign a new value to that position.
238259
239260
~~~
240-
zero = skimage.io.imread(fname="data/eight.tif")
261+
zero = iio.imread(uri="data/eight.tif")
241262
zero[2,1]= 1.0
242263
"""
243264
The follwing line of code creates a new figure for imshow to use in displaying our output. Without it, plt.imshow() would overwrite our previous image in the cell above
@@ -307,7 +328,7 @@ print(zero)
307328
> > There are many possible solutions, but one method would be . . .
308329
> >
309330
> > ~~~
310-
> > five = skimage.io.imread(fname="data/eight.tif")
331+
> > five = iio.imread(uri="data/eight.tif")
311332
> > five[1,2]= 1.0
312333
> > five[3,0]= 1.0
313334
> > fig, ax = plt.subplots()
@@ -338,13 +359,14 @@ One common way is to use the numbers between 0 and 255 to allow for
338359
Let's try that out.
339360
340361
~~~
341-
#make a copy of eight
342-
three_colours = skimage.io.imread(fname="data/eight.tif")
362+
# make a copy of eight
363+
three_colours = iio.imread(uri="data/eight.tif")
343364

344-
#multiply the whole matrix by 128
365+
# multiply the whole matrix by 128
345366
three_colours = three_colours * 128
346367

347-
# set the middle row (index 2) to the value of 255., so you end up with the values 0.,128.,and 255
368+
# set the middle row (index 2) to the value of 255.,
369+
# so you end up with the values 0., 128., and 255.
348370
three_colours[2,:] = 255.
349371
fig, ax = plt.subplots()
350372
plt.imshow(three_colours)
@@ -413,15 +435,14 @@ for the colours red, green, and blue.
413435
Rather than loading it from a file, we will generate this example using numpy.
414436
415437
~~~
416-
#set the random seed so we all get the same matrix
438+
# set the random seed so we all get the same matrix
417439
pseudorandomizer = np.random.RandomState(2021)
418-
#create a 4 X 4 checkerboard of random colours
419-
checkerboard = pseudorandomizer.randint(0,255,size=(4,4,3)
420-
)
421-
#restore the default map as you show the image
440+
# create a 4 × 4 checkerboard of random colours
441+
checkerboard = pseudorandomizer.randint(0, 255, size=(4, 4, 3))
442+
# restore the default map as you show the image
422443
fig, ax = plt.subplots()
423444
plt.imshow(checkerboard)
424-
#display the arrays
445+
# display the arrays
425446
print(checkerboard)
426447
~~~
427448
{: .language-python}
@@ -454,11 +475,11 @@ print(checkerboard)
454475
Previously we had one number being mapped to one colour or intensity.
455476
Now we are combining the effect of 3 numbers to arrive at a single colour value.
456477
Let's see an example of that using the blue square at the end of the second row,
457-
which has the index [1,3].
478+
which has the index [1, 3].
458479
459480
~~~
460481
# extract all the colour information for the blue square
461-
upper_right_square = checkerboard[1,3,:]
482+
upper_right_square = checkerboard[1, 3, :]
462483
upper_right_square
463484
~~~
464485
{: .language-python}
@@ -484,38 +505,38 @@ We can do that by multiplying our image array representation with
484505
a 1d matrix that has a one for the channel we want to keep and zeros for the rest.
485506
486507
~~~
487-
red_channel = checkerboard * [1,0,0]
508+
red_channel = checkerboard * [1, 0, 0]
488509
fig, ax = plt.subplots()
489510
plt.imshow(red_channel)
490511
~~~
491512
{: .language-python}
492513
![Image of red channel](../fig/checkerboard-red-channel.png)
493514
~~~
494-
green_channel = checkerboard * [0,1,0]
515+
green_channel = checkerboard * [0, 1, 0]
495516
fig, ax = plt.subplots()
496517
plt.imshow(green_channel)
497518
~~~
498519
{: .language-python}
499520
![Image of green channel](../fig/checkerboard-green-channel.png)
500521
~~~
501-
blue_channel = checkerboard * [0,0,1]
522+
blue_channel = checkerboard * [0, 0, 1]
502523
fig, ax = plt.subplots()
503524
plt.imshow(blue_channel)
504525
~~~
505526
{: .language-python}
506527
507528
![Image of blue channel](../fig/checkerboard-blue-channel.png)
508529
509-
If we look at the upper [1,3] square in all three figures,
530+
If we look at the upper [1, 3] square in all three figures,
510531
we can see each of those colour contributions in action.
511532
Notice that there are several squares in the blue figure that look
512-
even more intensely blue than square [1,3].
533+
even more intensely blue than square [1, 3].
513534
When all three channels are combined though,
514535
the blue light of those squares is being diluted by the relative strength
515536
of red and green being mixed in with them.
516537
517538
518-
## 24 bit RGB Colour
539+
## 24-bit RGB Colour
519540
520541
This last colour model we used,
521542
known as the *RGB (Red, Green, Blue)* model, is the most common.
@@ -820,16 +841,16 @@ JPEG images can be viewed and manipulated easily on all computing platforms.
820841
> and then saves it as a BMP and as a JPEG image.
821842
>
822843
> ~~~
823-
> import skimage.io
844+
> import imageio.v3 as iio
824845
> import numpy as np
825846
>
826847
> dim = 5000
827848
>
828849
> img = np.zeros((dim, dim, 3), dtype="uint8")
829850
> img.fill(255)
830851
>
831-
> skimage.io.imsave(fname="data/ws.bmp", arr=img)
832-
> skimage.io.imsave(fname="data/ws.jpg", arr=img)
852+
> iio.imwrite(uri="data/ws.bmp", image=img)
853+
> iio.imwrite(uri="data/ws.jpg", image=img)
833854
> ~~~
834855
> {: .language-python}
835856
>
@@ -916,7 +937,7 @@ are shown below:
916937
917938
![Uncompressed histogram](../fig/quality-histogram.jpg)
918939
919-
We we learn how to make histograms such as these later on in the workshop.
940+
We learn how to make histograms such as these later on in the workshop.
920941
The differences in the colour histograms are even more apparent than in the
921942
images themselves;
922943
clearly the colours in the JPEG image are different from the uncompressed version.
@@ -957,26 +978,59 @@ such as when the image was captured,
957978
where it was captured,
958979
what type of camera was used and with what settings, etc.
959980
We normally don't see this metadata when we view an image,
960-
but programs exist that can allow us to view it if we wish to
981+
but we can view it independently if we wish to
961982
(see [_Accessing Metadata_](#viewing-metadata), below).
962983
The important thing to be aware of at this stage is that
963-
you cannot rely on the metadata of an image being preserved
984+
you cannot rely on the metadata of an image being fully preserved
964985
when you use software to process that image.
965-
The image processing library that we will use in the rest of this lesson,
966-
`skimage`, _does not_ include metadata when saving new images.
967-
So remember: **if metadata is important to you,
986+
The image reader/writer library that we use throughout this lesson,
987+
`imageio.v3`, includes metadata when saving new images but may fail to keep
988+
certain metadata fields.
989+
In any case, remember: **if metadata is important to you,
968990
take precautions to always preserve the original files**.
969991
970992
> ## Accessing Metadata
971993
>
972-
> Although `skimage` does not provide a way to display or explore the metadata
973-
> associated with an image (and subsequently cannot preserve that metadata
974-
> when modifying an image file),
975-
> other software exists that can help you to do so,
976-
> e.g. [Fiji](https://imagej.net/Fiji)
994+
> `imageio.v3` provides a way to display or explore the metadata
995+
> associated with an image. Metadata is served independently from pixel data:
996+
>
997+
> ~~~
998+
> # read metadata
999+
> metadata = iio.immeta(uri="data/eight.tif")
1000+
> # display the format-specific metadata
1001+
> metadata
1002+
> ~~~
1003+
> {: .language-python}
1004+
>
1005+
> ~~~
1006+
> {'is_fluoview': False,
1007+
> 'is_nih': False,
1008+
> 'is_micromanager': False,
1009+
> 'is_ome': False,
1010+
> 'is_lsm': False,
1011+
> 'is_reduced': False,
1012+
> 'is_shaped': True,
1013+
> 'is_stk': False,
1014+
> 'is_tiled': False,
1015+
> 'is_mdgel': False,
1016+
> 'compression': <COMPRESSION.NONE: 1>,
1017+
> 'predictor': 1,
1018+
> 'is_mediacy': False,
1019+
> 'description': '{"shape": [5, 3]}',
1020+
> 'description1': '',
1021+
> 'is_imagej': False,
1022+
> 'software': 'tifffile.py',
1023+
> 'resolution_unit': 1,
1024+
> 'resolution': (1.0, 1.0, 'NONE')}
1025+
> ~~~
1026+
> {: .output }
1027+
>
1028+
> Other software exists that can help you handle metadata,
1029+
> e.g., [Fiji](https://imagej.net/Fiji)
9771030
> and [ImageMagick](https://imagemagick.org/index.php).
978-
> We recommend you explore these options if you need to work with
1031+
> You may want to explore these options if you need to work with
9791032
> the metadata of your images.
1033+
>
9801034
{: .callout }
9811035
9821036
## Summary of image formats used in this lesson

0 commit comments

Comments
 (0)