import numpy as np
import matplotlib.pyplot as plt
Let's get started working tartans by generating vertical and horizontal stripes for the following pattern (see project page for details):
B14 K6 B6 K6 B6 K32 OG32
where the colors B, K, and OG are given by:
B : [52, 80, 100], K : [16, 16, 16], OG : [92, 100, 40]
What is the total width of this pattern?
total_width = 14 + 6 + 6 + 6 + 6 + 32 + 32
print(total_width)
Let's initialize an array that has shape (102, 102, 3)
.
vertical_stripes = np.zeros((102,102,3))
plt.imshow(vertical_stripes)
Right now, our array is a pure black picture. Let's add the first vertical stripe, which has color B = [52, 80, 100] and has width 14.
vertical_stripes[:, 0:14] = (52,80,100)
plt.imshow(vertical_stripes)
Note: vertical_stripes
is an array of floating point data, so RGB information should be between 0
and 1
. We can either change vertical_stripes
to be integer data and use integer RGB values (scaled between 0
and 255
), or rescale our RGB values to lie between 0
and 1
. For now, let's rescale to floats.
vertical_stripes[:, 0:14] = (52/255,80/255,100/255)
plt.imshow(vertical_stripes)
Exercise: Add the remaining stripes from our sample pattern to the vertical_stripes
array:
B14 K6 B6 K6 B6 K32 OG32
B : [52, 80, 100], K : [16, 16, 16], OG : [92, 100, 40]
vertical_stripes = np.ones((102,102,3))
# Add the first stripe:
vertical_stripes[:, 0:14] = (52/255,80/255,100/255)
# Add the second stripe:
vertical_stripes[:,14:20] = (16/255, 16/255, 16/255)
# Add the rest of the stripes:
vertical_stripes[:,20:26] = (52/255, 80/255, 100/255)
vertical_stripes[:,26:32] = (16/255, 16/255, 16/255)
vertical_stripes[:,32:38] = (52/255, 80/255, 100/255)
vertical_stripes[:,38:70] = (16/255, 16/255, 16/255)
vertical_stripes[:,70:102] = (92/255, 100/255, 40/255)
plt.imshow(vertical_stripes)
Comments:
For a 2-dimensional matrix, the transpose flips rows and columns. In Python, we can use the .T
method on a 2D array to get its transpose:
a = np.arange(25).reshape(5,5)
print(a)
print(a.T)
We would like to take the transpose of our vertical_stripes
array so that the columns of vertical_stripes
become the rows of horizontal_stripes
.
Problem: The vertical_stripes
array is a 3-dimensional array (rows, columns, color channels). What does vertical_stripes.T
give us in this case?
vertical_stripes.shape
vertical_stripes.T.shape
It turns out that the .T
method reverses the order of the axes. In this case, the color channel axis became the row axis, the column axis remained as the column axis, and the row axis became the color channel axis. For our purposes, this is not useful. Instead, we just want to swap the row and column axes.
We can use the np.transpose
function to do more targeted transposing:
#help(np.transpose)
When using np.transpose(vertical_stripes)
, we can optionally supply a keyword argument axes
. In particular, using axes = [1, 0, 2]
will give a transposed matrix where the old columns (axis 1
) become the new rows, the old rows (axis 0
) become the new columns, and the old color channel (axis 2
) remains as axis 2
.
horizontal_stripes = np.transpose(vertical_stripes, axes=[1,0,2])
plt.imshow(horizontal_stripes)
We now have vertical and horizontal stripes. Can we combine them to get a tartan pattern?
One way to combine them is by taking the average:
averaged_stripes = (vertical_stripes + horizontal_stripes) / 2
plt.imshow(averaged_stripes)
This gives flat colors rather than an interleaved combination. Can we instead generate a checkerboard pattern to interleave these stripes?
We want to go row by row, column by column, and alternatingly select a color from vertical_stripes
and horizontal_stripes
.
fig = plt.figure(figsize=(8,8))
checkerboard_tartan = np.zeros((102,102,3))
for i in range(102):
for j in range(102):
if (i + j) % 2 == 0:
checkerboard_tartan[i,j] = vertical_stripes[i,j]
else:
checkerboard_tartan[i,j] = horizontal_stripes[i,j]
plt.imshow(checkerboard_tartan)
Where to go from here:
vertical_stripes
.500
by 500
rows/columns.Let's go back to the beginning:
B14 K6 B6 K6 B6 K32 OG32
where the colors B, K, and OG are given by:
B : [52, 80, 100], K : [16, 16, 16], OG : [92, 100, 40]
Can we code this information as some sort of Python list?
B = (52/255, 80/255, 100/255)
K = (16/255, 16/255, 16/255)
OG = (92/255, 100/255, 40/255)
widths = [14, 6, 6, 6, 6, 32, 32]
RGBs = [B, K, B, K, B, K, OG]
What is the total width of this pattern? We can use the sum
function on widths
:
total_width = sum(widths)
print(total_width)
vertical_stripes = np.zeros((total_width, total_width, 3))
Now we want to iterate through each width/RGB pair and add the corresponding stripe:
stripe_start = 0
for width, RGB in zip(widths, RGBs):
vertical_stripes[:, stripe_start:stripe_start + width] = RGB
stripe_start = stripe_start + width
plt.imshow(vertical_stripes)
import numpy as np
import matplotlib.pyplot as plt
Last time, we looked at generating tartan patterns. We constructed tartans by using a checkerboard combination of horizontal and vertical stripes. For the project, we still need to use the more authentic combination. We also need to pad our tartan pattern out to fill a 500
by 500
image.
It would also be nice if we had an easier way of processing the "recipe" for our tartan in order get the widths and color information.
From the project page, we have the sample pattern recipe:
B14 K6 B6 K6 B6 K32 OG32 K6 OG32 K32 B32 K6 B6 K6 B32 K32 OG32 K6 OG32 K32 B6 K6 B6 K6 B28
Last time, we manually typed in the width and color information (or rather, a portion of the recpie) by hand:
B = (52/255, 80/255, 100/255)
K = (16/255, 16/255, 16/255)
OG = (92/255, 100/255, 40/255)
widths = [14, 6, 6, 6, 6, 32, 32]
RGBs = [B, K, B, K, B, K, OG]
Some useful tools for accomplishing this:
pattern_recipe = 'B14 K6 B6 K6 B6 K32 OG32 K6 OG32 K32 B32 K6 B6 K6 B32 K32 OG32 K6 OG32 K32 B6 K6 B6 K6 B28'
Note: our stripe information is separated by spaces. Each stripe is then described by some letters which indicate the color and some numbers which indicate the width.
It would be convenient if we could deal with the stripe information one stripe at a time. To do this, we can use the .split
method:
pattern_recipe.split()
This will separate the string into substrings that were separated by spaces. More generally, we can supply a string to the .split
method, and it will split based on the input string as a separator:
s = 'This is an example of a string.'
s
s.split()
s.split('e')
s.split('is')
In our case, splitting the tartan pattern string gives stripe information:
stripe_recipes = pattern_recipe.split()
Can we pull out the width information from a stripe recipe?
stripe_recipe = stripe_recipes[0]
stripe_recipe
int(stripe_recipe[-2:])
This works for this specific stripe, but other stripes may not have 2-digit widths.
We can use the .isalpha
and .isnumeric
methods to test whether a string contains alphabetic or numeric characters:
'a'.isalpha()
'3 is my favorite number'.isalpha()
'Three is my favorite number'.isalpha()
Note: spaces are not considered alphabetic characters.
'3'.isnumeric()
'3 is my favorite number'.isnumeric()
Back to our stripe recipe:
stripe_recipe
[c for c in stripe_recipe if c.isnumeric()]
The list comprehension above will generate a list of numeric digits in our stripe recipe. We can use the .join
method on an empty string to concatenate these digits:
''.join([c for c in stripe_recipe if c.isnumeric()])
Then convert this to an integer:
int(''.join([c for c in stripe_recipe if c.isnumeric()]))
Let's put this all together to generate the width information:
pattern_recipe = 'B14 K6 B6 K6 B6 K32 OG32 K6 OG32 K32 B32 K6 B6 K6 B32 K32 OG32 K6 OG32 K32 B6 K6 B6 K6 B28'
widths = []
stripe_recipes = pattern_recipe.split()
for stripe_recipe in stripe_recipes:
width = int(''.join([c for c in stripe_recipe if c.isnumeric()]))
widths.append(width)
widths[:10]
Exercise: Modify the code above to also pick out the color code information. You will need to use the .isalpha
method instead of the .isnumeric
method.
Once we're able to generate a list of color codes, we need to somehow translate them to the corresponding RGB values.
color_codes = ['B', 'K', 'B', 'K', 'B', 'K', 'OG']
This is a perfect use case for dictionaries. In the same way that lists are mapping between indices and values, dictionaries give a way to map keys to values. For dictionaries, keys can be lots of things (e.g. integers, strings, functions).
In our case, we want to map the strings 'B'
, 'K'
, and 'OG'
to their respective RGB tuples. To define a dictionary, we use curly braces {}
containing a comma separated list of <key>:<value>
mappings.
color_code_dict = {'B': (52/255, 80/255, 100/255),
'K': (16/255, 16/255, 16/255),
'OG': (92/255, 100/255, 40/255)}
We've already defined the variables B
, K
, and OG
, so we could use them here:
color_code_dict = {'B': B,
'K': K,
'OG': OG}
color_code_dict
Just like lists, we access values of a dictionary by plugging in keys with square brackets []
:
color_code_dict['B']
We can now iterate through our color codes and plug them into the dictionary to map the respective RGB values:
color_codes
RGBs = [color_code_dict[color_code] for color_code in color_codes]
RGBs
For now, I will consider a portion of the sample tartan shown in the project page:
B = (52/255, 80/255, 100/255)
K = (16/255, 16/255, 16/255)
OG = (92/255, 100/255, 40/255)
widths = [14, 6, 6, 6, 6, 32, 32]
RGBs = [B, K, B, K, B, K, OG]
total_width = sum(widths)
vertical_stripes = np.zeros((total_width, total_width, 3))
stripe_start = 0
for width, RGB in zip(widths, RGBs):
vertical_stripes[:, stripe_start:stripe_start + width] = RGB
stripe_start = stripe_start + width
horizontal_stripes = np.transpose(vertical_stripes,[1,0,2])
tartan = np.zeros((total_width, total_width, 3))
for i in range(total_width):
for j in range(total_width):
if (i + j) % 2 == 0:
tartan[i,j] = vertical_stripes[i,j]
else:
tartan[i,j] = horizontal_stripes[i,j]
plt.imshow(tartan)
We've generated a portion of the example tartan (with a checkerboard combination). Can we compare this to the example shown on the project page? First, let's download the sample image from the project page.
We can use plt.imread
to load in a PNG file as an array:
example_tartan = plt.imread('sample_checkerboard_tartan.png')
Note: when reading in with plt.imread
, we get RGBA data:
example_tartan.shape
We can just take a slice keep only the first three RGBA channels:
example_tartan = example_tartan[:,:,:3]
example_tartan.shape
How can we compare this to our tartan we've generated?
fig = plt.figure()
ax = plt.subplot(1,2,1)
plt.imshow(tartan)
plt.title('My tartan')
ax = plt.subplot(1,2,2)
plt.imshow(example_tartan)
plt.title('Example tartan')
Note: I've only used the first few stripes, so this comparison is not valid. For simplicity, let's just take a slice of example_tartan
that coincides with the stripes that I've used:
fig = plt.figure()
ax = plt.subplot(1,2,1)
plt.imshow(tartan)
plt.title('My tartan')
ax = plt.subplot(1,2,2)
plt.imshow(example_tartan[:total_width, :total_width])
plt.title('Example tartan')
Instead of making this comparison visually, can we numerically compare the two? We could subtract the two arrays and check how close they are to zero:
(tartan - example_tartan[:total_width, :total_width]).max()
We can see that the maximum difference between all row, column, color combinations is ~$10^{-9}$ (effectively 0).
Let's compare to the authentic tartan:
example_tartan = plt.imread('sample_tartan.png')[:,:,:3]
plt.imshow(example_tartan)
fig = plt.figure()
ax = plt.subplot(1,2,1)
plt.imshow(tartan)
plt.title('My tartan')
ax = plt.subplot(1,2,2)
plt.imshow(example_tartan[:total_width, :total_width])
plt.title('Example tartan')
(tartan - example_tartan[:total_width, total_width]).max()
We can also use plt.imshow
on this difference to see where the discrepencies crop up:
plt.imshow(np.abs(tartan - example_tartan[:total_width, total_width]).mean(axis=2),cmap='gray')
plt.colorbar()