<jupyter_start><jupyter_text>Day 8 - building a simple image* https://adventofcode.com/2019/day/8This is mostly an 'iterate over data in chunks' task; the image is 25 x 6, we are given the data in layers, so we want to process the data in 25 x 6 == 150 integers at a time.For part 1, we then put each layer through a `Counter` so we can select the layer with the lowest zero count, and then take the *1* and *2* counts to multiply them.For part 2, we can then iterate over the layers to combine them into a merged image and display the data in rows of 25, another chunked iteration.<jupyter_code>import aocd
data = list(map(int, aocd.get_data(day=8, year=2019)))
from typing import Iterable, Sequence, TypeVar

T = TypeVar("T")

W, H = 25, 6

def per_n(s: Sequence[T], n: int) -> Iterable[Sequence[T]]:
    return (s[i : i + n] for i in range(0, len(s), n))
from collections import Counter
layercounts = [Counter(layer) for layer in per_n(data, W * H)]
least0 = min(layercounts, key=lambda c: c[0])
print("Part 1:", least0[1] * least0[2])
# construct image; 2 means we ignore the pixel and look at the next layer value
merged = [2] * W * H
for layer in per_n(data, W * H):
    merged = [a if a != 2 else b for a, b in zip(merged, layer)]
from PIL import Image

def render_image(merged: Sequence, scale: int = 10, border: bool = True) -> Image:
    img = Image.new('1', (W, H), None)
    img.putdata(merged)
    if border:
        with_border = Image.new('1', (W + 2, H + 2))
        with_border.paste(img, (1, 1))
        img = with_border
    if scale != 1:
        img = img.resize((img.width * scale, img.height * scale))
    return img

print("Part 2:")
render_image(merged)
%matplotlib inline
from typing import Optional, Tuple
from matplotlib import animation, rc
from matplotlib.cm import get_cmap
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import numpy as np

rc('animation', html='html5')


def animate_layers(data: Sequence[int]) -> animation.FuncAnimation:
    # frames are determined by the layers, in reverse order (bottom layer first)
    frames: Sequence[Optional[Sequence[int]]] = [
        *(list(per_n(data, W * H))[::-1]),
        *([None] * 64)  # full fade for remainder
    ]

    fig, ax = plt.subplots(figsize=(12, 12 * (H / W)), facecolor='black', frameon=True)
    fig.subplots_adjust(left=0, bottom=0, right=1, top=1)
    ax.set_axis_off()
    
    grid = np.zeros((H + 2, W + 2), dtype=np.int8)
    image = ax.imshow(grid, vmin=0, vmax=1, cmap=get_cmap('binary').reversed())
    
    def render(layer: Optional[Sequence[int]]) -> Tuple[Figure]:
        if layer is not None:
            a = image.get_array()
            layerdata = np.array(layer).reshape(H, W)
            non_transparent = layerdata < 2
            a[1:-1, 1:-1][non_transparent] = layerdata[non_transparent]
        return image,

    anim = animation.FuncAnimation(
        fig, render, frames, interval=60, blit=True,
        repeat_delay=1000
    )
    plt.close(fig)
    return anim


animate_layers(data)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12434725848563968, 'fraction_numerical': 0.025456919060052218, 'mean_word_length': 3.4809941520467835, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 9, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '1', 'dir': 'jupyter-structured-clean-dedup', 'id': '515164', 'license': 'MIT', 'n_tokens_mistral': 1082, 'n_tokens_neox': 1012, 'n_words': 357, 'path': '2019/Day 08.ipynb', 'repo_name': 'AwesomeGitHubRepos/adventofcode', 'size': '3064'}