About¶

This notebook builds the example for matrix multiplications in the book.

Note on Matrix Transpose¶

We will see .T (transpose) frequently because brushcue internally uses a column-major representation for matrices when applying transformations, while numpy's default matrix multiplication (@) assumes a row-major interpretation for vectors. This transpose ensures consistency between numpy's calculations and brushcue's image processing.

In [20]:
TEST_IMAGE_URL = 'https://www.brushcue.com/book/images/a-sunday-on-la-grande-jatte.jpg'
ctx = bc.Context()
EXAMPLE_COLOR = np.array([(162/255), (167/255), (101/255), 1]).T # this color needs work, probably should extract from the picture itself at a given point.

def matrix_for_step(step: int) -> np.array:
  if step == 1:
    return np.array([
        [1.0, 0.0, 0.0, 0.0],
        [0.0, 1.0, 0.0, 0.0],
        [0.0, 0.0, 0.7, 0.0],
        [0.0, 0.0, 0.0, 1.0]
    ]).T
  elif step == 2:
    return np.array([
        [0.7, 0.15, 0.15, 0.0],
        [0.15, 0.7, 0.15, 0.0],
        [0.15, 0.15, 0.7, 0.0],
        [0.0, 0.0, 0.0, 1.0]
    ]).T
  elif step == 3:
      return np.array([
        [1.5, 0.0, 0.0, 0.0],
        [0.0, 1.5, 0.0, 0.0],
        [0.0, 0.0, 1.5, 0.0],
        [0.0, 0.0, 0.0, 1.0]
    ]).T
  elif step == 4:
    return np.array([
        [0.0, 1.0, 0.0, 0.0],
        [0.0, 0.0, 1.0, 0.0],
        [1.0, 0.0, 0.0, 0.0],
        [0.0, 0.0, 0.0, 1.0]
    ]).T
  else:
    raise ValueError("Matrix not defined at step", step)

def download_test_image() -> str:
  temp_dir = '/tmp'
  os.makedirs(temp_dir, exist_ok=True)
  file_name = TEST_IMAGE_URL.split('/')[-1]
  temp_file_path = os.path.join(temp_dir, file_name)

  if os.path.exists(temp_file_path):
    return temp_file_path
  else:
    response = requests.get(TEST_IMAGE_URL, stream=True)
    response.raise_for_status()  # Raise an HTTPError for bad responses (4xx or 5xx)
    with open(temp_file_path, 'wb') as f:
      for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)
    return temp_file_path

def path_for_end_of_step_image(step_number):
  if step_number == 0:
    return download_test_image()
  return f'/tmp/step_{step_number}.png'

def path_for_color_image(step_number):
  return f'/tmp/color_{step_number}.png'

def apply_matrix_to_image(image_path: str, matrix: np.array, output_path: str):
  image = bc.load_composition(image_path)
  image = bc.composition_linear_transform(
      image,
      matrix[0, 0], matrix[0, 1], matrix[0, 2], matrix[0, 3],
      matrix[1, 0], matrix[1, 1], matrix[1, 2], matrix[1, 3],
      matrix[2, 0], matrix[2, 1], matrix[2, 2], matrix[2, 3],
      matrix[3, 0], matrix[3, 1], matrix[3, 2], matrix[3, 3]
  )
  result = image.execute(ctx)
  output_bytes = result.as_composition().to_image_bytes(ctx)
  with open(output_path, "wb") as f:
      f.write(bytes(output_bytes))

def color_at_end_of_step(step: int) -> np.array:
  if step == 0:
    return EXAMPLE_COLOR
  previous_color = color_at_end_of_step(step - 1)
  return matrix_for_step(step).T @ previous_color


def make_color_rect_image(step: int) -> str:
  color = color_at_end_of_step(step)
  color = bc.r_g_b_a_color_constant(color[0], color[1], color[2], color[3])
  color_profile = bc.color_profile_s_r_g_b()
  size = bc.vector_2i_constant(300, 300)
  color_rect = bc.composition_color_rect(color, color_profile, size)
  result = color_rect.execute(ctx)
  output_path = path_for_color_image(step)
  output_bytes = result.as_composition().to_image_bytes(ctx)
  with open(output_path, "wb") as f:
      f.write(bytes(output_bytes))
  return output_path

def image_at_end_of_step(step: int) -> str:
  output_file_path = path_for_end_of_step_image(step)
  if os.path.exists(output_file_path):
    return output_file_path
  previous_image = image_at_end_of_step(step - 1)
  apply_matrix_to_image(previous_image, matrix_for_step(step), output_file_path)
  return output_file_path
In [21]:
from IPython.display import Image, Markdown, display
from PIL import Image as PILImage
import io

def display_scaled(path: str, width: int = 500):
  with PILImage.open(path) as img:
    h = int(img.height * width / img.width)
    resized = img.resize((width, h), PILImage.LANCZOS)
    buf = io.BytesIO()
    resized.save(buf, format="PNG", optimize=True)
    display(Image(data=buf.getvalue(), width=width))

def display_step(step: int):
  display(Markdown(f"### Step {step}"))
  # Print the matrix once at the top for each step
  display(Markdown("#### Transformation Matrix"))
  print(matrix_for_step(step).T)

  # Before state
  image_path_before = image_at_end_of_step(step - 1)
  color_values_before = color_at_end_of_step(step - 1)
  color_path_before = make_color_rect_image(step - 1)
  display(Markdown("#### Before Transformation"))
  display_scaled(image_path_before, width=500)
  print(f"Image path before: {image_path_before}")
  display_scaled(color_path_before, width=500)
  print(f"Color RGB values before: {color_values_before}")

  # After state
  image_path_after = image_at_end_of_step(step)
  color_values_after = color_at_end_of_step(step)
  color_path_after = make_color_rect_image(step)
  display(Markdown("#### After Transformation"))
  display_scaled(image_path_after, width=500)
  print(f"Image path after: {image_path_after}")
  display_scaled(color_path_after, width=500)
  print(f"Color RGB values after: {color_values_after}")
In [22]:
display_step(1)

Step 1¶

Transformation Matrix¶

[[1.  0.  0.  0. ]
 [0.  1.  0.  0. ]
 [0.  0.  0.7 0. ]
 [0.  0.  0.  1. ]]

Before Transformation¶

No description has been provided for this image
Image path before: /tmp/a-sunday-on-la-grande-jatte.jpg
No description has been provided for this image
Color RGB values before: [0.63529412 0.65490196 0.39607843 1.        ]

After Transformation¶

No description has been provided for this image
Image path after: /tmp/step_1.png
No description has been provided for this image
Color RGB values after: [0.63529412 0.65490196 0.2772549  1.        ]
In [23]:
display_step(2)

Step 2¶

Transformation Matrix¶

[[0.7  0.15 0.15 0.  ]
 [0.15 0.7  0.15 0.  ]
 [0.15 0.15 0.7  0.  ]
 [0.   0.   0.   1.  ]]
[image_recipe::TextureCache] reusing tiled texture requested_hash=952501458716392912 cache_key=952501458716392912

Before Transformation¶

No description has been provided for this image
Image path before: /tmp/step_1.png
No description has been provided for this image
Color RGB values before: [0.63529412 0.65490196 0.2772549  1.        ]

After Transformation¶

No description has been provided for this image
Image path after: /tmp/step_2.png
No description has been provided for this image
Color RGB values after: [0.58452941 0.59531373 0.38760784 1.        ]
In [24]:
display_step(3)

Step 3¶

Transformation Matrix¶

[[1.5 0.  0.  0. ]
 [0.  1.5 0.  0. ]
 [0.  0.  1.5 0. ]
 [0.  0.  0.  1. ]]
[image_recipe::TextureCache] reusing tiled texture requested_hash=14315100108546816044 cache_key=14315100108546816044

Before Transformation¶

No description has been provided for this image
Image path before: /tmp/step_2.png
No description has been provided for this image
Color RGB values before: [0.58452941 0.59531373 0.38760784 1.        ]

After Transformation¶

No description has been provided for this image
Image path after: /tmp/step_3.png
No description has been provided for this image
Color RGB values after: [0.87679412 0.89297059 0.58141176 1.        ]
In [25]:
display_step(4)

Step 4¶

Transformation Matrix¶

[[0. 1. 0. 0.]
 [0. 0. 1. 0.]
 [1. 0. 0. 0.]
 [0. 0. 0. 1.]]
[image_recipe::TextureCache] reusing tiled texture requested_hash=10901882859637130592 cache_key=10901882859637130592

Before Transformation¶

No description has been provided for this image
Image path before: /tmp/step_3.png
No description has been provided for this image
Color RGB values before: [0.87679412 0.89297059 0.58141176 1.        ]

After Transformation¶

No description has been provided for this image
Image path after: /tmp/step_4.png
No description has been provided for this image
Color RGB values after: [0.89297059 0.58141176 0.87679412 1.        ]
In [26]:
full_transform = matrix_for_step(4).T @ matrix_for_step(3).T @ matrix_for_step(2).T @ matrix_for_step(1).T
full_transform
Out[26]:
array([[0.225 , 1.05  , 0.1575, 0.    ],
       [0.225 , 0.225 , 0.735 , 0.    ],
       [1.05  , 0.225 , 0.1575, 0.    ],
       [0.    , 0.    , 0.    , 1.    ]])
In [27]:
full_transform @ EXAMPLE_COLOR
Out[27]:
array([0.89297059, 0.58141176, 0.87679412, 1.        ])

Final Transformation¶

In [28]:
final_image_path = '/tmp/final_transformed_image.png'
initial_image_path = path_for_end_of_step_image(0)
apply_matrix_to_image(initial_image_path, full_transform.T, final_image_path)
display_scaled(final_image_path, width=500)
print(f"Final transformed image saved to: {final_image_path}")
new pipeline: color_transformer_shader_rgba16float_compute_74330651998959330_inarr_false
No description has been provided for this image
Final transformed image saved to: /tmp/final_transformed_image.png