Reputation: 1
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
Upvotes: 0
Views: 153
Reputation: 168966
It's honestly a bit hard to tell what you're really looking for, but here's some code that:
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.)
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)
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()
Upvotes: 1