|
from __future__ import print_function, division |
|
|
|
""" |
|
COUNTLESS performance test in Python. |
|
|
|
python countless2d.py ./images/NAMEOFIMAGE |
|
""" |
|
|
|
import six |
|
from six.moves import range |
|
from collections import defaultdict |
|
from functools import reduce |
|
import operator |
|
import io |
|
import os |
|
from PIL import Image |
|
import math |
|
import numpy as np |
|
import random |
|
import sys |
|
import time |
|
from tqdm import tqdm |
|
from scipy import ndimage |
|
|
|
def simplest_countless(data): |
|
""" |
|
Vectorized implementation of downsampling a 2D |
|
image by 2 on each side using the COUNTLESS algorithm. |
|
|
|
data is a 2D numpy array with even dimensions. |
|
""" |
|
sections = [] |
|
|
|
|
|
|
|
|
|
factor = (2,2) |
|
for offset in np.ndindex(factor): |
|
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
sections.append(part) |
|
|
|
a, b, c, d = sections |
|
|
|
ab = a * (a == b) |
|
ac = a * (a == c) |
|
bc = b * (b == c) |
|
|
|
a = ab | ac | bc |
|
|
|
return a + (a == 0) * d |
|
|
|
def quick_countless(data): |
|
""" |
|
Vectorized implementation of downsampling a 2D |
|
image by 2 on each side using the COUNTLESS algorithm. |
|
|
|
data is a 2D numpy array with even dimensions. |
|
""" |
|
sections = [] |
|
|
|
|
|
|
|
|
|
factor = (2,2) |
|
for offset in np.ndindex(factor): |
|
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
sections.append(part) |
|
|
|
a, b, c, d = sections |
|
|
|
ab_ac = a * ((a == b) | (a == c)) |
|
bc = b * (b == c) |
|
|
|
a = ab_ac | bc |
|
return a + (a == 0) * d |
|
|
|
def quickest_countless(data): |
|
""" |
|
Vectorized implementation of downsampling a 2D |
|
image by 2 on each side using the COUNTLESS algorithm. |
|
|
|
data is a 2D numpy array with even dimensions. |
|
""" |
|
sections = [] |
|
|
|
|
|
|
|
|
|
factor = (2,2) |
|
for offset in np.ndindex(factor): |
|
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
sections.append(part) |
|
|
|
a, b, c, d = sections |
|
|
|
ab_ac = a * ((a == b) | (a == c)) |
|
ab_ac |= b * (b == c) |
|
return ab_ac + (ab_ac == 0) * d |
|
|
|
def quick_countless_xor(data): |
|
""" |
|
Vectorized implementation of downsampling a 2D |
|
image by 2 on each side using the COUNTLESS algorithm. |
|
|
|
data is a 2D numpy array with even dimensions. |
|
""" |
|
sections = [] |
|
|
|
|
|
|
|
|
|
factor = (2,2) |
|
for offset in np.ndindex(factor): |
|
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
sections.append(part) |
|
|
|
a, b, c, d = sections |
|
|
|
ab = a ^ (a ^ b) |
|
ab += (ab != a) * ((ab ^ (ab ^ c)) - b) |
|
ab += (ab == c) * ((ab ^ (ab ^ d)) - c) |
|
return ab |
|
|
|
def stippled_countless(data): |
|
""" |
|
Vectorized implementation of downsampling a 2D |
|
image by 2 on each side using the COUNTLESS algorithm |
|
that treats zero as "background" and inflates lone |
|
pixels. |
|
|
|
data is a 2D numpy array with even dimensions. |
|
""" |
|
sections = [] |
|
|
|
|
|
|
|
|
|
factor = (2,2) |
|
for offset in np.ndindex(factor): |
|
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
sections.append(part) |
|
|
|
a, b, c, d = sections |
|
|
|
ab_ac = a * ((a == b) | (a == c)) |
|
ab_ac |= b * (b == c) |
|
|
|
nonzero = a + (a == 0) * (b + (b == 0) * c) |
|
return ab_ac + (ab_ac == 0) * (d + (d == 0) * nonzero) |
|
|
|
def zero_corrected_countless(data): |
|
""" |
|
Vectorized implementation of downsampling a 2D |
|
image by 2 on each side using the COUNTLESS algorithm. |
|
|
|
data is a 2D numpy array with even dimensions. |
|
""" |
|
|
|
|
|
data, upgraded = upgrade_type(data) |
|
|
|
|
|
|
|
data += 1 |
|
|
|
sections = [] |
|
|
|
|
|
|
|
|
|
factor = (2,2) |
|
for offset in np.ndindex(factor): |
|
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
sections.append(part) |
|
|
|
a, b, c, d = sections |
|
|
|
ab = a * (a == b) |
|
ac = a * (a == c) |
|
bc = b * (b == c) |
|
|
|
a = ab | ac | bc |
|
|
|
result = a + (a == 0) * d - 1 |
|
|
|
if upgraded: |
|
return downgrade_type(result) |
|
|
|
|
|
|
|
data -= 1 |
|
|
|
return result |
|
|
|
def countless_extreme(data): |
|
nonzeros = np.count_nonzero(data) |
|
|
|
|
|
N = reduce(operator.mul, data.shape) |
|
|
|
if nonzeros == N: |
|
print("quick") |
|
return quick_countless(data) |
|
elif np.count_nonzero(data + 1) == N: |
|
print("quick") |
|
|
|
return quick_countless(data) |
|
else: |
|
return countless(data) |
|
|
|
|
|
def countless(data): |
|
""" |
|
Vectorized implementation of downsampling a 2D |
|
image by 2 on each side using the COUNTLESS algorithm. |
|
|
|
data is a 2D numpy array with even dimensions. |
|
""" |
|
|
|
|
|
data, upgraded = upgrade_type(data) |
|
|
|
|
|
|
|
data += 1 |
|
|
|
sections = [] |
|
|
|
|
|
|
|
|
|
factor = (2,2) |
|
for offset in np.ndindex(factor): |
|
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
sections.append(part) |
|
|
|
a, b, c, d = sections |
|
|
|
ab_ac = a * ((a == b) | (a == c)) |
|
ab_ac |= b * (b == c) |
|
result = ab_ac + (ab_ac == 0) * d - 1 |
|
|
|
if upgraded: |
|
return downgrade_type(result) |
|
|
|
|
|
|
|
data -= 1 |
|
|
|
return result |
|
|
|
def upgrade_type(arr): |
|
dtype = arr.dtype |
|
|
|
if dtype == np.uint8: |
|
return arr.astype(np.uint16), True |
|
elif dtype == np.uint16: |
|
return arr.astype(np.uint32), True |
|
elif dtype == np.uint32: |
|
return arr.astype(np.uint64), True |
|
|
|
return arr, False |
|
|
|
def downgrade_type(arr): |
|
dtype = arr.dtype |
|
|
|
if dtype == np.uint64: |
|
return arr.astype(np.uint32) |
|
elif dtype == np.uint32: |
|
return arr.astype(np.uint16) |
|
elif dtype == np.uint16: |
|
return arr.astype(np.uint8) |
|
|
|
return arr |
|
|
|
def odd_to_even(image): |
|
""" |
|
To facilitate 2x2 downsampling segmentation, change an odd sized image into an even sized one. |
|
Works by mirroring the starting 1 pixel edge of the image on odd shaped sides. |
|
|
|
e.g. turn a 3x3x5 image into a 4x4x5 (the x and y are what are getting downsampled) |
|
|
|
For example: [ 3, 2, 4 ] => [ 3, 3, 2, 4 ] which is now easy to downsample. |
|
|
|
""" |
|
shape = np.array(image.shape) |
|
|
|
offset = (shape % 2)[:2] |
|
|
|
|
|
|
|
if not np.any(offset): |
|
return image |
|
|
|
oddshape = image.shape[:2] + offset |
|
oddshape = np.append(oddshape, shape[2:]) |
|
oddshape = oddshape.astype(int) |
|
|
|
newimg = np.empty(shape=oddshape, dtype=image.dtype) |
|
|
|
ox,oy = offset |
|
sx,sy = oddshape |
|
|
|
newimg[0,0] = image[0,0] |
|
newimg[ox:sx,0] = image[:,0] |
|
newimg[0,oy:sy] = image[0,:] |
|
|
|
return newimg |
|
|
|
def counting(array): |
|
factor = (2, 2, 1) |
|
shape = array.shape |
|
|
|
while len(shape) < 4: |
|
array = np.expand_dims(array, axis=-1) |
|
shape = array.shape |
|
|
|
output_shape = tuple(int(math.ceil(s / f)) for s, f in zip(shape, factor)) |
|
output = np.zeros(output_shape, dtype=array.dtype) |
|
|
|
for chan in range(0, shape[3]): |
|
for z in range(0, shape[2]): |
|
for x in range(0, shape[0], 2): |
|
for y in range(0, shape[1], 2): |
|
block = array[ x:x+2, y:y+2, z, chan ] |
|
|
|
hashtable = defaultdict(int) |
|
for subx, suby in np.ndindex(block.shape[0], block.shape[1]): |
|
hashtable[block[subx, suby]] += 1 |
|
|
|
best = (0, 0) |
|
for segid, val in six.iteritems(hashtable): |
|
if best[1] < val: |
|
best = (segid, val) |
|
|
|
output[ x // 2, y // 2, chan ] = best[0] |
|
|
|
return output |
|
|
|
def ndzoom(array): |
|
if len(array.shape) == 3: |
|
ratio = ( 1 / 2.0, 1 / 2.0, 1.0 ) |
|
else: |
|
ratio = ( 1 / 2.0, 1 / 2.0) |
|
return ndimage.interpolation.zoom(array, ratio, order=1) |
|
|
|
def countless_if(array): |
|
factor = (2, 2, 1) |
|
shape = array.shape |
|
|
|
if len(shape) < 3: |
|
array = array[ :,:, np.newaxis ] |
|
shape = array.shape |
|
|
|
output_shape = tuple(int(math.ceil(s / f)) for s, f in zip(shape, factor)) |
|
output = np.zeros(output_shape, dtype=array.dtype) |
|
|
|
for chan in range(0, shape[2]): |
|
for x in range(0, shape[0], 2): |
|
for y in range(0, shape[1], 2): |
|
block = array[ x:x+2, y:y+2, chan ] |
|
|
|
if block[0,0] == block[1,0]: |
|
pick = block[0,0] |
|
elif block[0,0] == block[0,1]: |
|
pick = block[0,0] |
|
elif block[1,0] == block[0,1]: |
|
pick = block[1,0] |
|
else: |
|
pick = block[1,1] |
|
|
|
output[ x // 2, y // 2, chan ] = pick |
|
|
|
return np.squeeze(output) |
|
|
|
def downsample_with_averaging(array): |
|
""" |
|
Downsample x by factor using averaging. |
|
|
|
@return: The downsampled array, of the same type as x. |
|
""" |
|
|
|
if len(array.shape) == 3: |
|
factor = (2,2,1) |
|
else: |
|
factor = (2,2) |
|
|
|
if np.array_equal(factor[:3], np.array([1,1,1])): |
|
return array |
|
|
|
output_shape = tuple(int(math.ceil(s / f)) for s, f in zip(array.shape, factor)) |
|
temp = np.zeros(output_shape, float) |
|
counts = np.zeros(output_shape, np.int) |
|
for offset in np.ndindex(factor): |
|
part = array[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
indexing_expr = tuple(np.s_[:s] for s in part.shape) |
|
temp[indexing_expr] += part |
|
counts[indexing_expr] += 1 |
|
return np.cast[array.dtype](temp / counts) |
|
|
|
def downsample_with_max_pooling(array): |
|
|
|
factor = (2,2) |
|
|
|
if np.all(np.array(factor, int) == 1): |
|
return array |
|
|
|
sections = [] |
|
|
|
for offset in np.ndindex(factor): |
|
part = array[tuple(np.s_[o::f] for o, f in zip(offset, factor))] |
|
sections.append(part) |
|
|
|
output = sections[0].copy() |
|
|
|
for section in sections[1:]: |
|
np.maximum(output, section, output) |
|
|
|
return output |
|
|
|
def striding(array): |
|
"""Downsample x by factor using striding. |
|
|
|
@return: The downsampled array, of the same type as x. |
|
""" |
|
factor = (2,2) |
|
if np.all(np.array(factor, int) == 1): |
|
return array |
|
return array[tuple(np.s_[::f] for f in factor)] |
|
|
|
def benchmark(): |
|
filename = sys.argv[1] |
|
img = Image.open(filename) |
|
data = np.array(img.getdata(), dtype=np.uint8) |
|
|
|
if len(data.shape) == 1: |
|
n_channels = 1 |
|
reshape = (img.height, img.width) |
|
else: |
|
n_channels = min(data.shape[1], 3) |
|
data = data[:, :n_channels] |
|
reshape = (img.height, img.width, n_channels) |
|
|
|
data = data.reshape(reshape).astype(np.uint8) |
|
|
|
methods = [ |
|
simplest_countless, |
|
quick_countless, |
|
quick_countless_xor, |
|
quickest_countless, |
|
stippled_countless, |
|
zero_corrected_countless, |
|
countless, |
|
downsample_with_averaging, |
|
downsample_with_max_pooling, |
|
ndzoom, |
|
striding, |
|
|
|
|
|
] |
|
|
|
formats = { |
|
1: 'L', |
|
3: 'RGB', |
|
4: 'RGBA' |
|
} |
|
|
|
if not os.path.exists('./results'): |
|
os.mkdir('./results') |
|
|
|
N = 500 |
|
img_size = float(img.width * img.height) / 1024.0 / 1024.0 |
|
print("N = %d, %dx%d (%.2f MPx) %d chan, %s" % (N, img.width, img.height, img_size, n_channels, filename)) |
|
print("Algorithm\tMPx/sec\tMB/sec\tSec") |
|
for fn in methods: |
|
print(fn.__name__, end='') |
|
sys.stdout.flush() |
|
|
|
start = time.time() |
|
|
|
|
|
for _ in tqdm(range(N), desc=fn.__name__, disable=True): |
|
result = fn(data) |
|
end = time.time() |
|
print("\r", end='') |
|
|
|
total_time = (end - start) |
|
mpx = N * img_size / total_time |
|
mbytes = N * img_size * n_channels / total_time |
|
|
|
print("%s\t%.3f\t%.3f\t%.2f" % (fn.__name__, mpx, mbytes, total_time)) |
|
outimg = Image.fromarray(np.squeeze(result), formats[n_channels]) |
|
outimg.save('./results/{}.png'.format(fn.__name__, "PNG")) |
|
|
|
if __name__ == '__main__': |
|
benchmark() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|