2f6f2b93ef
esp. the new force mode
286 lines
11 KiB
Python
Executable File
286 lines
11 KiB
Python
Executable File
#!/usr/bin/python
|
|
# vim: set fileencoding=utf-8 :
|
|
|
|
import unittest
|
|
import math
|
|
|
|
#import logging
|
|
#logging.basicConfig(level = logging.DEBUG)
|
|
|
|
import gi
|
|
gi.require_version('Vips', '8.0')
|
|
from gi.repository import Vips
|
|
|
|
Vips.leak_set(True)
|
|
|
|
unsigned_formats = [Vips.BandFormat.UCHAR,
|
|
Vips.BandFormat.USHORT,
|
|
Vips.BandFormat.UINT]
|
|
signed_formats = [Vips.BandFormat.CHAR,
|
|
Vips.BandFormat.SHORT,
|
|
Vips.BandFormat.INT]
|
|
float_formats = [Vips.BandFormat.FLOAT,
|
|
Vips.BandFormat.DOUBLE]
|
|
complex_formats = [Vips.BandFormat.COMPLEX,
|
|
Vips.BandFormat.DPCOMPLEX]
|
|
int_formats = unsigned_formats + signed_formats
|
|
noncomplex_formats = int_formats + float_formats
|
|
all_formats = int_formats + float_formats + complex_formats
|
|
|
|
# Run a function expecting a complex image on a two-band image
|
|
def run_cmplx(fn, image):
|
|
if image.format == Vips.BandFormat.FLOAT:
|
|
new_format = Vips.BandFormat.COMPLEX
|
|
elif image.format == Vips.BandFormat.DOUBLE:
|
|
new_format = Vips.BandFormat.DPCOMPLEX
|
|
else:
|
|
raise "run_cmplx: not float or double"
|
|
|
|
# tag as complex, run, revert tagging
|
|
cmplx = image.copy(bands = 1, format = new_format)
|
|
cmplx_result = fn(cmplx)
|
|
|
|
return cmplx_result.copy(bands = 2, format = image.format)
|
|
|
|
def to_polar(image):
|
|
"""Transform image coordinates to polar.
|
|
|
|
The image is transformed so that it is wrapped around a point in the
|
|
centre. Vertical straight lines become circles or segments of circles,
|
|
horizontal straight lines become radial spokes.
|
|
"""
|
|
# xy image, zero in the centre, scaled to fit image to a circle
|
|
xy = Vips.Image.xyz(image.width, image.height)
|
|
xy -= [image.width / 2.0, image.height / 2.0]
|
|
scale = min(image.width, image.height) / float(image.width)
|
|
xy *= 2.0 / scale
|
|
|
|
# to polar, scale vertical axis to 360 degrees
|
|
index = run_cmplx(lambda x: x.polar(), xy)
|
|
index *= [1, image.height / 360.0]
|
|
|
|
return image.mapim(index)
|
|
|
|
def to_rectangular(image):
|
|
"""Transform image coordinates to rectangular.
|
|
|
|
The image is transformed so that it is unwrapped from a point in the
|
|
centre. Circles or segments of circles become vertical straight lines,
|
|
radial lines become horizontal lines.
|
|
"""
|
|
# xy image, vertical scaled to 360 degrees
|
|
xy = Vips.Image.xyz(image.width, image.height)
|
|
xy *= [1, 360.0 / image.height]
|
|
|
|
# to rect, scale to image rect
|
|
index = run_cmplx(lambda x: x.rect(), xy)
|
|
scale = min(image.width, image.height) / float(image.width)
|
|
index *= scale / 2.0
|
|
index += [image.width / 2.0, image.height / 2.0]
|
|
|
|
return image.mapim(index)
|
|
|
|
# an expanding zip ... if either of the args is a scalar or a one-element list,
|
|
# duplicate it down the other side
|
|
def zip_expand(x, y):
|
|
# handle singleton list case
|
|
if isinstance(x, list) and len(x) == 1:
|
|
x = x[0]
|
|
if isinstance(y, list) and len(y) == 1:
|
|
y = y[0]
|
|
|
|
if isinstance(x, list) and isinstance(y, list):
|
|
return list(zip(x, y))
|
|
elif isinstance(x, list):
|
|
return [[i, y] for i in x]
|
|
elif isinstance(y, list):
|
|
return [[x, j] for j in y]
|
|
else:
|
|
return [[x, y]]
|
|
|
|
class TestResample(unittest.TestCase):
|
|
# test a pair of things which can be lists for approx. equality
|
|
def assertAlmostEqualObjects(self, a, b, places = 4, msg = ''):
|
|
# print 'assertAlmostEqualObjects %s = %s' % (a, b)
|
|
for x, y in zip_expand(a, b):
|
|
self.assertAlmostEqual(x, y, places = places, msg = msg)
|
|
|
|
def setUp(self):
|
|
self.jpeg_file = "images/йцук.jpg"
|
|
self.rotated_jpeg_file = "images/Landscape_6.jpg"
|
|
|
|
def test_affine(self):
|
|
im = Vips.Image.new_from_file(self.jpeg_file)
|
|
|
|
# vsqbs is non-interpolatory, don't test this way
|
|
for name in ["nearest", "bicubic", "bilinear", "nohalo", "lbb"]:
|
|
x = im
|
|
interpolate = Vips.Interpolate.new(name)
|
|
for i in range(4):
|
|
x = x.affine([0, 1, 1, 0], interpolate = interpolate)
|
|
|
|
self.assertEqual((x - im).abs().max(), 0)
|
|
|
|
def test_reduce(self):
|
|
im = Vips.Image.new_from_file(self.jpeg_file)
|
|
# cast down to 0-127, the smallest range, so we aren't messed up by
|
|
# clipping
|
|
im = im.cast(Vips.BandFormat.CHAR)
|
|
bicubic = Vips.Interpolate.new("bicubic")
|
|
bilinear = Vips.Interpolate.new("bilinear")
|
|
nearest = Vips.Interpolate.new("nearest")
|
|
|
|
for fac in [1, 1.1, 1.5, 1.999]:
|
|
for fmt in all_formats:
|
|
x = im.cast(fmt)
|
|
r = x.reduce(fac, fac, kernel = "cubic")
|
|
a = x.affine([1.0 / fac, 0, 0, 1.0 / fac],
|
|
interpolate = bicubic,
|
|
oarea = [0, 0, x.width / fac, x.height / fac])
|
|
d = (r - a).abs().max()
|
|
self.assertLess(d, 10)
|
|
|
|
for fac in [1, 1.1, 1.5, 1.999]:
|
|
for fmt in all_formats:
|
|
x = im.cast(fmt)
|
|
r = x.reduce(fac, fac, kernel = "linear")
|
|
a = x.affine([1.0 / fac, 0, 0, 1.0 / fac],
|
|
interpolate = bilinear,
|
|
oarea = [0, 0, x.width / fac, x.height / fac])
|
|
d = (r - a).abs().max()
|
|
self.assertLess(d, 10)
|
|
|
|
# for other kernels, just see if avg looks about right
|
|
for fac in [1, 1.1, 1.5, 1.999]:
|
|
for fmt in all_formats:
|
|
for kernel in ["nearest", "lanczos2", "lanczos3"]:
|
|
x = im.cast(fmt)
|
|
r = x.reduce(fac, fac, kernel = kernel)
|
|
d = abs(r.avg() - im.avg())
|
|
self.assertLess(d, 2)
|
|
|
|
# try constant images ... should not change the constant
|
|
for const in [0, 1, 2, 254, 255]:
|
|
im = (Vips.Image.black(10, 10) + const).cast("uchar")
|
|
for kernel in ["nearest", "linear", "cubic", "lanczos2", "lanczos3"]:
|
|
# print "testing kernel =", kernel
|
|
# print "testing const =", const
|
|
shr = im.reduce(2, 2, kernel = kernel)
|
|
d = abs(shr.avg() - im.avg())
|
|
self.assertEqual(d, 0)
|
|
|
|
def test_resize(self):
|
|
im = Vips.Image.new_from_file(self.jpeg_file)
|
|
im2 = im.resize(0.25)
|
|
self.assertEqual(im2.width, round(im.width / 4.0))
|
|
self.assertEqual(im2.height, round(im.height / 4.0))
|
|
|
|
# test geometry rounding corner case
|
|
im = Vips.Image.black(100, 1);
|
|
x = im.resize(0.5)
|
|
self.assertEqual(x.width, 50)
|
|
self.assertEqual(x.height, 1)
|
|
|
|
def test_shrink(self):
|
|
im = Vips.Image.new_from_file(self.jpeg_file)
|
|
im2 = im.shrink(4, 4)
|
|
self.assertEqual(im2.width, round(im.width / 4.0))
|
|
self.assertEqual(im2.height, round(im.height / 4.0))
|
|
self.assertTrue(abs(im.avg() - im2.avg()) < 1)
|
|
|
|
im2 = im.shrink(2.5, 2.5)
|
|
self.assertEqual(im2.width, round(im.width / 2.5))
|
|
self.assertEqual(im2.height, round(im.height / 2.5))
|
|
self.assertLess(abs(im.avg() - im2.avg()), 1)
|
|
|
|
def test_thumbnail(self):
|
|
im = Vips.Image.thumbnail(self.jpeg_file, 100)
|
|
|
|
self.assertEqual(im.width, 100)
|
|
self.assertEqual(im.bands, 3)
|
|
self.assertEqual(im.bands, 3)
|
|
|
|
# the average shouldn't move too much
|
|
im_orig = Vips.Image.new_from_file(self.jpeg_file)
|
|
self.assertLess(abs(im_orig.avg() - im.avg()), 1)
|
|
|
|
# make sure we always get the right width
|
|
for width in range(1000, 1, -13):
|
|
im = Vips.Image.thumbnail(self.jpeg_file, width)
|
|
self.assertEqual(im.width, width)
|
|
|
|
# should fit one of width or height
|
|
im = Vips.Image.thumbnail(self.jpeg_file, 100, height = 300)
|
|
self.assertEqual(im.width, 100)
|
|
self.assertNotEqual(im.height, 300)
|
|
im = Vips.Image.thumbnail(self.jpeg_file, 300, height = 100)
|
|
self.assertNotEqual(im.width, 300)
|
|
self.assertEqual(im.height, 100)
|
|
|
|
# force should fit width and height ... although this jpg has an
|
|
# orientation tag, we ignore it unless autorot is on
|
|
im = Vips.Image.thumbnail(self.rotated_jpeg_file, 100, height = 300,
|
|
size = "force")
|
|
self.assertEqual(im.width, 100)
|
|
self.assertEqual(im.height, 300)
|
|
|
|
# with force + autorot, we spin the image, but the output size should
|
|
# not change
|
|
im = Vips.Image.thumbnail(self.rotated_jpeg_file, 100, height = 300,
|
|
size = "force", auto_rotate = True)
|
|
self.assertEqual(im.width, 100)
|
|
self.assertEqual(im.height, 300)
|
|
|
|
# with @crop, should fit both width and height
|
|
im = Vips.Image.thumbnail(self.jpeg_file, 100,
|
|
height = 300, crop = "centre")
|
|
self.assertEqual(im.width, 100)
|
|
self.assertEqual(im.height, 300)
|
|
|
|
# with size up, should not downsize
|
|
im = Vips.Image.thumbnail(self.jpeg_file, 100, size = "up")
|
|
self.assertEqual(im.width, im_orig.width)
|
|
im = Vips.Image.thumbnail(self.jpeg_file, 10000, size = "up")
|
|
self.assertEqual(im.width, 10000)
|
|
|
|
# with size down, should not upsize
|
|
im = Vips.Image.thumbnail(self.jpeg_file, 100, size = "down")
|
|
self.assertEqual(im.width, 100)
|
|
im = Vips.Image.thumbnail(self.jpeg_file, 10000, size = "down")
|
|
self.assertEqual(im.width, im_orig.width)
|
|
|
|
im1 = Vips.Image.thumbnail(self.jpeg_file, 100)
|
|
with open(self.jpeg_file, 'rb') as f:
|
|
buf = f.read()
|
|
im2 = Vips.Image.thumbnail_buffer(buf, 100)
|
|
self.assertLess(abs(im1.avg() - im2.avg()), 1)
|
|
|
|
def test_similarity(self):
|
|
im = Vips.Image.new_from_file(self.jpeg_file)
|
|
im2 = im.similarity(angle = 90)
|
|
im3 = im.affine([0, -1, 1, 0])
|
|
# rounding in calculating the affine transform from the angle stops
|
|
# this being exactly true
|
|
self.assertLess((im2 - im3).abs().max(), 50)
|
|
|
|
def test_similarity_scale(self):
|
|
im = Vips.Image.new_from_file(self.jpeg_file)
|
|
im2 = im.similarity(scale = 2)
|
|
im3 = im.affine([2, 0, 0, 2])
|
|
self.assertEqual((im2 - im3).abs().max(), 0)
|
|
|
|
def test_mapim(self):
|
|
im = Vips.Image.new_from_file(self.jpeg_file)
|
|
|
|
p = to_polar(im)
|
|
r = to_rectangular(p)
|
|
|
|
# the left edge (which is squashed to the origin) will be badly
|
|
# distorted, but the rest should not be too bad
|
|
a = r.crop(50, 0, im.width - 50, im.height).gaussblur(2)
|
|
b = im.crop(50, 0, im.width - 50, im.height).gaussblur(2)
|
|
self.assertLess((a - b).abs().max(), 20)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|