home

2d Convolution

Creator: Petar Veličković (original)

A two-dimensional convolution operator slides the kernel matrix across the target image and records elementwise products. Makes heavy use of the matrix environment in TikZ.


2d Convolution

Download

PNG PDF SVG

Code

2d-convolution.typ (137 lines)

#import "@preview/cetz:0.5.2": canvas, draw
#import draw: content, line, on-layer, rect

#set page(width: auto, height: auto, margin: 5pt, fill: none)
#set text(size: 12pt)

#canvas({
  let cell-size = 1.0
  let matrix-sep = 1.5
  let highlight = rgb(255, 200, 150) // orange!30
  let kernel-color = rgb("#9ae7e1") // teal!30
  let result-color = rgb(200, 200, 255) // blue!30

  let draw-matrix(origin, values, name, highlighted: false) = {
    for (row_idx, row) in values.enumerate() {
      for (col_idx, value) in row.enumerate() {
        let (coord_x, coord_y) = (
          origin.at(0) + col_idx * cell-size,
          origin.at(1) - row_idx * cell-size,
        )
        rect(
          (coord_x, coord_y),
          (coord_x + cell-size, coord_y + cell-size),
          fill: if highlighted and row_idx < 3 and 3 <= col_idx and col_idx <= 5 { highlight },
          name: name + "-" + str(row_idx) + "-" + str(col_idx),
          stroke: .5pt,
        )
        content((coord_x + cell-size / 2, coord_y + cell-size / 2), $#value$)
      }
    }
  }

  let cell-anchor(matrix-name, ii, jj, anchor) = (
    matrix-name + "-" + str(ii) + "-" + str(jj) + "." + anchor
  )

  let input-origin = (0, 4)
  let matrix-label-y = input-origin.at(1) - 6 * cell-size - 0.55
  let input-values = (
    (0, 1, 1, 1, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0),
    (0, 0, 0, 1, 1, 1, 0),
    (0, 0, 0, 1, 1, 0, 0),
    (0, 0, 1, 1, 0, 0, 0),
    (0, 1, 1, 0, 0, 0, 0),
    (1, 1, 0, 0, 0, 0, 0),
  )
  draw-matrix(input-origin, input-values, "I", highlighted: true)
  content(
    (input-origin.at(0) + 7 * cell-size / 2, matrix-label-y),
    $bold(I)$,
    name: "I-label",
  )

  content((rel: (1, 0), to: "I-3-6"), text(size: 18pt)[$*$], name: "times")

  let kernel-origin = (
    input-origin.at(0) + 7 * cell-size + matrix-sep,
    input-origin.at(1) - 2 * cell-size,
  )
  let kernel-values = ((1, 0, 1), (0, 1, 0), (1, 0, 1))
  draw-matrix(kernel-origin, kernel-values, "K")
  // Fill kernel matrix background
  rect(
    cell-anchor("K", 0, 0, "north-west"),
    cell-anchor("K", 2, 2, "south-east"),
    fill: kernel-color,
    stroke: none,
  )
  // Redraw matrix on top of background
  draw-matrix(kernel-origin, kernel-values, "K")
  content(
    (kernel-origin.at(0) + 3 * cell-size / 2, matrix-label-y),
    $bold(K)$,
    name: "K-label",
  )

  content((rel: (1, 0), to: "K-1-2"), text(size: 18pt)[$=$], name: "equals")

  let result-origin = (
    kernel-origin.at(0) + 3 * cell-size + matrix-sep,
    input-origin.at(1) - cell-size,
  )
  let result-values = (
    (1, 4, 3, 4, 1),
    (1, 2, 4, 3, 3),
    (1, 2, 3, 4, 1),
    (1, 3, 3, 1, 1),
    (3, 3, 1, 1, 0),
  )
  draw-matrix(result-origin, result-values, "R")
  on-layer(-1, rect(
    cell-anchor("R", 0, 3, "north-west"),
    cell-anchor("R", 0, 3, "south-east"),
    fill: result-color,
    stroke: none,
  ))
  content(
    (result-origin.at(0) + 5 * cell-size / 2, matrix-label-y),
    $bold(I * K)$,
    name: "R-label",
  )

  // dashed guides tie the input patch to the kernel, then the kernel to the cell it produces
  for (paint, ends) in (
    (
      rgb(150, 220, 200),
      (
        (("I", 0, 5, "north-east"), ("K", 0, 0, "north-west")),
        (("I", 2, 5, "south-east"), ("K", 2, 0, "south-west")),
      ),
    ),
    (
      rgb(150, 150, 220),
      (
        (("K", 0, 2, "north-east"), ("R", 0, 3, "north-west")),
        (("K", 2, 2, "south-east"), ("R", 0, 3, "south-west")),
      ),
    ),
  ) {
    for (from, to) in ends {
      line(cell-anchor(..from), cell-anchor(..to), stroke: (dash: "dashed", paint: paint))
    }
  }

  for ii in range(3) {
    for jj in (3, 4, 5) {
      content(
        cell-anchor("I", ii, jj, "south-west"),
        text(size: 12pt)[×#calc.rem(ii + jj, 2)],
        anchor: "south-west",
        padding: 1pt,
      )
    }
  }
})