Image tilings and arrays

In this notebook I decided to get more familiar with Julia basic functions, arrays, and operations by giving myself a small computational photography challenge! And on a suggestion, I played with the Interact.jl package too.

I’ve included the notebook I used for my image tiling manipulation, ending with playing with the Interact.jl library.

My notebook

In this notebook I want to take an image, break it into regions, and tile it with the maximum color from that region. It is an experiment in computational photography!

In [1]:
using Colors, Images, TestImages;


The one thing I find hard to get used to with Julia is that I don’t need to explicitly call functions on module names, like in Python. Just a small detail, but I’ll get used to it.

In [2]:
img = testimage("lighthouse");


Here’s some documentation about Arrays, which are what images are represented as: http://docs.julialang.org/en/stable/manual/arrays/

In [3]:
# find the size of the image
println(size(img))

Out[3]:
(512,768)

In [4]:
summary(img)

Out[4]:
"512×768 Array{RGB{N0f8},2}"
In [5]:
# create a random dense array the same size as the image
# it gets initialized with random numbers, which makes a really cool pattern!
tiled_img = Array{RGB{N0f8}, 2}(size(img))

Out[5]:

Another way you can initialize an array like another:

In [6]:
tiled_img = similar(img);


Like in IPython, you can add a “?” at the end of a function. In Julia, you put the question mark at the start of a function.

In [7]:
?linspace

Out[7]:
search:

Iterating and Generating¶

There are many ways of generating a range of integers (that I will use to index into the image)

In [8]:
linspace(1, size(img, 1), 8)

Out[8]:
8-element LinSpace{Float64}:
1.0,74.0,147.0,220.0,293.0,366.0,439.0,512.0
In [9]:
Int16.(linspace(1, size(img, 1), 8))

Out[9]:
8-element Array{Int16,1}:
1
74
147
220
293
366
439
512
In [10]:
Int16[i for i in linspace(1, size(img, 1), 8)]

Out[10]:
8-element Array{Int16,1}:
1
74
147
220
293
366
439
512

You can generate x, y tile indices with an interator-like thing or a generator-like thing.

In [11]:
# or to directly generate ints
1:8:size(img, 1)

Out[11]:
1:8:505
In [12]:
typeof(1:8:size(img, 1))

Out[12]:
StepRange{Int64,Int64}

And the visualizations for strips of images is awesome!

In [13]:
img[1:8:size(img, 1)]

Out[13]:

If you use these generators to index in both directions, what it basically does is tile the image and downsample.

In [14]:
stepsize = 16;
img[1:stepsize:size(img, 1), 1:stepsize:size(img, 2)]

Out[14]:
In [15]:
maximum(green.(img[1:10]))

Out[15]:
0.518N0f8
In [16]:
maximum(green.(img[1:16, 1:16]))

Out[16]:
0.533N0f8

And now let’s put it all together to take the max R, G, and B values in a block.

In [17]:
for x in 1:stepsize:size(img, 1)
for y in 1:stepsize:size(img, 2)
x_end = min(x + stepsize, size(img, 1));
y_end = min(y + stepsize, size(img, 2));
# the @view takes a view into the image rather than making a copy
imgv = @view img[x:x_end, y:y_end]
tiled_img[x:x_end, y:y_end] = RGB{N0f8}(
maximum(red.(imgv)), maximum(green.(imgv)), maximum(blue.(imgv)));
end
end


And we can concatenate the images to put them size by side.

In [18]:
[img tiled_img]

Out[18]:

I can even put all this into a function to call separately. Small notes about functions:

• Arguments can either be positional or keyword, but not both. The function below means I have to call stepsize= every time I want to pass stepsize.
• The last statement is the object returned by the function.
In [19]:
function max_tiling(img; stepsize=16)
tiled_img = similar(img);
for x in 1:stepsize:size(img, 1)
for y in 1:stepsize:size(img, 2)
x_end = min(x + stepsize, size(img, 1));
y_end = min(y + stepsize, size(img, 2));
imgv = @view img[x:x_end, y:y_end];
tiled_img[x:x_end, y:y_end] = RGB{N0f8}(
maximum(red.(imgv)), maximum(green.(imgv)), maximum(blue.(imgv)));
end
end
tiled_img
end

Out[19]:
max_tiling (generic function with 1 method)
In [20]:
[img max_tiling(img, stepsize=16)]

Out[20]:
In [21]:
[img max_tiling(img, stepsize=8)]

Out[21]:

I am told that the interact.jl package (https://github.com/JuliaGizmos/Interact.jl) can let me make a slider to change the stepsize!

In [22]:
for x in 1:stepsize:size(img, 1)
for y in 1:stepsize:size(img, 2)
x_end = min(x + stepsize, size(img, 1));
y_end = min(y + stepsize, size(img, 2));
imgv = @view img[x:x_end, y:y_end]
tiled_img[x:x_end, y:y_end] = RGB{N0f8}(maximum(red.(imgv)), maximum(green.(imgv)), maximum(blue.(imgv)));
end
end

In [23]:
# Pkg.add("Interact")

In [24]:
using Interact;

Out[24]:

The widget lets me manipulate the step size and re-generate images directly in the notebook! Highly recommend this package =)

In [25]:
@manipulate for s=[2^x for x in 0:6]
[img max_tiling(img, stepsize=s)]
end

Out[25]:

The widget in the last output of the notebook did not render properly, so I’m including a screenshot here:

Random memory initialization

In line 4 (In[4]:), an uninitialized array takes random memory and random values. I don’t fully understand where this memory comes from, but I’m pretty sure it has to do with most-recently accessed memory. When I was running on my local machine, line 2 (In[2]:) loaded the original image of the lighthouse we saw in my first post. So when line 4 (creating a random uninitialized array) was executed, I got this pretty lighthouse artifact! You can see it is different image than came up in the second run of the notebook rendered above!

Jupyter notebook rendering aside

As another aside, in rendering the Jupyter notebook, the Interact library created widgets that generated custom Javascript during the nbconvert phase. I just removed that entire script section from the generated HTML, which is why I had to include a screenshot of the final widget at the end. I’m sorry for that ugliness – Jupyter and nbconvert are fantastic projects, they just can’t possibly cover all use cases!