Quickstart
If you're comfortable with Julia or have used another image-processing package before, this page may help you get started quickly. If some of the terms or concepts here seem strange, don't worry—there are much more detailed explanations in the following sections.
To start with, let's load the Images.jl
package:
julia> using Images
Images are just arrays
For most purposes, any AbstractArray
can be treated as an image. For example, numeric array can be interpreted as a grayscale image.
julia> img = rand(4, 3)
4×3 Matrix{Float64}: 0.517866 0.525796 0.374141 0.749719 0.840641 0.728543 0.552534 0.523948 0.00277646 0.0834401 0.0128461 0.134685
Don't worry if you don't get the "image" result, that's expected because it's actually recognized as a numerical array and not an image. You will learn how to automatically display an image later in JuliaImages.
We could also select a region-of-interest from a larger image
# generate an image that starts black in the upper left
# and gets bright in the lower right
img = Array(reshape(range(0,stop=1,length=10^4), 100, 100))
# make a copy
img_c = img[51:70, 21:70] # see the red region
# make a view
img_v = @view img[16:35, 41:90] # see the blue region
As you might know, changing the value of a view affects the original image, while changing a copy does not:
fill!(img_c, 1) # red region in original doesn't change
fill!(img_v, 0) # blue region is changed
Some add-on packages enable additional behavior. For example,
using Unitful, AxisArrays
using Unitful: mm, s
img = AxisArray(rand(256, 256, 6, 50), (:x, :y, :z, :time), (0.4mm, 0.4mm, 1mm, 2s))
defines a 4d image (3 space dimensions plus one time dimension) with the specified name and physical pixel spacing for each coordinate. The AxisArrays
package supports rich and efficient operations on such arrays, and can be useful to keep track of not just pixel spacing but the orientation convention used for multidimensional images.
JuliaImages interoperates smoothly with AxisArrays
and many other packages. As further examples,
the
ImageMetadata
package (incorporated intoImages
itself) allows you to "tag" images with custom metadatathe
IndirectArrays
package supports indexed (colormap) imagesthe
MappedArrays
package allows you to represent lazy value-transformations, facilitating work with images that may be too large to store in memory at onceImageTransformations
allows you to encode rotations, shears, deformations, etc., either eagerly or lazily
It is very easy to define new array types in Julia–and consequently specialized images or operations–and have them interoperate smoothly with the vast majority of functions in JuliaImages.
Elements of images are pixel colorants
Elements of an image are called pixels, and JuliaImages treats pixels as first-class objects. For example, we have Gray
for grayscale pixels, RGB
for RGB color pixels, Lab
for Lab colors, etc.
To create a pixel, initiate a struct of that type:
Gray(0.0) # black
Gray(1.0) # white
RGB(1.0, 0.0, 0.0) # red
RGB(0.0, 1.0, 0.0) # green
RGB(0.0, 0.0, 1.0) # blue
An image is just an array of pixel objects:
julia> img_gray = rand(Gray, 2, 2)
2×2 Array{Gray{Float64},2} with eltype Gray{Float64}: Gray{Float64}(0.412225) Gray{Float64}(0.755344) Gray{Float64}(0.0368978) Gray{Float64}(0.198098)
julia> img_rgb = rand(RGB, 2, 2)
2×2 Array{RGB{Float64},2} with eltype RGB{Float64}: RGB{Float64}(0.709158,0.584705,0.186682) … RGB{Float64}(0.202959,0.435521,0.116792) RGB{Float64}(0.760013,0.693292,0.20883) RGB{Float64}(0.0709577,0.888894,0.310744)
julia> img_lab = rand(Lab, 2, 2)
2×2 Array{Lab{Float64},2} with eltype Lab{Float64}: Lab{Float64}(14.7804,-55.7728,-50.3997) … Lab{Float64}(68.5718,-91.6112,127.303) Lab{Float64}(61.1431,62.8829,-55.5891) Lab{Float64}(74.6444,-105.937,81.6293)
Color channels are not their own dimension of the image array; rather, an image is an array of pixels. As you can see, both img_rgb
and img_lab
images are of size $2 \times 2$ (instead of $2 \times 2 \times 3$ or $3 \times 2 \times 2$).
It's recommended to use Gray
instead of Number
types since it indicates that the array is best interpreted as a grayscale image. For example, this tells Atom/Juno
and Jupyter
to display the array as an image instead of a matrix of numbers. There's no performance overhead for using Gray
over Number
.
This design choice facilitates generic code that can handle both grayscale and color images without needing to introduce extra loops or checks for a color dimension. It also provides more rational support for 3d grayscale images—which might happen to have size 3 along the third dimension—and consequently helps unify the "computer vision" and "biomedical image processing" communities.
Converting between colorants and separate color channels
Conversions between different Colorant
s are straightforward:
julia> RGB.(img_gray) # Gray => RGB
2×2 Array{RGB{Float64},2} with eltype RGB{Float64}: RGB{Float64}(0.412225,0.412225,0.412225) … RGB{Float64}(0.755344,0.755344,0.755344) RGB{Float64}(0.0368978,0.0368978,0.0368978) RGB{Float64}(0.198098,0.198098,0.198098)
julia> Gray.(img_rgb) # RGB => Gray
2×2 Array{Gray{Float64},2} with eltype Gray{Float64}: Gray{Float64}(0.576542) Gray{Float64}(0.32965) Gray{Float64}(0.658013) Gray{Float64}(0.578422)
You'll see broadcasting semantics used in JuliaImages here and there, check the documentation if you're not familiar with it.
Sometimes, to work with other packages, you'll need to convert a $m \times n$ RGB
image to $m \times n \times 3$ numeric array and vice versa. The functions channelview
and colorview
are designed for this purpose. For example:
julia> img_CHW = channelview(img_rgb) # 3 * 2 * 2
3×2×2 reinterpret(reshape, Float64, ::Array{RGB{Float64},2}) with eltype Float64: [:, :, 1] = 0.709158 0.760013 0.584705 0.693292 0.186682 0.20883 [:, :, 2] = 0.202959 0.0709577 0.435521 0.888894 0.116792 0.310744
julia> img_HWC = permutedims(img_CHW, (2, 3, 1)) # 2 * 2 * 3
2×2×3 Array{Float64, 3}: [:, :, 1] = 0.709158 0.202959 0.760013 0.0709577 [:, :, 2] = 0.584705 0.435521 0.693292 0.888894 [:, :, 3] = 0.186682 0.116792 0.20883 0.310744
To convert a channelview
array into an array of pixels, ensure the first dimension is the color channel and use colorview
:
julia> img_rgb = colorview(RGB, img_CHW) # 2 * 2
2×2 Array{RGB{Float64},2} with eltype RGB{Float64}: RGB{Float64}(0.709158,0.584705,0.186682) … RGB{Float64}(0.202959,0.435521,0.116792) RGB{Float64}(0.760013,0.693292,0.20883) RGB{Float64}(0.0709577,0.888894,0.310744)
It is best practice to manipulate images in the colorview
representation. Users from other languages may be more familiar with the channelview
representation, however this loses the colorant information by converting an image to a raw numeric array.
We use channel-height-width (CHW) order instead of HWC order because this provides a memory friendly indexing mechanisim for Array
. By default, in Julia the first index is also the fastest (i.e., has adjacent storage in memory). For more details, please refer to the performance tip: Access arrays in memory order, along columns
You can use PermutedDimsArray
to "reinterpret" the orientation of a chunk of memory without making a copy, or permutedims
if you want a copy.
For Gray
images, the following codes are almost equivalent except that the construction version copies the data while the view version doesn't.
img_num = rand(4, 4)
img_gray_copy = Gray.(img_num) # construction
img_num_copy = Float64.(img_gray_copy) # construction
img_gray_view = colorview(Gray, img_num) # view
img_num_view = channelview(img_gray_view) # view
The 0-to-1 intensity scale
In JuliaImages, images are displayed assuming by default that 0 means "black" and 1 means "white" (or "saturated" for an RGB image).
Importantly, this applies even for intensities encoded as 8-bit integers which themselves range from 0 to 255 (not from 0 to 1). This is made possible with the special number type N0f8
which interprets an 8-bit "integer" as if it had been scaled by 1/255, encoding values from 0 to 1 in 256 steps.
The name N0f8
stands for Normalized, with 0 integer bits and 8 fractional bits. There are other types like N0f16
for working with 16-bit images (and even N2f14
for images acquired with a 14-bit camera, etc.).
julia> img_n0f8 = rand(N0f8, 2, 2)
2×2 reinterpret(N0f8, ::Matrix{UInt8}): 0.435 0.725 0.816 0.192
julia> float.(img_n0f8)
2×2 Matrix{Float32}: 0.435294 0.72549 0.815686 0.192157
This infrastructure allows us to unify "integer" and floating-point images, and avoids the need for special conversion functions (e.g., im2double
in MATLAB) that change the value of pixels when your main goal is simply to change the type (numeric precision and properties) used to represent the pixel.
Although it's not recommended, but you can use rawview
to get the underlying storage data and convert it to UInt8
(or other types) if you insist.
julia> img_n0f8_raw = rawview(img_n0f8)
2×2 rawview(reinterpret(N0f8, ::Matrix{UInt8})) with eltype UInt8: 0x6f 0xb9 0xd0 0x31
julia> float.(img_n0f8_raw)
2×2 Matrix{Float64}: 111.0 185.0 208.0 49.0
Conversions between the storage type, i.e., the actual numeric type, without changing the color type are supported by the following functions:
julia> img = rand(Gray{N0f8}, 2, 2)
2×2 Array{Gray{N0f8},2} with eltype Gray{N0f8}: Gray{N0f8}(0.925) Gray{N0f8}(0.267) Gray{N0f8}(0.345) Gray{N0f8}(0.827)
julia> img_float32 = float32.(img) # Gray{N0f8} => Gray{Float32}
2×2 Array{Gray{Float32},2} with eltype Gray{Float32}: Gray{Float32}(0.92549) Gray{Float32}(0.266667) Gray{Float32}(0.345098) Gray{Float32}(0.827451)
julia> img_n0f16 = n0f16.(img_float32) # Gray{Float32} => Gray{N0f16}
2×2 Array{Gray{N0f16},2} with eltype Gray{N0f16}: Gray{N0f16}(0.92549) Gray{N0f16}(0.26667) Gray{N0f16}(0.3451) Gray{N0f16}(0.82745)
If you don't want to specify the destination type, float
is designed for this:
julia> img_n0f8 = rand(Gray{N0f8}, 2, 2)
2×2 Array{Gray{N0f8},2} with eltype Gray{N0f8}: Gray{N0f8}(0.886) Gray{N0f8}(0.98) Gray{N0f8}(0.522) Gray{N0f8}(0.412)
julia> img_float = float.(img_n0f8) # Gray{N0f8} => Gray{Float32}
2×2 Array{Gray{Float32},2} with eltype Gray{Float32}: Gray{Float32}(0.886275) Gray{Float32}(0.980392) Gray{Float32}(0.521569) Gray{Float32}(0.411765)
For a view-like conversion without new memory allocation, of_eltype
in MappedArrays
is designed for this:
julia> using MappedArrays
julia> img_float_view = of_eltype(Gray{Float32}, img_n0f8)
2×2 mappedarray(MappedArrays.var"#7#9"{Gray{Float32}}(), MappedArrays.var"#8#10"{Gray{N0f8}}(), ::Array{Gray{N0f8},2}) with eltype Gray{Float32}: Gray{Float32}(0.886275) Gray{Float32}(0.980392) Gray{Float32}(0.521569) Gray{Float32}(0.411765)
julia> eltype(img_float_view)
Gray{Float32}
Arrays with arbitrary indices
If you have an input image and perform some kind of spatial transformation on it, how do pixels (or voxels) in the transformed image match up to pixels in the input? Through Julia's support for arrays with indices that start at values other than 1, it is possible to allow array indices to represent absolute position in space, making it straightforward to keep track of the correspondence between location across multiple images. More information can be found in Keeping track of location with unconventional indices.
Index of functions
See Summary and function reference for more information about each of the topics below. The list below is accessible via ?Images
from the Julia REPL. If you've used other frameworks previously, you may also be interested in the Comparison with other image processing frameworks. Also described are the ImageFeatures.jl and ImageSegmentation.jl packages, which support a number of algorithms important for computer vision.
Constructors, conversions, and traits:
- Construction: use constructors of specialized packages, e.g.,
AxisArray
,ImageMeta
, etc. - "Conversion":
colorview
,channelview
,rawview
,normedview
,PermutedDimsArray
,paddedviews
- Traits:
pixelspacing
,sdims
,timeaxis
,timedim
,spacedirections
Contrast/coloration:
clamp01
,clamp01nan
,scaleminmax
,colorsigned
,scalesigned
Algorithms:
- Reductions:
maxfinite
,maxabsfinite
,minfinite
,meanfinite
,sad
,ssd
,integral_image
,boxdiff
,gaussian_pyramid
- Resizing and spatial transformations:
restrict
,imresize
,warp
- Filtering:
imfilter
,imfilter!
,imfilter_LoG
,mapwindow
,imROF
,padarray
- Filtering kernels:
Kernel.
orKernelFactors.
, followed byando[345]
,guassian2d
,imaverage
,imdog
,imlaplacian
,prewitt
,sobel
- Exposure :
build_histogram
,adjust_histogram
,imadjustintensity
,imstretch
,imcomplement
,AdaptiveEqualization
,GammaCorrection
,cliphist
- Gradients:
backdiffx
,backdiffy
,forwarddiffx
,forwarddiffy
,imgradients
- Edge detection:
imedge
,imgradients
,thin_edges
,magnitude
,phase
,magnitudephase
,orientation
,canny
- Corner detection:
imcorner
,harris
,shi_tomasi
,kitchen_rosenfeld
,meancovs
,gammacovs
,fastcorners
- Blob detection:
blob_LoG
,findlocalmaxima
,findlocalminima
- Morphological operations:
dilate
,erode
,closing
,opening
,tophat
,bothat
,morphogradient
,morpholaplace
,feature_transform
,distance_transform
- Connected components:
label_components
,component_boxes
,component_lengths
,component_indices
,component_subscripts
,component_centroids
- Interpolation:
bilinear_interpolation
Test images and phantoms (see also TestImages.jl):
shepp_logan