0

i have a very large array which i need to iterate over. The array is a large tiff image and instead of a color value i want to add a 4-4 bit 2d pattern. My code looks like this right now. It needs very long to finish. tiff is a array(x,y,4). x and y are very large. Values is a list with patterns who match the value i am searching for and giving me the index. Patterns is a array of 4-4 Patterns. Thanks

for iy, ix in np.ndindex(tiff[:, :, 0].shape):
    tiff[iy, ix, 0] = np.random.choice(np.argwhere(Values == tiff[iy, ix, 0])[:, 0], 1, replace=False)
    tiff[iy, ix, 1] = np.random.choice(np.argwhere(Values == tiff[iy, ix, 1])[:, 0], 1, replace=False)
    tiff[iy, ix, 2] = np.random.choice(np.argwhere(Values == tiff[iy, ix, 2])[:, 0], 1, replace=False)
    tiff[iy, ix, 3] = np.random.choice(np.argwhere(Values == tiff[iy, ix, 3])[:, 0], 1, replace=False)
    Rippedimage[iy * 8 : (iy + 1) * 8 - 4, ix * 8 : (ix + 1) * 8 - 4] = Array_Pattern_4_4[tiff[iy, ix, 0]]
    Rippedimage[iy * 8 : (iy + 1) * 8 - 4, ix * 8 + 4 : (ix + 1) * 8] = Array_Pattern_4_4[tiff[iy, ix, 1]]
    Rippedimage[iy * 8 + 4 : (iy + 1) * 8, ix * 8 : (ix + 1) * 8 - 4] = Array_Pattern_4_4[tiff[iy, ix, 2]]
    Rippedimage[iy * 8 + 4 : (iy + 1) * 8, ix * 8 + 4 : (ix + 1) * 8] = Array_Pattern_4_4[tiff[iy, ix, 3]]

left is before, right how it should look like after

9
  • Instead of saying "I need to iterate over this array", please tell us what your end goal is? What is this pattern? Why is the TIFF 4-channel? Commented Jan 17, 2022 at 7:09
  • Thanks, i added a picture which shows what my intention is. The tiff is split in 4channels, because i want a 8-8 pattern which is split in 4 4-4 subpatterns. Commented Jan 17, 2022 at 7:15
  • So is this some sort of dithering thing for a grayscale image? Sounds like you might have a better time just copying the dither version into a new array? Commented Jan 17, 2022 at 7:18
  • 2
    replace is a bit moot if your size is 1, no? Commented Jan 17, 2022 at 7:47
  • 1
    What is Values? What is tiff exactly? What types of numbers do they hold? My ability to help you vectorize this hinges on tidbits like this. Commented Jan 17, 2022 at 7:50

1 Answer 1

1

It's honestly a bit hard to tell what you're really looking for, but here's some code that:

  • generates a variety of NxN random dither patterns for each grayscale shade (assuming 8-bit images)
  • chooses a random pattern per NxN pixels in the original image to generate a dithered version

On my Macbook, dithering an 920x920 image takes about 17 milliseconds:

image generation 4.377
pattern generation 6.06
dither generation 16.915
import time
from contextlib import contextmanager

import numpy as np
from PIL import Image


def generate_patterns(
    *,
    pattern_size: int = 8,
    pattern_options_per_shade: int = 8,
    shades: int = 256,
):
    patterns = []
    for shade in range(shades):
        shade_patterns = [
            np.random.random((pattern_size, pattern_size)) < (shade / shades)
            for i in range(pattern_options_per_shade)
        ]
        patterns.append(shade_patterns)
    return np.array(patterns)


def dither(image, patterns):
    (
        shades,
        pattern_options_per_shade,
        pattern_width,
        pattern_height,
    ) = patterns.shape
    assert shades == 256  # TODO

    # image sampled at pattern_sizes
    resampled = (
        image[::pattern_width, ::pattern_height].round().astype(np.uint8)
    )
    # mask of pattern option per pattern_size block
    pat_mask = np.random.randint(
        0, pattern_options_per_shade, size=resampled.shape
    )

    dithered = np.zeros_like(image)
    for (iy, ix), c in np.ndenumerate(resampled):
        pattern = patterns[c, pat_mask[iy, ix]]
        dithered[
            iy * pattern_height : (iy + 1) * pattern_height,
            ix * pattern_width : (ix + 1) * pattern_width,
        ] = pattern

    return dithered * 255


@contextmanager
def stopwatch(title):
    t0 = time.perf_counter()
    yield
    t1 = time.perf_counter()
    print(title, round((t1 - t0) * 1000, 3))


def main():
    with stopwatch("image generation"):
        img_size = 920
        image = (
            np.linspace(0, 255, img_size)
            .repeat(img_size)
            .reshape((img_size, img_size))
        )
        image[200:280, 200:280] = 0

    with stopwatch("pattern generation"):
        patterns = generate_patterns()

    with stopwatch("dither generation"):
        dithered = dither(image, patterns)

    import matplotlib.pyplot as plt

    plt.figure(dpi=450)
    plt.imshow(dithered, interpolation="none")
    plt.show()


if __name__ == "__main__":
    main()

The output image looks like (e.g.)

enter image description here

EDIT

A version that upscales the source image to the dithered version:

image generation 3.886
pattern generation 5.581
dither generation 1361.194
def dither_embiggen(image, patterns):
    shades, pattern_options_per_shade, pattern_width, pattern_height = patterns.shape
    assert shades == 256  # TODO

    # mask of pattern option per source pixel
    pat_mask = np.random.randint(0, pattern_options_per_shade, size=image.shape)

    dithered = np.zeros((image.shape[0] * pattern_height, image.shape[1] * pattern_width))
    for (iy, ix), c in np.ndenumerate(image.round().astype(np.uint8)):
        pattern = patterns[c, pat_mask[iy, ix]]
        dithered[iy * pattern_height:(iy + 1) * pattern_height, ix * pattern_width:(ix + 1) * pattern_width] = pattern

    return (dithered * 255)

EDIT 2

This version directly writes the dithered lines to disk as a raw binary file. The reader is expected to know how many pixels there are per line. Based on a little empirical testing this seems to do the trick...

import time
from contextlib import contextmanager

import numpy as np


def generate_patterns(
    *,
    pattern_size: int = 8,
    pattern_options_per_shade: int = 16,
    shades: int = 256,
):
    patterns = []
    for shade in range(shades):
        shade_patterns = [
            np.packbits(
                np.random.random((pattern_size, pattern_size))
                < (shade / shades),
                axis=0,
            )[0]
            for i in range(pattern_options_per_shade)
        ]
        patterns.append(shade_patterns)
    return np.array(patterns)


def dither_to_disk(bio, image, patterns):
    assert image.dtype == np.uint8
    shades, pattern_options_per_shade, pattern_height = patterns.shape
    pat_mask = np.random.randint(0, pattern_options_per_shade, size=image.shape)
    for y in range(image.shape[0]):
        patterns[image[y, :], pat_mask[y, :]].tofile(bio)


@contextmanager
def stopwatch(title):
    t0 = time.perf_counter()
    yield
    t1 = time.perf_counter()
    print(title, round((t1 - t0) * 1000, 3))


def main():
    with stopwatch("image generation"):
        img_width = 25_000
        img_height = 5_000
        image = (
            np.linspace(0, 255, img_height)
            .repeat(img_width)
            .reshape((img_height, img_width))
        )
        image[200:280, 200:280] = 0
        image = image.round().astype(np.uint8)

    with stopwatch("pattern generation"):
        patterns = generate_patterns()

    with stopwatch(f"dither_to_disk {image.shape}"):
        with open("x.bin", "wb") as f:
            dither_to_disk(f, image, patterns)


if __name__ == "__main__":
    main()
Sign up to request clarification or add additional context in comments.

9 Comments

Thank you very much, it is nearly what i want. problem is you downsample the picture first and then upcale it with the pattern size. I dont want to downsample it first. the image i want to work with have pixel sizes of 25.000 * 25.000 and after putting the pattern on 200.000*200.000. So if my calculations are right, your script will need 15min for that size.
So the result should be NxN the dimensions of the original image?
(I added a dither_embiggen() version that doesn't downsample the image.)
By the way, a 200,000 x 200,000 byte array will consume 40 gigabytes of memory. Are you sure you'll be able to work with that?
artwork.com/raster/bigtiff.htm describes relative good what i want to do and why i need this size. My beam is round about 6um big. Right now we multiply the tiff with 255 to have it easy and its hard via python to change tiff meta and tell windows it a 1 bit file, the final tiff is 200.000*200.000 bit big, because we dont need a greyscale.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.