diff --git a/.gitignore b/.gitignore index c91c0ee7..901addc6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ compile +.pytest_cache a.out *.log *.trs diff --git a/ChangeLog b/ChangeLog index 5fc4db35..637cc439 100644 --- a/ChangeLog +++ b/ChangeLog @@ -32,6 +32,7 @@ - pyramid builders have a choice of 2x2 shrinkers [harukizaemon] - add `palette` option to pngsave [felixbuenemann] - support writing string-valued fields via libexif +- paste in the test suite from pyvips 12/3/18 started 8.6.4 - better fitting of fonts with overhanging edges [Adrià] diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..c84c9935 --- /dev/null +++ b/test/README.md @@ -0,0 +1,5 @@ +# libvips test suite + +This is in two parts: a few simple bash scripts in this directory are run on +"make check", and a fancier Python test suite that's run by Travis on each +commit. diff --git a/test/images/Landscape_6.jpg b/test/images/Landscape_6.jpg deleted file mode 100644 index c692f195..00000000 Binary files a/test/images/Landscape_6.jpg and /dev/null differ diff --git a/test/test-suite/__init__.py b/test/test-suite/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test-suite/conftest.py b/test/test-suite/conftest.py new file mode 100644 index 00000000..9661c490 --- /dev/null +++ b/test/test-suite/conftest.py @@ -0,0 +1,4 @@ +import sys +import os + +sys.path.append(os.path.join(os.path.dirname(__file__), 'helpers')) diff --git a/test/test-suite/helpers/helpers.py b/test/test-suite/helpers/helpers.py new file mode 100644 index 00000000..41a02e5b --- /dev/null +++ b/test/test-suite/helpers/helpers.py @@ -0,0 +1,232 @@ +# vim: set fileencoding=utf-8 : +# test helpers +import os +import tempfile +import pytest + +import pyvips + +IMAGES = os.path.join(os.path.dirname(__file__), os.pardir, 'images') +JPEG_FILE = os.path.join(IMAGES, "йцук.jpg") +SRGB_FILE = os.path.join(IMAGES, "sRGB.icm") +MATLAB_FILE = os.path.join(IMAGES, "sample.mat") +PNG_FILE = os.path.join(IMAGES, "sample.png") +TIF_FILE = os.path.join(IMAGES, "sample.tif") +OME_FILE = os.path.join(IMAGES, "multi-channel-z-series.ome.tif") +ANALYZE_FILE = os.path.join(IMAGES, "t00740_tr1_segm.hdr") +GIF_FILE = os.path.join(IMAGES, "cramps.gif") +WEBP_FILE = os.path.join(IMAGES, "1.webp") +EXR_FILE = os.path.join(IMAGES, "sample.exr") +FITS_FILE = os.path.join(IMAGES, "WFPC2u5780205r_c0fx.fits") +OPENSLIDE_FILE = os.path.join(IMAGES, "CMU-1-Small-Region.svs") +PDF_FILE = os.path.join(IMAGES, "ISO_12233-reschart.pdf") +CMYK_PDF_FILE = os.path.join(IMAGES, "cmyktest.pdf") +SVG_FILE = os.path.join(IMAGES, "vips-profile.svg") +SVGZ_FILE = os.path.join(IMAGES, "vips-profile.svgz") +SVG_GZ_FILE = os.path.join(IMAGES, "vips-profile.svg.gz") +GIF_ANIM_FILE = os.path.join(IMAGES, "cogs.gif") +DICOM_FILE = os.path.join(IMAGES, "dicom_test_image.dcm") +BMP_FILE = os.path.join(IMAGES, "MARBLES.BMP") + +unsigned_formats = [pyvips.BandFormat.UCHAR, + pyvips.BandFormat.USHORT, + pyvips.BandFormat.UINT] +signed_formats = [pyvips.BandFormat.CHAR, + pyvips.BandFormat.SHORT, + pyvips.BandFormat.INT] +float_formats = [pyvips.BandFormat.FLOAT, + pyvips.BandFormat.DOUBLE] +complex_formats = [pyvips.BandFormat.COMPLEX, + pyvips.BandFormat.DPCOMPLEX] +int_formats = unsigned_formats + signed_formats +noncomplex_formats = int_formats + float_formats +all_formats = int_formats + float_formats + complex_formats + +colour_colourspaces = [pyvips.Interpretation.XYZ, + pyvips.Interpretation.LAB, + pyvips.Interpretation.LCH, + pyvips.Interpretation.CMC, + pyvips.Interpretation.LABS, + pyvips.Interpretation.SCRGB, + pyvips.Interpretation.HSV, + pyvips.Interpretation.SRGB, + pyvips.Interpretation.YXY] +coded_colourspaces = [pyvips.Interpretation.LABQ] +mono_colourspaces = [pyvips.Interpretation.B_W] +sixteenbit_colourspaces = [pyvips.Interpretation.GREY16, + pyvips.Interpretation.RGB16] +all_colourspaces = colour_colourspaces + mono_colourspaces + \ + coded_colourspaces + sixteenbit_colourspaces + +max_value = {pyvips.BandFormat.UCHAR: 0xff, + pyvips.BandFormat.USHORT: 0xffff, + pyvips.BandFormat.UINT: 0xffffffff, + pyvips.BandFormat.CHAR: 0x7f, + pyvips.BandFormat.SHORT: 0x7fff, + pyvips.BandFormat.INT: 0x7fffffff, + pyvips.BandFormat.FLOAT: 1.0, + pyvips.BandFormat.DOUBLE: 1.0, + pyvips.BandFormat.COMPLEX: 1.0, + pyvips.BandFormat.DPCOMPLEX: 1.0} + +sizeof_format = {pyvips.BandFormat.UCHAR: 1, + pyvips.BandFormat.USHORT: 2, + pyvips.BandFormat.UINT: 4, + pyvips.BandFormat.CHAR: 1, + pyvips.BandFormat.SHORT: 2, + pyvips.BandFormat.INT: 4, + pyvips.BandFormat.FLOAT: 4, + pyvips.BandFormat.DOUBLE: 8, + pyvips.BandFormat.COMPLEX: 8, + pyvips.BandFormat.DPCOMPLEX: 16} + +rot45_angles = [pyvips.Angle45.D0, + pyvips.Angle45.D45, + pyvips.Angle45.D90, + pyvips.Angle45.D135, + pyvips.Angle45.D180, + pyvips.Angle45.D225, + pyvips.Angle45.D270, + pyvips.Angle45.D315] + +rot45_angle_bonds = [pyvips.Angle45.D0, + pyvips.Angle45.D315, + pyvips.Angle45.D270, + pyvips.Angle45.D225, + pyvips.Angle45.D180, + pyvips.Angle45.D135, + pyvips.Angle45.D90, + pyvips.Angle45.D45] + +rot_angles = [pyvips.Angle.D0, + pyvips.Angle.D90, + pyvips.Angle.D180, + pyvips.Angle.D270] + +rot_angle_bonds = [pyvips.Angle.D0, + pyvips.Angle.D270, + pyvips.Angle.D180, + pyvips.Angle.D90] + + +# 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]] + + +# run a 1-ary function on a thing -- loop over elements if the +# thing is a list +def run_fn(fn, x): + if isinstance(x, list): + return [fn(i) for i in x] + else: + return fn(x) + + +# make a temp filename with the specified suffix and in the +# specified directory +def temp_filename(directory, suffix): + temp_name = next(tempfile._get_candidate_names()) + filename = os.path.join(directory, temp_name + suffix) + + return filename + + +# test for an operator exists +def have(name): + return pyvips.type_find("VipsOperation", name) != 0 + + +def skip_if_no(operation_name): + return pytest.mark.skipif(not have(operation_name), + reason='no {}, skipping test'.format(operation_name)) + + +# run a 2-ary function on two things -- loop over elements pairwise if the +# things are lists +def run_fn2(fn, x, y): + if isinstance(x, pyvips.Image) or isinstance(y, pyvips.Image): + return fn(x, y) + elif isinstance(x, list) or isinstance(y, list): + return [fn(i, j) for i, j in zip_expand(x, y)] + else: + return fn(x, y) + + +# test a pair of things which can be lists for approx. equality +def assert_almost_equal_objects(a, b, threshold=0.0001, msg=''): + # print 'assertAlmostEqualObjects %s = %s' % (a, b) + assert all([pytest.approx(x, abs=threshold) == y + for x, y in zip_expand(a, b)]), msg + + +# test a pair of things which can be lists for equality +def assert_equal_objects(a, b, msg=''): + # print 'assertEqualObjects %s = %s' % (a, b) + assert all([x == y for x, y in zip_expand(a, b)]), msg + + +# test a pair of things which can be lists for difference less than a +# threshold +def assert_less_threshold(a, b, diff): + assert all([abs(x - y) < diff for x, y in zip_expand(a, b)]) + + +# run a function on an image and on a single pixel, the results +# should match +def run_cmp(message, im, x, y, fn): + a = im(x, y) + v1 = fn(a) + im2 = fn(im) + v2 = im2(x, y) + assert_almost_equal_objects(v1, v2, msg=message) + + +# run a function on an image, +# 50,50 and 10,10 should have different values on the test image +def run_image(message, im, fn): + run_cmp(message, im, 50, 50, fn) + run_cmp(message, im, 10, 10, fn) + + +# run a function on (image, constant), and on (constant, image). +# 50,50 and 10,10 should have different values on the test image +def run_const(message, fn, im, c): + run_cmp(message, im, 50, 50, lambda x: run_fn2(fn, x, c)) + run_cmp(message, im, 50, 50, lambda x: run_fn2(fn, c, x)) + run_cmp(message, im, 10, 10, lambda x: run_fn2(fn, x, c)) + run_cmp(message, im, 10, 10, lambda x: run_fn2(fn, c, x)) + + +# run a function on a pair of images and on a pair of pixels, the results +# should match +def run_cmp2(message, left, right, x, y, fn): + a = left(x, y) + b = right(x, y) + v1 = fn(a, b) + after = fn(left, right) + v2 = after(x, y) + assert_almost_equal_objects(v1, v2, msg=message) + + +# run a function on a pair of images +# 50,50 and 10,10 should have different values on the test image +def run_image2(message, left, right, fn): + run_cmp2(message, left, right, 50, 50, + lambda x, y: run_fn2(fn, x, y)) + run_cmp2(message, left, right, 10, 10, + lambda x, y: run_fn2(fn, x, y)) diff --git a/test/images/1.webp b/test/test-suite/images/1.webp similarity index 100% rename from test/images/1.webp rename to test/test-suite/images/1.webp diff --git a/test/images/CMU-1-Small-Region.svs b/test/test-suite/images/CMU-1-Small-Region.svs similarity index 100% rename from test/images/CMU-1-Small-Region.svs rename to test/test-suite/images/CMU-1-Small-Region.svs diff --git a/test/images/ISO_12233-reschart.pdf b/test/test-suite/images/ISO_12233-reschart.pdf similarity index 100% rename from test/images/ISO_12233-reschart.pdf rename to test/test-suite/images/ISO_12233-reschart.pdf diff --git a/test/test-suite/images/MARBLES.BMP b/test/test-suite/images/MARBLES.BMP new file mode 100644 index 00000000..06a1c009 Binary files /dev/null and b/test/test-suite/images/MARBLES.BMP differ diff --git a/test/images/WFPC2u5780205r_c0fx.fits b/test/test-suite/images/WFPC2u5780205r_c0fx.fits similarity index 100% rename from test/images/WFPC2u5780205r_c0fx.fits rename to test/test-suite/images/WFPC2u5780205r_c0fx.fits diff --git a/test/images/blankpage.pdf b/test/test-suite/images/blankpage.pdf similarity index 100% rename from test/images/blankpage.pdf rename to test/test-suite/images/blankpage.pdf diff --git a/test/images/blankpage.pdf.png b/test/test-suite/images/blankpage.pdf.png similarity index 100% rename from test/images/blankpage.pdf.png rename to test/test-suite/images/blankpage.pdf.png diff --git a/test/images/blankpage.svg b/test/test-suite/images/blankpage.svg similarity index 100% rename from test/images/blankpage.svg rename to test/test-suite/images/blankpage.svg diff --git a/test/images/blankpage.svg.png b/test/test-suite/images/blankpage.svg.png similarity index 100% rename from test/images/blankpage.svg.png rename to test/test-suite/images/blankpage.svg.png diff --git a/test/images/cmyktest.pdf b/test/test-suite/images/cmyktest.pdf similarity index 100% rename from test/images/cmyktest.pdf rename to test/test-suite/images/cmyktest.pdf diff --git a/test/images/cogs.gif b/test/test-suite/images/cogs.gif similarity index 100% rename from test/images/cogs.gif rename to test/test-suite/images/cogs.gif diff --git a/test/images/cramps.gif b/test/test-suite/images/cramps.gif similarity index 100% rename from test/images/cramps.gif rename to test/test-suite/images/cramps.gif diff --git a/test/images/dicom_test_image.dcm b/test/test-suite/images/dicom_test_image.dcm similarity index 100% rename from test/images/dicom_test_image.dcm rename to test/test-suite/images/dicom_test_image.dcm diff --git a/test/images/multi-channel-z-series.ome.tif b/test/test-suite/images/multi-channel-z-series.ome.tif similarity index 100% rename from test/images/multi-channel-z-series.ome.tif rename to test/test-suite/images/multi-channel-z-series.ome.tif diff --git a/test/images/sRGB.icm b/test/test-suite/images/sRGB.icm similarity index 100% rename from test/images/sRGB.icm rename to test/test-suite/images/sRGB.icm diff --git a/test/images/sample.exr b/test/test-suite/images/sample.exr similarity index 100% rename from test/images/sample.exr rename to test/test-suite/images/sample.exr diff --git a/test/images/sample.mat b/test/test-suite/images/sample.mat similarity index 100% rename from test/images/sample.mat rename to test/test-suite/images/sample.mat diff --git a/test/images/sample.png b/test/test-suite/images/sample.png similarity index 100% rename from test/images/sample.png rename to test/test-suite/images/sample.png diff --git a/test/images/sample.tif b/test/test-suite/images/sample.tif similarity index 100% rename from test/images/sample.tif rename to test/test-suite/images/sample.tif diff --git a/test/images/t00740_tr1_segm.hdr b/test/test-suite/images/t00740_tr1_segm.hdr similarity index 100% rename from test/images/t00740_tr1_segm.hdr rename to test/test-suite/images/t00740_tr1_segm.hdr diff --git a/test/images/t00740_tr1_segm.img b/test/test-suite/images/t00740_tr1_segm.img similarity index 100% rename from test/images/t00740_tr1_segm.img rename to test/test-suite/images/t00740_tr1_segm.img diff --git a/test/images/trans-x.gif b/test/test-suite/images/trans-x.gif similarity index 100% rename from test/images/trans-x.gif rename to test/test-suite/images/trans-x.gif diff --git a/test/images/trans-x.png b/test/test-suite/images/trans-x.png similarity index 100% rename from test/images/trans-x.png rename to test/test-suite/images/trans-x.png diff --git a/test/images/vips-profile.svg b/test/test-suite/images/vips-profile.svg similarity index 100% rename from test/images/vips-profile.svg rename to test/test-suite/images/vips-profile.svg diff --git a/test/images/vips-profile.svg.gz b/test/test-suite/images/vips-profile.svg.gz similarity index 100% rename from test/images/vips-profile.svg.gz rename to test/test-suite/images/vips-profile.svg.gz diff --git a/test/images/vips-profile.svgz b/test/test-suite/images/vips-profile.svgz similarity index 100% rename from test/images/vips-profile.svgz rename to test/test-suite/images/vips-profile.svgz diff --git a/test/images/йцук.jpg b/test/test-suite/images/йцук.jpg similarity index 100% rename from test/images/йцук.jpg rename to test/test-suite/images/йцук.jpg diff --git a/test/test-suite/test_arithmetic.py b/test/test-suite/test_arithmetic.py new file mode 100644 index 00000000..8e6f3e46 --- /dev/null +++ b/test/test-suite/test_arithmetic.py @@ -0,0 +1,641 @@ +# vim: set fileencoding=utf-8 : +import math +import pytest + +import pyvips +from helpers import unsigned_formats, float_formats, noncomplex_formats, \ + all_formats, run_fn, run_image2, run_const, run_cmp, \ + assert_almost_equal_objects + + +class TestArithmetic: + def run_arith(self, fn, fmt=all_formats): + [run_image2(fn.__name__ + ' image', x.cast(y), x.cast(z), fn) + for x in self.all_images for y in fmt for z in fmt] + + def run_arith_const(self, fn, fmt=all_formats): + [run_const(fn.__name__ + ' scalar', fn, x.cast(y), 2) + for x in self.all_images for y in fmt] + [run_const(fn.__name__ + ' vector', fn, self.colour.cast(y), + [1, 2, 3]) + for y in fmt] + + # run a function on an image, + # 50,50 and 10,10 should have different values on the test image + def run_imageunary(self, message, im, fn): + run_cmp(message, im, 50, 50, lambda x: run_fn(fn, x)) + run_cmp(message, im, 10, 10, lambda x: run_fn(fn, x)) + + def run_unary(self, images, fn, fmt=all_formats): + [self.run_imageunary(fn.__name__ + ' image', x.cast(y), fn) + for x in images for y in fmt] + + @classmethod + def setup_class(cls): + im = pyvips.Image.mask_ideal(100, 100, 0.5, + reject=True, optical=True) + cls.colour = im * [1, 2, 3] + [2, 3, 4] + cls.mono = cls.colour.extract_band(1) + cls.all_images = [cls.mono, cls.colour] + + # test all operator overloads we define + + def test_add(self): + def add(x, y): + return x + y + + self.run_arith_const(add) + self.run_arith(add) + + def test_sub(self): + def sub(x, y): + return x - y + + self.run_arith_const(sub) + self.run_arith(sub) + + def test_mul(self): + def mul(x, y): + return x * y + + self.run_arith_const(mul) + self.run_arith(mul) + + def test_div(self): + def div(x, y): + return x / y + + # (const / image) needs (image ** -1), which won't work for complex + self.run_arith_const(div, fmt=noncomplex_formats) + self.run_arith(div) + + def test_floordiv(self): + def my_floordiv(x, y): + return x // y + + # (const // image) needs (image ** -1), which won't work for complex + self.run_arith_const(my_floordiv, fmt=noncomplex_formats) + self.run_arith(my_floordiv, fmt=noncomplex_formats) + + def test_pow(self): + def my_pow(x, y): + return x ** y + + # (image ** x) won't work for complex images ... just test non-complex + self.run_arith_const(my_pow, fmt=noncomplex_formats) + self.run_arith(my_pow, fmt=noncomplex_formats) + + def test_and(self): + def my_and(x, y): + # python doesn't allow bools on float + if isinstance(x, float): + x = int(x) + if isinstance(y, float): + y = int(y) + return x & y + + self.run_arith_const(my_and, fmt=noncomplex_formats) + self.run_arith(my_and, fmt=noncomplex_formats) + + def test_or(self): + def my_or(x, y): + # python doesn't allow bools on float + if isinstance(x, float): + x = int(x) + if isinstance(y, float): + y = int(y) + return x | y + + self.run_arith_const(my_or, fmt=noncomplex_formats) + self.run_arith(my_or, fmt=noncomplex_formats) + + def test_xor(self): + def my_xor(x, y): + # python doesn't allow bools on float + if isinstance(x, float): + x = int(x) + if isinstance(y, float): + y = int(y) + return x ^ y + + self.run_arith_const(my_xor, fmt=noncomplex_formats) + self.run_arith(my_xor, fmt=noncomplex_formats) + + def test_more(self): + def more(x, y): + if isinstance(x, pyvips.Image) or isinstance(y, pyvips.Image): + return x > y + else: + if x > y: + return 255 + else: + return 0 + + self.run_arith_const(more) + self.run_arith(more) + + def test_moreeq(self): + def moreeq(x, y): + if isinstance(x, pyvips.Image) or isinstance(y, pyvips.Image): + return x >= y + else: + if x >= y: + return 255 + else: + return 0 + + self.run_arith_const(moreeq) + self.run_arith(moreeq) + + def test_less(self): + def less(x, y): + if isinstance(x, pyvips.Image) or isinstance(y, pyvips.Image): + return x < y + else: + if x < y: + return 255 + else: + return 0 + + self.run_arith_const(less) + self.run_arith(less) + + def test_lesseq(self): + def lesseq(x, y): + if isinstance(x, pyvips.Image) or isinstance(y, pyvips.Image): + return x <= y + else: + if x <= y: + return 255 + else: + return 0 + + self.run_arith_const(lesseq) + self.run_arith(lesseq) + + def test_equal(self): + def equal(x, y): + if isinstance(x, pyvips.Image) or isinstance(y, pyvips.Image): + return x == y + else: + if x == y: + return 255 + else: + return 0 + + self.run_arith_const(equal) + self.run_arith(equal) + + def test_noteq(self): + def noteq(x, y): + if isinstance(x, pyvips.Image) or isinstance(y, pyvips.Image): + return x != y + else: + if x != y: + return 255 + else: + return 0 + + self.run_arith_const(noteq) + self.run_arith(noteq) + + def test_abs(self): + def my_abs(x): + return abs(x) + + im = -self.colour + self.run_unary([im], my_abs) + + def test_lshift(self): + def my_lshift(x): + # python doesn't allow float << int + if isinstance(x, float): + x = int(x) + return x << 2 + + # we don't support constant << image, treat as a unary + self.run_unary(self.all_images, my_lshift, fmt=noncomplex_formats) + + def test_rshift(self): + def my_rshift(x): + # python doesn't allow float >> int + if isinstance(x, float): + x = int(x) + return x >> 2 + + # we don't support constant >> image, treat as a unary + self.run_unary(self.all_images, my_rshift, fmt=noncomplex_formats) + + def test_mod(self): + def my_mod(x): + return x % 2 + + # we don't support constant % image, treat as a unary + self.run_unary(self.all_images, my_mod, fmt=noncomplex_formats) + + def test_pos(self): + def my_pos(x): + return +x + + self.run_unary(self.all_images, my_pos) + + def test_neg(self): + def my_neg(x): + return -x + + self.run_unary(self.all_images, my_neg) + + def test_invert(self): + def my_invert(x): + if isinstance(x, float): + x = int(x) + return ~x & 0xff + + # ~image is trimmed to image max so it's hard to test for all formats + # just test uchar + self.run_unary(self.all_images, my_invert, + fmt=[pyvips.BandFormat.UCHAR]) + + # test the rest of VipsArithmetic + + def test_avg(self): + im = pyvips.Image.black(50, 100) + test = im.insert(im + 100, 50, 0, expand=True) + + for fmt in all_formats: + assert pytest.approx(test.cast(fmt).avg()) == 50 + + def test_deviate(self): + im = pyvips.Image.black(50, 100) + test = im.insert(im + 100, 50, 0, expand=True) + + for fmt in noncomplex_formats: + assert pytest.approx(test.cast(fmt).deviate(), abs=0.01) == 50 + + def test_polar(self): + im = pyvips.Image.black(100, 100) + 100 + im = im.complexform(im) + + im = im.polar() + + assert pytest.approx(im.real().avg()) == 100 * 2 ** 0.5 + assert pytest.approx(im.imag().avg()) == 45 + + def test_rect(self): + im = pyvips.Image.black(100, 100) + im = (im + 100 * 2 ** 0.5).complexform(im + 45) + + im = im.rect() + + assert pytest.approx(im.real().avg()) == 100 + assert pytest.approx(im.imag().avg()) == 100 + + def test_conjugate(self): + im = pyvips.Image.black(100, 100) + 100 + im = im.complexform(im) + + im = im.conj() + + assert pytest.approx(im.real().avg()) == 100 + assert pytest.approx(im.imag().avg()) == -100 + + def test_histfind(self): + im = pyvips.Image.black(50, 100) + test = im.insert(im + 10, 50, 0, expand=True) + + for fmt in all_formats: + hist = test.cast(fmt).hist_find() + assert_almost_equal_objects(hist(0, 0), [5000]) + assert_almost_equal_objects(hist(10, 0), [5000]) + assert_almost_equal_objects(hist(5, 0), [0]) + + test = test * [1, 2, 3] + + for fmt in all_formats: + hist = test.cast(fmt).hist_find(band=0) + assert_almost_equal_objects(hist(0, 0), [5000]) + assert_almost_equal_objects(hist(10, 0), [5000]) + assert_almost_equal_objects(hist(5, 0), [0]) + + hist = test.cast(fmt).hist_find(band=1) + assert_almost_equal_objects(hist(0, 0), [5000]) + assert_almost_equal_objects(hist(20, 0), [5000]) + assert_almost_equal_objects(hist(5, 0), [0]) + + def test_histfind_indexed(self): + im = pyvips.Image.black(50, 100) + test = im.insert(im + 10, 50, 0, expand=True) + index = test // 10 + + for x in noncomplex_formats: + for y in [pyvips.BandFormat.UCHAR, pyvips.BandFormat.USHORT]: + a = test.cast(x) + b = index.cast(y) + hist = a.hist_find_indexed(b) + + assert_almost_equal_objects(hist(0, 0), [0]) + assert_almost_equal_objects(hist(1, 0), [50000]) + + def test_histfind_ndim(self): + im = pyvips.Image.black(100, 100) + [1, 2, 3] + + for fmt in noncomplex_formats: + hist = im.cast(fmt).hist_find_ndim() + + assert_almost_equal_objects(hist(0, 0)[0], 10000) + assert_almost_equal_objects(hist(5, 5)[5], 0) + + hist = im.cast(fmt).hist_find_ndim(bins=1) + + assert_almost_equal_objects(hist(0, 0)[0], 10000) + assert hist.width == 1 + assert hist.height == 1 + assert hist.bands == 1 + + def test_hough_circle(self): + test = pyvips.Image.black(100, 100).draw_circle(100, 50, 50, 40) + + for fmt in all_formats: + im = test.cast(fmt) + hough = im.hough_circle(min_radius=35, max_radius=45) + + v, x, y = hough.maxpos() + vec = hough(x, y) + r = vec.index(v) + 35 + + assert pytest.approx(x) == 50 + assert pytest.approx(y) == 50 + assert pytest.approx(r) == 40 + + @pytest.mark.skipif(not pyvips.base.at_least_libvips(8, 7), + reason="requires libvips >= 8.7") + def test_hough_line(self): + # hough_line changed the way it codes parameter space in 8.7 ... don't + # test earlier versions + test = pyvips.Image.black(100, 100).draw_line(100, 10, 90, 90, 10) + + for fmt in all_formats: + im = test.cast(fmt) + hough = im.hough_line() + + v, x, y = hough.maxpos() + + angle = 180.0 * x // hough.width + distance = test.height * y // hough.height + + assert pytest.approx(angle) == 45 + assert pytest.approx(distance) == 70 + + def test_sin(self): + def my_sin(x): + if isinstance(x, pyvips.Image): + return x.sin() + else: + return math.sin(math.radians(x)) + + self.run_unary(self.all_images, my_sin, fmt=noncomplex_formats) + + def test_cos(self): + def my_cos(x): + if isinstance(x, pyvips.Image): + return x.cos() + else: + return math.cos(math.radians(x)) + + self.run_unary(self.all_images, my_cos, fmt=noncomplex_formats) + + def test_tan(self): + def my_tan(x): + if isinstance(x, pyvips.Image): + return x.tan() + else: + return math.tan(math.radians(x)) + + self.run_unary(self.all_images, my_tan, fmt=noncomplex_formats) + + def test_asin(self): + def my_asin(x): + if isinstance(x, pyvips.Image): + return x.asin() + else: + return math.degrees(math.asin(x)) + + im = (pyvips.Image.black(100, 100) + [1, 2, 3]) / 3.0 + self.run_unary([im], my_asin, fmt=noncomplex_formats) + + def test_acos(self): + def my_acos(x): + if isinstance(x, pyvips.Image): + return x.acos() + else: + return math.degrees(math.acos(x)) + + im = (pyvips.Image.black(100, 100) + [1, 2, 3]) / 3.0 + self.run_unary([im], my_acos, fmt=noncomplex_formats) + + def test_atan(self): + def my_atan(x): + if isinstance(x, pyvips.Image): + return x.atan() + else: + return math.degrees(math.atan(x)) + + im = (pyvips.Image.black(100, 100) + [1, 2, 3]) / 3.0 + self.run_unary([im], my_atan, fmt=noncomplex_formats) + + def test_log(self): + def my_log(x): + if isinstance(x, pyvips.Image): + return x.log() + else: + return math.log(x) + + self.run_unary(self.all_images, my_log, fmt=noncomplex_formats) + + def test_log10(self): + def my_log10(x): + if isinstance(x, pyvips.Image): + return x.log10() + else: + return math.log10(x) + + self.run_unary(self.all_images, my_log10, fmt=noncomplex_formats) + + def test_exp(self): + def my_exp(x): + if isinstance(x, pyvips.Image): + return x.exp() + else: + return math.exp(x) + + self.run_unary(self.all_images, my_exp, fmt=noncomplex_formats) + + def test_exp10(self): + def my_exp10(x): + if isinstance(x, pyvips.Image): + return x.exp10() + else: + return math.pow(10, x) + + self.run_unary(self.all_images, my_exp10, fmt=noncomplex_formats) + + def test_floor(self): + def my_floor(x): + if isinstance(x, pyvips.Image): + return x.floor() + else: + return math.floor(x) + + self.run_unary(self.all_images, my_floor) + + def test_ceil(self): + def my_ceil(x): + if isinstance(x, pyvips.Image): + return x.ceil() + else: + return math.ceil(x) + + self.run_unary(self.all_images, my_ceil) + + def test_rint(self): + def my_rint(x): + if isinstance(x, pyvips.Image): + return x.rint() + else: + return round(x) + + self.run_unary(self.all_images, my_rint) + + def test_sign(self): + def my_sign(x): + if isinstance(x, pyvips.Image): + return x.sign() + else: + if x > 0: + return 1 + elif x < 0: + return -1 + else: + return 0 + + self.run_unary(self.all_images, my_sign) + + def test_max(self): + test = pyvips.Image.black(100, 100).draw_rect(100, 40, 50, 1, 1) + + for fmt in all_formats: + v = test.cast(fmt).max() + + assert pytest.approx(v) == 100 + v, x, y = test.cast(fmt).maxpos() + assert pytest.approx(v) == 100 + assert pytest.approx(x) == 40 + assert pytest.approx(y) == 50 + + def test_min(self): + test = (pyvips.Image.black(100, 100) + 100).draw_rect(0, 40, 50, 1, 1) + + for fmt in all_formats: + v = test.cast(fmt).min() + + assert pytest.approx(v) == 0 + v, x, y = test.cast(fmt).minpos() + assert pytest.approx(v) == 0 + assert pytest.approx(x) == 40 + assert pytest.approx(y) == 50 + + def test_measure(self): + im = pyvips.Image.black(50, 50) + test = im.insert(im + 10, 50, 0, expand=True) + + for x in noncomplex_formats: + a = test.cast(x) + matrix = a.measure(2, 1) + [p1] = matrix(0, 0) + [p2] = matrix(0, 1) + + assert pytest.approx(p1) == 0 + assert pytest.approx(p2) == 10 + + def test_find_trim(self): + if pyvips.type_find("VipsOperation", "find_trim") != 0: + im = pyvips.Image.black(50, 60) + 100 + test = im.embed(10, 20, 200, 300, extend="white") + + for x in unsigned_formats + float_formats: + a = test.cast(x) + left, top, width, height = a.find_trim() + + assert left == 10 + assert top == 20 + assert width == 50 + assert height == 60 + + test_rgb = test.bandjoin([test, test]) + left, top, width, height = test_rgb.find_trim(background=[255, 255, + 255]) + assert left == 10 + assert top == 20 + assert width == 50 + assert height == 60 + + def test_profile(self): + test = pyvips.Image.black(100, 100).draw_rect(100, 40, 50, 1, 1) + + for fmt in noncomplex_formats: + columns, rows = test.cast(fmt).profile() + + v, x, y = columns.minpos() + assert pytest.approx(v) == 50 + assert pytest.approx(x) == 40 + assert pytest.approx(y) == 0 + + v, x, y = rows.minpos() + assert pytest.approx(v) == 40 + assert pytest.approx(x) == 0 + assert pytest.approx(y) == 50 + + def test_project(self): + im = pyvips.Image.black(50, 50) + test = im.insert(im + 10, 50, 0, expand=True) + + for fmt in noncomplex_formats: + columns, rows = test.cast(fmt).project() + + assert_almost_equal_objects(columns(10, 0), [0]) + assert_almost_equal_objects(columns(70, 0), [50 * 10]) + + assert_almost_equal_objects(rows(0, 10), [50 * 10]) + + def test_stats(self): + im = pyvips.Image.black(50, 50) + test = im.insert(im + 10, 50, 0, expand=True) + + for x in noncomplex_formats: + a = test.cast(x) + matrix = a.stats() + + assert_almost_equal_objects(matrix(0, 0), [a.min()]) + assert_almost_equal_objects(matrix(1, 0), [a.max()]) + assert_almost_equal_objects(matrix(2, 0), [50 * 50 * 10]) + assert_almost_equal_objects(matrix(3, 0), [50 * 50 * 100]) + assert_almost_equal_objects(matrix(4, 0), [a.avg()]) + assert_almost_equal_objects(matrix(5, 0), [a.deviate()]) + + assert_almost_equal_objects(matrix(0, 1), [a.min()]) + assert_almost_equal_objects(matrix(1, 1), [a.max()]) + assert_almost_equal_objects(matrix(2, 1), [50 * 50 * 10]) + assert_almost_equal_objects(matrix(3, 1), [50 * 50 * 100]) + assert_almost_equal_objects(matrix(4, 1), [a.avg()]) + assert_almost_equal_objects(matrix(5, 1), [a.deviate()]) + + def test_sum(self): + for fmt in all_formats: + im = pyvips.Image.black(50, 50) + im2 = [(im + x).cast(fmt) for x in range(0, 100, 10)] + im3 = pyvips.Image.sum(im2) + assert pytest.approx(im3.max()) == sum(range(0, 100, 10)) + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_colour.py b/test/test-suite/test_colour.py new file mode 100644 index 00000000..b9b5a1bc --- /dev/null +++ b/test/test-suite/test_colour.py @@ -0,0 +1,155 @@ +# vim: set fileencoding=utf-8 : +import pytest + +import pyvips +from helpers import JPEG_FILE, SRGB_FILE, colour_colourspaces, \ + mono_colourspaces, assert_almost_equal_objects + + +class TestColour: + def test_colourspace(self): + # mid-grey in Lab ... put 42 in the extra band, it should be copied + # unmodified + test = pyvips.Image.black(100, 100) + [50, 0, 0, 42] + test = test.copy(interpretation=pyvips.Interpretation.LAB) + + # a long series should come in a circle + im = test + for col in colour_colourspaces + [pyvips.Interpretation.LAB]: + im = im.colourspace(col) + assert im.interpretation == col + + for i in range(0, 4): + min_l = im.extract_band(i).min() + max_h = im.extract_band(i).max() + assert pytest.approx(min_l) == max_h + + pixel = im(10, 10) + assert pytest.approx(pixel[3], 0.01) == 42 + + # alpha won't be equal for RGB16, but it should be preserved if we go + # there and back + im = im.colourspace(pyvips.Interpretation.RGB16) + im = im.colourspace(pyvips.Interpretation.LAB) + + before = test(10, 10) + after = im(10, 10) + assert_almost_equal_objects(before, after, threshold=0.1) + + # go between every pair of colour spaces + for start in colour_colourspaces: + for end in colour_colourspaces: + im = test.colourspace(start) + im2 = im.colourspace(end) + im3 = im2.colourspace(pyvips.Interpretation.LAB) + + before = test(10, 10) + after = im3(10, 10) + + assert_almost_equal_objects(before, after, threshold=0.1) + + # test Lab->XYZ on mid-grey + # checked against http://www.brucelindbloom.com + im = test.colourspace(pyvips.Interpretation.XYZ) + after = im(10, 10) + assert_almost_equal_objects(after, [17.5064, 18.4187, 20.0547, 42]) + + # grey->colour->grey should be equal + for mono_fmt in mono_colourspaces: + test_grey = test.colourspace(mono_fmt) + im = test_grey + for col in colour_colourspaces + [mono_fmt]: + im = im.colourspace(col) + assert im.interpretation == col + [before, alpha_before] = test_grey(10, 10) + [after, alpha_after] = im(10, 10) + assert abs(alpha_after - alpha_before) < 1 + if mono_fmt == pyvips.Interpretation.GREY16: + # GREY16 can wind up rather different due to rounding + assert abs(after - before) < 30 + else: + # but 8-bit we should hit exactly + assert abs(after - before) < 1 + + # test results from Bruce Lindbloom's calculator: + # http://www.brucelindbloom.com + def test_dE00(self): + # put 42 in the extra band, it should be copied unmodified + reference = pyvips.Image.black(100, 100) + [50, 10, 20, 42] + reference = reference.copy(interpretation=pyvips.Interpretation.LAB) + sample = pyvips.Image.black(100, 100) + [40, -20, 10] + sample = sample.copy(interpretation=pyvips.Interpretation.LAB) + + difference = reference.dE00(sample) + result, alpha = difference(10, 10) + assert pytest.approx(result, 0.001) == 30.238 + assert pytest.approx(alpha, 0.001) == 42.0 + + def test_dE76(self): + # put 42 in the extra band, it should be copied unmodified + reference = pyvips.Image.black(100, 100) + [50, 10, 20, 42] + reference = reference.copy(interpretation=pyvips.Interpretation.LAB) + sample = pyvips.Image.black(100, 100) + [40, -20, 10] + sample = sample.copy(interpretation=pyvips.Interpretation.LAB) + + difference = reference.dE76(sample) + result, alpha = difference(10, 10) + assert pytest.approx(result, 0.001) == 33.166 + assert pytest.approx(alpha, 0.001) == 42.0 + + # the vips CMC calculation is based on distance in a colorspace + # derived from the CMC formula, so it won't match exactly ... + # see vips_LCh2CMC() for details + def test_dECMC(self): + reference = pyvips.Image.black(100, 100) + [50, 10, 20, 42] + reference = reference.copy(interpretation=pyvips.Interpretation.LAB) + sample = pyvips.Image.black(100, 100) + [55, 11, 23] + sample = sample.copy(interpretation=pyvips.Interpretation.LAB) + + difference = reference.dECMC(sample) + result, alpha = difference(10, 10) + assert abs(result - 4.97) < 0.5 + assert pytest.approx(alpha, 0.001) == 42.0 + + def test_icc(self): + test = pyvips.Image.new_from_file(JPEG_FILE) + + im = test.icc_import().icc_export() + assert im.dE76(test).max() < 6 + + im = test.icc_import() + im2 = im.icc_export(depth=16) + assert im2.format == pyvips.BandFormat.USHORT + im3 = im2.icc_import() + assert (im - im3).abs().max() < 3 + + im = test.icc_import(intent=pyvips.Intent.ABSOLUTE) + im2 = im.icc_export(intent=pyvips.Intent.ABSOLUTE) + assert im2.dE76(test).max() < 6 + + im = test.icc_import() + im2 = im.icc_export(output_profile=SRGB_FILE) + im3 = im.colourspace(pyvips.Interpretation.SRGB) + assert im2.dE76(im3).max() < 6 + + before_profile = test.get("icc-profile-data") + im = test.icc_transform(SRGB_FILE) + after_profile = im.get("icc-profile-data") + im2 = test.icc_import() + im3 = im2.colourspace(pyvips.Interpretation.SRGB) + assert im.dE76(im3).max() < 6 + assert len(before_profile) != len(after_profile) + + im = test.icc_import(input_profile=SRGB_FILE) + im2 = test.icc_import() + assert 6 < im.dE76(im2).max() + + im = test.icc_import(pcs=pyvips.PCS.XYZ) + assert im.interpretation == pyvips.Interpretation.XYZ + + im = test.icc_import() + assert im.interpretation == pyvips.Interpretation.LAB + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_conversion.py b/test/test-suite/test_conversion.py new file mode 100644 index 00000000..73409ee3 --- /dev/null +++ b/test/test-suite/test_conversion.py @@ -0,0 +1,767 @@ +# vim: set fileencoding=utf-8 : +from functools import reduce +import pytest + +import pyvips +from helpers import JPEG_FILE, unsigned_formats, \ + signed_formats, float_formats, int_formats, \ + noncomplex_formats, all_formats, max_value, \ + sizeof_format, rot45_angles, rot45_angle_bonds, \ + rot_angles, rot_angle_bonds, run_cmp, run_cmp2, \ + assert_almost_equal_objects + + +class TestConversion: + # run a function on an image, + # 50,50 and 10,10 should have different values on the test image + # don't loop over band elements + def run_image_pixels(self, message, im, fn): + run_cmp(message, im, 50, 50, fn) + run_cmp(message, im, 10, 10, fn) + + # run a function on a pair of images + # 50,50 and 10,10 should have different values on the test image + # don't loop over band elements + def run_image_pixels2(self, message, left, right, fn): + run_cmp2(message, left, right, 50, 50, fn) + run_cmp2(message, left, right, 10, 10, fn) + + def run_unary(self, images, fn, fmt=all_formats): + [self.run_image_pixels(fn.__name__ + (' %s' % y), x.cast(y), fn) + for x in images for y in fmt] + + def run_binary(self, images, fn, fmt=all_formats): + [self.run_image_pixels2(fn.__name__ + (' %s %s' % (y, z)), + x.cast(y), x.cast(z), fn) + for x in images for y in fmt for z in fmt] + + @classmethod + def setup_class(cls): + im = pyvips.Image.mask_ideal(100, 100, 0.5, + reject=True, optical=True) + cls.colour = (im * [1, 2, 3] + [2, 3, 4]).copy(interpretation="srgb") + cls.mono = cls.colour[1].copy(interpretation="b-w") + cls.all_images = [cls.mono, cls.colour] + cls.image = pyvips.Image.jpegload(JPEG_FILE) + + def test_band_and(self): + def band_and(x): + if isinstance(x, pyvips.Image): + return x.bandand() + else: + return [reduce(lambda a, b: int(a) & int(b), x)] + + self.run_unary(self.all_images, band_and, fmt=int_formats) + + def test_band_or(self): + def band_or(x): + if isinstance(x, pyvips.Image): + return x.bandor() + else: + return [reduce(lambda a, b: int(a) | int(b), x)] + + self.run_unary(self.all_images, band_or, fmt=int_formats) + + def test_band_eor(self): + def band_eor(x): + if isinstance(x, pyvips.Image): + return x.bandeor() + else: + return [reduce(lambda a, b: int(a) ^ int(b), x)] + + self.run_unary(self.all_images, band_eor, fmt=int_formats) + + def test_bandjoin(self): + def bandjoin(x, y): + if isinstance(x, pyvips.Image) and isinstance(y, pyvips.Image): + return x.bandjoin(y) + else: + return x + y + + self.run_binary(self.all_images, bandjoin) + + def test_bandjoin_const(self): + x = self.colour.bandjoin(1) + assert x.bands == 4 + assert x[3].avg() == 1 + + x = self.colour.bandjoin([1, 2]) + assert x.bands == 5 + assert x[3].avg() == 1 + assert x[4].avg() == 2 + + def test_bandmean(self): + def bandmean(x): + if isinstance(x, pyvips.Image): + return x.bandmean() + else: + return [sum(x) // len(x)] + + self.run_unary(self.all_images, bandmean, fmt=noncomplex_formats) + + def test_bandrank(self): + def median(x, y): + joined = [[a, b] for a, b in zip(x, y)] + # .sort() isn't a function, so we have to run this as a separate + # pass + [z.sort() for z in joined] + return [z[len(z) // 2] for z in joined] + + def bandrank(x, y): + if isinstance(x, pyvips.Image) and isinstance(y, pyvips.Image): + return x.bandrank([y]) + else: + return median(x, y) + + self.run_binary(self.all_images, bandrank, fmt=noncomplex_formats) + + # we can mix images and constants, and set the index arg + a = self.mono.bandrank([2], index=0) + b = (self.mono < 2).ifthenelse(self.mono, 2) + assert (a - b).abs().min() == 0 + + def test_cache(self): + def cache(x): + if isinstance(x, pyvips.Image): + return x.cache() + else: + return x + + self.run_unary(self.all_images, cache) + + def test_copy(self): + x = self.colour.copy(interpretation=pyvips.Interpretation.LAB) + assert x.interpretation == pyvips.Interpretation.LAB + x = self.colour.copy(xres=42) + assert x.xres == 42 + x = self.colour.copy(yres=42) + assert x.yres == 42 + x = self.colour.copy(xoffset=42) + assert x.xoffset == 42 + x = self.colour.copy(yoffset=42) + assert x.yoffset == 42 + x = self.colour.copy(coding=pyvips.Coding.NONE) + assert x.coding == pyvips.Coding.NONE + + def test_bandfold(self): + x = self.mono.bandfold() + assert x.width == 1 + assert x.bands == self.mono.width + + y = x.bandunfold() + assert y.width == self.mono.width + assert y.bands == 1 + assert x.avg() == y.avg() + + x = self.mono.bandfold(factor=2) + assert x.width == self.mono.width / 2 + assert x.bands == 2 + + y = x.bandunfold(factor=2) + assert y.width == self.mono.width + assert y.bands == 1 + assert x.avg() == y.avg() + + def test_byteswap(self): + x = self.mono.cast("ushort") + y = x.byteswap().byteswap() + assert x.width == y.width + assert x.height == y.height + assert x.bands == y.bands + assert x.avg() == y.avg() + + def test_embed(self): + for fmt in all_formats: + test = self.colour.cast(fmt) + + im = test.embed(20, 20, + self.colour.width + 40, + self.colour.height + 40) + pixel = im(10, 10) + assert_almost_equal_objects(pixel, [0, 0, 0]) + pixel = im(30, 30) + assert_almost_equal_objects(pixel, [2, 3, 4]) + pixel = im(im.width - 10, im.height - 10) + assert_almost_equal_objects(pixel, [0, 0, 0]) + + im = test.embed(20, 20, + self.colour.width + 40, + self.colour.height + 40, + extend=pyvips.Extend.COPY) + pixel = im(10, 10) + assert_almost_equal_objects(pixel, [2, 3, 4]) + pixel = im(im.width - 10, im.height - 10) + assert_almost_equal_objects(pixel, [2, 3, 4]) + + im = test.embed(20, 20, + self.colour.width + 40, + self.colour.height + 40, + extend=pyvips.Extend.BACKGROUND, + background=[7, 8, 9]) + pixel = im(10, 10) + assert_almost_equal_objects(pixel, [7, 8, 9]) + pixel = im(im.width - 10, im.height - 10) + assert_almost_equal_objects(pixel, [7, 8, 9]) + + im = test.embed(20, 20, + self.colour.width + 40, + self.colour.height + 40, + extend=pyvips.Extend.WHITE) + pixel = im(10, 10) + # uses 255 in all bytes of ints, 255.0 for float + pixel = [int(x) & 0xff for x in pixel] + assert_almost_equal_objects(pixel, [255, 255, 255]) + pixel = im(im.width - 10, im.height - 10) + pixel = [int(x) & 0xff for x in pixel] + assert_almost_equal_objects(pixel, [255, 255, 255]) + + @pytest.mark.skipif(pyvips.type_find("VipsOperation", "gravity") == 0, + reason="no gravity in this vips, skipping test") + def test_gravity(self): + im = pyvips.Image.black(1, 1) + 255 + + positions = [ + ['centre', 1, 1], + ['north', 1, 0], + ['south', 1, 2], + ['east', 2, 1], + ['west', 0, 1], + ['north-east', 2, 0], + ['south-east', 2, 2], + ['south-west', 0, 2], + ['north-west', 0, 0] + ] + + for direction, x, y in positions: + im2 = im.gravity(direction, 3, 3) + assert_almost_equal_objects(im2(x, y), [255]) + assert_almost_equal_objects(im2.avg(), 255.0 / 9.0) + + def test_extract(self): + for fmt in all_formats: + test = self.colour.cast(fmt) + + pixel = test(30, 30) + assert_almost_equal_objects(pixel, [2, 3, 4]) + + sub = test.extract_area(25, 25, 10, 10) + + pixel = sub(5, 5) + assert_almost_equal_objects(pixel, [2, 3, 4]) + + sub = test.extract_band(1, n=2) + + pixel = sub(30, 30) + assert_almost_equal_objects(pixel, [3, 4]) + + def test_slice(self): + test = self.colour + bands = [x.avg() for x in test] + + x = test[0].avg() + assert x == bands[0] + + x = test[-1].avg() + assert_almost_equal_objects(x, bands[2]) + + x = [i.avg() for i in test[1:3]] + assert_almost_equal_objects(x, bands[1:3]) + + x = [i.avg() for i in test[1:-1]] + assert_almost_equal_objects(x, bands[1:-1]) + + x = [i.avg() for i in test[:2]] + assert_almost_equal_objects(x, bands[:2]) + + x = [i.avg() for i in test[1:]] + assert_almost_equal_objects(x, bands[1:]) + + x = [i.avg() for i in test[-1]] + assert_almost_equal_objects(x, bands[-1]) + + def test_crop(self): + for fmt in all_formats: + test = self.colour.cast(fmt) + + pixel = test(30, 30) + assert_almost_equal_objects(pixel, [2, 3, 4]) + + sub = test.crop(25, 25, 10, 10) + + pixel = sub(5, 5) + assert_almost_equal_objects(pixel, [2, 3, 4]) + + @pytest.mark.skipif(pyvips.type_find("VipsOperation", "smartcrop") == 0, + reason="no smartcrop, skipping test") + def test_smartcrop(self): + test = self.image.smartcrop(100, 100) + assert test.width == 100 + assert test.height == 100 + + def test_falsecolour(self): + for fmt in all_formats: + test = self.colour.cast(fmt) + + im = test.falsecolour() + + assert im.width == test.width + assert im.height == test.height + assert im.bands == 3 + + pixel = im(30, 30) + assert_almost_equal_objects(pixel, [20, 0, 41]) + + def test_flatten(self): + for fmt in unsigned_formats + [pyvips.BandFormat.SHORT, + pyvips.BandFormat.INT] + float_formats: + mx = 255 + alpha = mx / 2.0 + nalpha = mx - alpha + test = self.colour.bandjoin(alpha).cast(fmt) + pixel = test(30, 30) + + predict = [int(x) * alpha / mx for x in pixel[:-1]] + + im = test.flatten() + + assert im.bands == 3 + pixel = im(30, 30) + for x, y in zip(pixel, predict): + # we use float arithetic for int and uint, so the rounding + # differs ... don't require huge accuracy + assert abs(x - y) < 2 + + im = test.flatten(background=[100, 100, 100]) + + pixel = test(30, 30) + predict = [int(x) * alpha / mx + (100 * nalpha) / mx + for x in pixel[:-1]] + + assert im.bands == 3 + pixel = im(30, 30) + for x, y in zip(pixel, predict): + assert abs(x - y) < 2 + + def test_premultiply(self): + for fmt in unsigned_formats + [pyvips.BandFormat.SHORT, + pyvips.BandFormat.INT] + float_formats: + mx = 255 + alpha = mx / 2.0 + test = self.colour.bandjoin(alpha).cast(fmt) + pixel = test(30, 30) + + predict = [int(x) * alpha / mx for x in pixel[:-1]] + [alpha] + + im = test.premultiply() + + assert im.bands == test.bands + pixel = im(30, 30) + for x, y in zip(pixel, predict): + # we use float arithetic for int and uint, so the rounding + # differs ... don't require huge accuracy + assert abs(x - y) < 2 + + @pytest.mark.skipif(pyvips.type_find("VipsConversion", "composite") == 0, + reason="no composite support, skipping test") + def test_composite(self): + # 50% transparent image + overlay = self.colour.bandjoin(128) + base = self.colour + 100 + comp = base.composite(overlay, "over") + + assert_almost_equal_objects(comp(0, 0), [51.8, 52.8, 53.8, 255], + threshold=0.1) + + def test_unpremultiply(self): + for fmt in unsigned_formats + [pyvips.BandFormat.SHORT, + pyvips.BandFormat.INT] + float_formats: + mx = 255 + alpha = mx / 2.0 + test = self.colour.bandjoin(alpha).cast(fmt) + pixel = test(30, 30) + + predict = [int(x) / (alpha / mx) for x in pixel[:-1]] + [alpha] + + im = test.unpremultiply() + + assert im.bands == test.bands + pixel = im(30, 30) + for x, y in zip(pixel, predict): + # we use float arithetic for int and uint, so the rounding + # differs ... don't require huge accuracy + assert abs(x - y) < 2 + + def test_flip(self): + for fmt in all_formats: + test = self.colour.cast(fmt) + + result = test.fliphor() + result = result.flipver() + result = result.fliphor() + result = result.flipver() + + diff = (test - result).abs().max() + + assert diff == 0 + + def test_gamma(self): + exponent = 2.4 + for fmt in noncomplex_formats: + mx = max_value[fmt] + test = (self.colour + mx / 2.0).cast(fmt) + + norm = mx ** exponent / mx + result = test.gamma() + before = test(30, 30) + after = result(30, 30) + predict = [x ** exponent / norm for x in before] + for a, b in zip(after, predict): + # ie. less than 1% error, rounding on 7-bit images + # means this is all we can expect + assert abs(a - b) < mx / 100.0 + + exponent = 1.2 + for fmt in noncomplex_formats: + mx = max_value[fmt] + test = (self.colour + mx / 2.0).cast(fmt) + + norm = mx ** exponent / mx + result = test.gamma(exponent=1.0 / 1.2) + before = test(30, 30) + after = result(30, 30) + predict = [x ** exponent / norm for x in before] + for a, b in zip(after, predict): + # ie. less than 1% error, rounding on 7-bit images + # means this is all we can expect + assert abs(a - b) < mx / 100.0 + + def test_grid(self): + test = self.colour.replicate(1, 12) + assert test.width == self.colour.width + assert test.height == self.colour.height * 12 + + for fmt in all_formats: + im = test.cast(fmt) + result = im.grid(test.width, 3, 4) + assert result.width == self.colour.width * 3 + assert result.height == self.colour.height * 4 + + before = im(10, 10) + after = result(10 + test.width * 2, 10 + test.width * 2) + assert_almost_equal_objects(before, after) + + before = im(50, 50) + after = result(50 + test.width * 2, 50 + test.width * 2) + assert_almost_equal_objects(before, after) + + def test_ifthenelse(self): + test = self.mono > 3 + for x in all_formats: + for y in all_formats: + t = (self.colour + 10).cast(x) + e = self.colour.cast(y) + r = test.ifthenelse(t, e) + + assert r.width == self.colour.width + assert r.height == self.colour.height + assert r.bands == self.colour.bands + + predict = e(10, 10) + result = r(10, 10) + assert_almost_equal_objects(result, predict) + + predict = t(50, 50) + result = r(50, 50) + assert_almost_equal_objects(result, predict) + + test = self.colour > 3 + for x in all_formats: + for y in all_formats: + t = (self.mono + 10).cast(x) + e = self.mono.cast(y) + r = test.ifthenelse(t, e) + + assert r.width == self.colour.width + assert r.height == self.colour.height + assert r.bands == self.colour.bands + + cp = test(10, 10) + tp = t(10, 10) * 3 + ep = e(10, 10) * 3 + predict = [te if ce != 0 else ee + for ce, te, ee in zip(cp, tp, ep)] + result = r(10, 10) + assert_almost_equal_objects(result, predict) + + cp = test(50, 50) + tp = t(50, 50) * 3 + ep = e(50, 50) * 3 + predict = [te if ce != 0 else ee + for ce, te, ee in zip(cp, tp, ep)] + result = r(50, 50) + assert_almost_equal_objects(result, predict) + + test = self.colour > 3 + for x in all_formats: + for y in all_formats: + t = (self.mono + 10).cast(x) + e = self.mono.cast(y) + r = test.ifthenelse(t, e, blend=True) + + assert r.width == self.colour.width + assert r.height == self.colour.height + assert r.bands == self.colour.bands + + result = r(10, 10) + assert_almost_equal_objects(result, [3, 3, 13]) + + test = self.mono > 3 + r = test.ifthenelse([1, 2, 3], self.colour) + assert r.width == self.colour.width + assert r.height == self.colour.height + assert r.bands == self.colour.bands + assert r.format == self.colour.format + assert r.interpretation == self.colour.interpretation + result = r(10, 10) + assert_almost_equal_objects(result, [2, 3, 4]) + result = r(50, 50) + assert_almost_equal_objects(result, [1, 2, 3]) + + test = self.mono + r = test.ifthenelse([1, 2, 3], self.colour, blend=True) + assert r.width == self.colour.width + assert r.height == self.colour.height + assert r.bands == self.colour.bands + assert r.format == self.colour.format + assert r.interpretation == self.colour.interpretation + result = r(10, 10) + assert_almost_equal_objects(result, [2, 3, 4], threshold=0.1) + result = r(50, 50) + assert_almost_equal_objects(result, [3.0, 4.9, 6.9], threshold=0.1) + + def test_insert(self): + for x in all_formats: + for y in all_formats: + main = self.mono.cast(x) + sub = self.colour.cast(y) + r = main.insert(sub, 10, 10) + + assert r.width == main.width + assert r.height == main.height + assert r.bands == sub.bands + + a = r(10, 10) + b = sub(0, 0) + assert_almost_equal_objects(a, b) + + a = r(0, 0) + b = main(0, 0) * 3 + assert_almost_equal_objects(a, b) + + for x in all_formats: + for y in all_formats: + main = self.mono.cast(x) + sub = self.colour.cast(y) + r = main.insert(sub, 10, 10, expand=True, background=100) + + assert r.width == main.width + 10 + assert r.height == main.height + 10 + assert r.bands == sub.bands + + a = r(r.width - 5, 5) + assert_almost_equal_objects(a, [100, 100, 100]) + + def test_arrayjoin(self): + max_width = 0 + max_height = 0 + max_bands = 0 + for image in self.all_images: + if image.width > max_width: + max_width = image.width + if image.height > max_height: + max_height = image.height + if image.bands > max_bands: + max_bands = image.bands + + im = pyvips.Image.arrayjoin(self.all_images) + assert im.width == max_width * len(self.all_images) + assert im.height == max_height + assert im.bands == max_bands + + im = pyvips.Image.arrayjoin(self.all_images, across=1) + assert im.width == max_width + assert im.height == max_height * len(self.all_images) + assert im.bands == max_bands + + im = pyvips.Image.arrayjoin(self.all_images, shim=10) + assert im.width == max_width * len(self.all_images) + 10 * (len(self.all_images) - 1) # noqa: E501 + assert im.height == max_height + assert im.bands == max_bands + + def test_msb(self): + for fmt in unsigned_formats: + mx = max_value[fmt] + size = sizeof_format[fmt] + test = (self.colour + mx / 8.0).cast(fmt) + im = test.msb() + + before = test(10, 10) + predict = [int(x) >> ((size - 1) * 8) for x in before] + result = im(10, 10) + assert_almost_equal_objects(result, predict) + + before = test(50, 50) + predict = [int(x) >> ((size - 1) * 8) for x in before] + result = im(50, 50) + assert_almost_equal_objects(result, predict) + + for fmt in signed_formats: + mx = max_value[fmt] + size = sizeof_format[fmt] + test = (self.colour + mx / 8.0).cast(fmt) + im = test.msb() + + before = test(10, 10) + predict = [128 + (int(x) >> ((size - 1) * 8)) for x in before] + result = im(10, 10) + assert_almost_equal_objects(result, predict) + + before = test(50, 50) + predict = [128 + (int(x) >> ((size - 1) * 8)) for x in before] + result = im(50, 50) + assert_almost_equal_objects(result, predict) + + for fmt in unsigned_formats: + mx = max_value[fmt] + size = sizeof_format[fmt] + test = (self.colour + mx / 8.0).cast(fmt) + im = test.msb(band=1) + + before = [test(10, 10)[1]] + predict = [int(x) >> ((size - 1) * 8) for x in before] + result = im(10, 10) + assert_almost_equal_objects(result, predict) + + before = [test(50, 50)[1]] + predict = [int(x) >> ((size - 1) * 8) for x in before] + result = im(50, 50) + assert_almost_equal_objects(result, predict) + + def test_recomb(self): + array = [[0.2, 0.5, 0.3]] + + def recomb(x): + if isinstance(x, pyvips.Image): + return x.recomb(array) + else: + sum = 0 + for i, c in zip(array[0], x): + sum += i * c + return [sum] + + self.run_unary([self.colour], recomb, fmt=noncomplex_formats) + + def test_replicate(self): + for fmt in all_formats: + im = self.colour.cast(fmt) + + test = im.replicate(10, 10) + assert test.width == self.colour.width * 10 + assert test.height == self.colour.height * 10 + + before = im(10, 10) + after = test(10 + im.width * 2, 10 + im.width * 2) + assert_almost_equal_objects(before, after) + + before = im(50, 50) + after = test(50 + im.width * 2, 50 + im.width * 2) + assert_almost_equal_objects(before, after) + + def test_rot45(self): + # test has a quarter-circle in the bottom right + test = self.colour.crop(0, 0, 51, 51) + for fmt in all_formats: + im = test.cast(fmt) + + im2 = im.rot45() + before = im(50, 50) + after = im2(25, 50) + assert_almost_equal_objects(before, after) + + for a, b in zip(rot45_angles, rot45_angle_bonds): + im2 = im.rot45(angle=a) + after = im2.rot45(angle=b) + diff = (after - im).abs().max() + assert diff == 0 + + def test_rot(self): + # test has a quarter-circle in the bottom right + test = self.colour.crop(0, 0, 51, 51) + for fmt in all_formats: + im = test.cast(fmt) + + im2 = im.rot(pyvips.Angle.D90) + before = im(50, 50) + after = im2(0, 50) + assert_almost_equal_objects(before, after) + + for a, b in zip(rot_angles, rot_angle_bonds): + im2 = im.rot(a) + after = im2.rot(b) + diff = (after - im).abs().max() + assert diff == 0 + + def test_scaleimage(self): + for fmt in noncomplex_formats: + test = self.colour.cast(fmt) + + im = test.scaleimage() + assert im.max() == 255 + assert im.min() == 0 + + im = test.scaleimage(log=True) + assert im.max() == 255 + + def test_subsample(self): + for fmt in all_formats: + test = self.colour.cast(fmt) + + im = test.subsample(3, 3) + assert im.width == test.width // 3 + assert im.height == test.height // 3 + + before = test(60, 60) + after = im(20, 20) + assert_almost_equal_objects(before, after) + + def test_zoom(self): + for fmt in all_formats: + test = self.colour.cast(fmt) + + im = test.zoom(3, 3) + assert im.width == test.width * 3 + assert im.height == test.height * 3 + + before = test(50, 50) + after = im(150, 150) + assert_almost_equal_objects(before, after) + + def test_wrap(self): + for fmt in all_formats: + test = self.colour.cast(fmt) + + im = test.wrap() + assert im.width == test.width + assert im.height == test.height + + before = test(0, 0) + after = im(50, 50) + assert_almost_equal_objects(before, after) + + before = test(50, 50) + after = im(0, 0) + assert_almost_equal_objects(before, after) + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_convolution.py b/test/test-suite/test_convolution.py new file mode 100644 index 00000000..1778c5c2 --- /dev/null +++ b/test/test-suite/test_convolution.py @@ -0,0 +1,206 @@ +# vim: set fileencoding=utf-8 : +import operator +import pytest +from functools import reduce + +import pyvips +from helpers import noncomplex_formats, run_fn2, run_fn, \ + assert_almost_equal_objects, assert_less_threshold + + +# point convolution +def conv(image, mask, x_position, y_position): + s = 0.0 + for x in range(0, mask.width): + for y in range(0, mask.height): + m = mask(x, y) + i = image(x + x_position, y + y_position) + p = run_fn2(operator.mul, m, i) + s = run_fn2(operator.add, s, p) + + return run_fn2(operator.truediv, s, mask.scale) + + +def compass(image, mask, x_position, y_position, n_rot, fn): + acc = [] + for i in range(0, n_rot): + result = conv(image, mask, x_position, y_position) + result = run_fn(abs, result) + acc.append(result) + mask = mask.rot45() + + return reduce(lambda a, b: run_fn2(fn, a, b), acc) + + +class TestConvolution: + @classmethod + def setup_class(cls): + im = pyvips.Image.mask_ideal(100, 100, 0.5, reject=True, optical=True) + cls.colour = im * [1, 2, 3] + [2, 3, 4] + cls.colour = cls.colour.copy(interpretation=pyvips.Interpretation.SRGB) + cls.mono = cls.colour.extract_band(1) + cls.mono = cls.mono.copy(interpretation=pyvips.Interpretation.B_W) + cls.all_images = [cls.mono, cls.colour] + cls.sharp = pyvips.Image.new_from_array([[-1, -1, -1], + [-1, 16, -1], + [-1, -1, -1]], scale=8) + cls.blur = pyvips.Image.new_from_array([[1, 1, 1], + [1, 1, 1], + [1, 1, 1]], scale=9) + cls.line = pyvips.Image.new_from_array([[1, 1, 1], + [-2, -2, -2], + [1, 1, 1]]) + cls.sobel = pyvips.Image.new_from_array([[1, 2, 1], + [0, 0, 0], + [-1, -2, -1]]) + cls.all_masks = [cls.sharp, cls.blur, cls.line, cls.sobel] + + def test_conv(self): + for im in self.all_images: + for msk in self.all_masks: + for prec in [pyvips.Precision.INTEGER, pyvips.Precision.FLOAT]: + convolved = im.conv(msk, precision=prec) + + result = convolved(25, 50) + true = conv(im, msk, 24, 49) + assert_almost_equal_objects(result, true) + + result = convolved(50, 50) + true = conv(im, msk, 49, 49) + assert_almost_equal_objects(result, true) + + # don't test conva, it's still not done + def dont_est_conva(self): + for im in self.all_images: + for msk in self.all_masks: + print("msk:") + msk.matrixprint() + print("im.bands = %s" % im.bands) + + convolved = im.conv(msk, + precision=pyvips.Precision.APPROXIMATE) + + result = convolved(25, 50) + true = conv(im, msk, 24, 49) + print("result = %s, true = %s" % (result, true)) + assert_less_threshold(result, true, 5) + + result = convolved(50, 50) + true = conv(im, msk, 49, 49) + print("result = %s, true = %s" % (result, true)) + assert_less_threshold(result, true, 5) + + def test_compass(self): + for im in self.all_images: + for msk in self.all_masks: + for prec in [pyvips.Precision.INTEGER, pyvips.Precision.FLOAT]: + for times in range(1, 4): + convolved = im.compass(msk, + times=times, + angle=pyvips.Angle45.D45, + combine=pyvips.Combine.MAX, + precision=prec) + + result = convolved(25, 50) + true = compass(im, msk, 24, 49, times, max) + assert_almost_equal_objects(result, true) + + for im in self.all_images: + for msk in self.all_masks: + for prec in [pyvips.Precision.INTEGER, pyvips.Precision.FLOAT]: + for times in range(1, 4): + convolved = im.compass(msk, + times=times, + angle=pyvips.Angle45.D45, + combine=pyvips.Combine.SUM, + precision=prec) + + result = convolved(25, 50) + true = compass(im, msk, 24, 49, times, operator.add) + assert_almost_equal_objects(result, true) + + def test_convsep(self): + for im in self.all_images: + for prec in [pyvips.Precision.INTEGER, pyvips.Precision.FLOAT]: + gmask = pyvips.Image.gaussmat(2, 0.1, + precision=prec) + gmask_sep = pyvips.Image.gaussmat(2, 0.1, + separable=True, + precision=prec) + + assert gmask.width == gmask.height + assert gmask_sep.width == gmask.width + assert gmask_sep.height == 1 + + a = im.conv(gmask, precision=prec) + b = im.convsep(gmask_sep, precision=prec) + + a_point = a(25, 50) + b_point = b(25, 50) + + assert_almost_equal_objects(a_point, b_point, threshold=0.1) + + def test_fastcor(self): + for im in self.all_images: + for fmt in noncomplex_formats: + small = im.crop(20, 45, 10, 10).cast(fmt) + cor = im.fastcor(small) + v, x, y = cor.minpos() + + assert v == 0 + assert x == 25 + assert y == 50 + + def test_spcor(self): + for im in self.all_images: + for fmt in noncomplex_formats: + small = im.crop(20, 45, 10, 10).cast(fmt) + cor = im.spcor(small) + v, x, y = cor.maxpos() + + assert v == 1.0 + assert x == 25 + assert y == 50 + + def test_gaussblur(self): + for im in self.all_images: + for prec in [pyvips.Precision.INTEGER, pyvips.Precision.FLOAT]: + for i in range(5, 10): + sigma = i / 5.0 + gmask = pyvips.Image.gaussmat(sigma, 0.2, + precision=prec) + + a = im.conv(gmask, precision=prec) + b = im.gaussblur(sigma, min_ampl=0.2, precision=prec) + + a_point = a(25, 50) + b_point = b(25, 50) + + assert_almost_equal_objects(a_point, b_point, + threshold=0.1) + + def test_sharpen(self): + for im in self.all_images: + for fmt in noncomplex_formats: + # old vipses used "radius", check that that still works + sharp = im.sharpen(radius=5) + + for sigma in [0.5, 1, 1.5, 2]: + im = im.cast(fmt) + sharp = im.sharpen(sigma=sigma) + + # hard to test much more than this + assert im.width == sharp.width + assert im.height == sharp.height + + # if m1 and m2 are zero, sharpen should do nothing + sharp = im.sharpen(sigma=sigma, m1=0, m2=0) + sharp = sharp.colourspace(im.interpretation) + # print("testing sig = %g" % sigma) + # print("testing fmt = %s" % fmt) + # print("max diff = %g" % (im - sharp).abs().max()) + assert (im - sharp).abs().max() == 0 + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_create.py b/test/test-suite/test_create.py new file mode 100644 index 00000000..aecac264 --- /dev/null +++ b/test/test-suite/test_create.py @@ -0,0 +1,439 @@ +# vim: set fileencoding=utf-8 : +import pytest + +import pyvips +from helpers import assert_almost_equal_objects + + +class TestCreate: + def test_black(self): + im = pyvips.Image.black(100, 100) + + assert im.width == 100 + assert im.height == 100 + assert im.format == pyvips.BandFormat.UCHAR + assert im.bands == 1 + for i in range(0, 100): + pixel = im(i, i) + assert len(pixel) == 1 + assert pixel[0] == 0 + + im = pyvips.Image.black(100, 100, bands=3) + + assert im.width == 100 + assert im.height == 100 + assert im.format == pyvips.BandFormat.UCHAR + assert im.bands == 3 + for i in range(0, 100): + pixel = im(i, i) + assert len(pixel) == 3 + assert_almost_equal_objects(pixel, [0, 0, 0]) + + def test_buildlut(self): + M = pyvips.Image.new_from_array([[0, 0], + [255, 100]]) + lut = M.buildlut() + assert lut.width == 256 + assert lut.height == 1 + assert lut.bands == 1 + p = lut(0, 0) + assert p[0] == 0.0 + p = lut(255, 0) + assert p[0] == 100.0 + p = lut(10, 0) + assert p[0] == 100 * 10.0 / 255.0 + + M = pyvips.Image.new_from_array([[0, 0, 100], + [255, 100, 0], + [128, 10, 90]]) + lut = M.buildlut() + assert lut.width == 256 + assert lut.height == 1 + assert lut.bands == 2 + p = lut(0, 0) + assert_almost_equal_objects(p, [0.0, 100.0]) + p = lut(64, 0) + assert_almost_equal_objects(p, [5.0, 95.0]) + + def test_eye(self): + im = pyvips.Image.eye(100, 90) + assert im.width == 100 + assert im.height == 90 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + assert im.max() == 1.0 + assert im.min() == -1.0 + + im = pyvips.Image.eye(100, 90, uchar=True) + assert im.width == 100 + assert im.height == 90 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.UCHAR + assert im.max() == 255.0 + assert im.min() == 0.0 + + def test_fractsurf(self): + im = pyvips.Image.fractsurf(100, 90, 2.5) + assert im.width == 100 + assert im.height == 90 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + def test_gaussmat(self): + im = pyvips.Image.gaussmat(1, 0.1) + assert im.width == 5 + assert im.height == 5 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.DOUBLE + assert im.max() == 20 + total = im.avg() * im.width * im.height + scale = im.get("scale") + assert total == scale + p = im(im.width / 2, im.height / 2) + assert p[0] == 20.0 + + im = pyvips.Image.gaussmat(1, 0.1, + separable=True, precision="float") + assert im.width == 5 + assert im.height == 1 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.DOUBLE + assert im.max() == 1.0 + total = im.avg() * im.width * im.height + scale = im.get("scale") + assert total == scale + p = im(im.width / 2, im.height / 2) + assert p[0] == 1.0 + + def test_gaussnoise(self): + im = pyvips.Image.gaussnoise(100, 90) + assert im.width == 100 + assert im.height == 90 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + im = pyvips.Image.gaussnoise(100, 90, sigma=10, mean=100) + assert im.width == 100 + assert im.height == 90 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + sigma = im.deviate() + mean = im.avg() + + assert pytest.approx(sigma, 0.2) == 10 + assert pytest.approx(mean, 0.2) == 100 + + def test_grey(self): + im = pyvips.Image.grey(100, 90) + assert im.width == 100 + assert im.height == 90 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + p = im(0, 0) + assert p[0] == 0.0 + p = im(99, 0) + assert p[0] == 1.0 + p = im(0, 89) + assert p[0] == 0.0 + p = im(99, 89) + assert p[0] == 1.0 + + im = pyvips.Image.grey(100, 90, uchar=True) + assert im.width == 100 + assert im.height == 90 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.UCHAR + + p = im(0, 0) + assert p[0] == 0 + p = im(99, 0) + assert p[0] == 255 + p = im(0, 89) + assert p[0] == 0 + p = im(99, 89) + assert p[0] == 255 + + def test_identity(self): + im = pyvips.Image.identity() + assert im.width == 256 + assert im.height == 1 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.UCHAR + + p = im(0, 0) + assert p[0] == 0.0 + p = im(255, 0) + assert p[0] == 255.0 + p = im(128, 0) + assert p[0] == 128.0 + + im = pyvips.Image.identity(ushort=True) + assert im.width == 65536 + assert im.height == 1 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.USHORT + + p = im(0, 0) + assert p[0] == 0 + p = im(99, 0) + assert p[0] == 99 + p = im(65535, 0) + assert p[0] == 65535 + + def test_invertlut(self): + lut = pyvips.Image.new_from_array([[0.1, 0.2, 0.3, 0.1], + [0.2, 0.4, 0.4, 0.2], + [0.7, 0.5, 0.6, 0.3]]) + im = lut.invertlut() + assert im.width == 256 + assert im.height == 1 + assert im.bands == 3 + assert im.format == pyvips.BandFormat.DOUBLE + + p = im(0, 0) + assert_almost_equal_objects(p, [0, 0, 0]) + p = im(255, 0) + assert_almost_equal_objects(p, [1, 1, 1]) + p = im(0.2 * 255, 0) + assert pytest.approx(p[0], 0.1) == 0.1 + p = im(0.3 * 255, 0) + assert pytest.approx(p[1], 0.1) == 0.1 + p = im(0.1 * 255, 0) + assert pytest.approx(p[2], 0.1) == 0.1 + + def test_logmat(self): + im = pyvips.Image.logmat(1, 0.1) + assert im.width == 7 + assert im.height == 7 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.DOUBLE + assert im.max() == 20 + total = im.avg() * im.width * im.height + scale = im.get("scale") + assert total == scale + p = im(im.width / 2, im.height / 2) + assert p[0] == 20.0 + + im = pyvips.Image.logmat(1, 0.1, + separable=True, precision="float") + assert im.width == 7 + assert im.height == 1 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.DOUBLE + assert im.max() == 1.0 + total = im.avg() * im.width * im.height + scale = im.get("scale") + assert total == scale + p = im(im.width / 2, im.height / 2) + assert p[0] == 1.0 + + def test_mask_butterworth_band(self): + im = pyvips.Image.mask_butterworth_band(128, 128, 2, + 0.5, 0.5, 0.7, + 0.1) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + assert pytest.approx(im.max(), 0.01) == 1 + p = im(32, 32) + assert p[0] == 1.0 + + im = pyvips.Image.mask_butterworth_band(128, 128, 2, + 0.5, 0.5, 0.7, + 0.1, uchar=True, optical=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.UCHAR + assert im.max() == 255 + p = im(32, 32) + assert p[0] == 255.0 + p = im(64, 64) + assert p[0] == 255.0 + + im = pyvips.Image.mask_butterworth_band(128, 128, 2, + 0.5, 0.5, 0.7, + 0.1, uchar=True, optical=True, + nodc=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.UCHAR + assert im.max() == 255 + p = im(32, 32) + assert p[0] == 255.0 + p = im(64, 64) + assert p[0] != 255 + + def test_mask_butterworth(self): + im = pyvips.Image.mask_butterworth(128, 128, 2, 0.7, 0.1, + nodc=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + assert pytest.approx(im.min(), 0.01) == 0 + p = im(0, 0) + assert p[0] == 0.0 + v, x, y = im.maxpos() + assert x == 64 + assert y == 64 + + im = pyvips.Image.mask_butterworth(128, 128, 2, 0.7, 0.1, + optical=True, uchar=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.UCHAR + assert pytest.approx(im.min(), 0.01) == 0 + p = im(64, 64) + assert p[0] == 255 + + def test_mask_butterworth_ring(self): + im = pyvips.Image.mask_butterworth_ring(128, 128, 2, 0.7, 0.1, 0.5, + nodc=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + p = im(45, 0) + assert pytest.approx(p[0], 0.0001) == 1.0 + v, x, y = im.minpos() + assert x == 64 + assert y == 64 + + def test_mask_fractal(self): + im = pyvips.Image.mask_fractal(128, 128, 2.3) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + def test_mask_gaussian_band(self): + im = pyvips.Image.mask_gaussian_band(128, 128, 0.5, 0.5, 0.7, 0.1) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + assert pytest.approx(im.max(), 0.01) == 1 + p = im(32, 32) + assert p[0] == 1.0 + + def test_mask_gaussian(self): + im = pyvips.Image.mask_gaussian(128, 128, 0.7, 0.1, + nodc=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + assert pytest.approx(im.min(), 0.01) == 0 + p = im(0, 0) + assert p[0] == 0.0 + + def test_mask_gaussian_ring(self): + im = pyvips.Image.mask_gaussian_ring(128, 128, 0.7, 0.1, 0.5, + nodc=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + p = im(45, 0) + assert pytest.approx(p[0], 0.001) == 1.0 + + def test_mask_ideal_band(self): + im = pyvips.Image.mask_ideal_band(128, 128, 0.5, 0.5, 0.7) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + assert pytest.approx(im.max(), 0.01) == 1 + p = im(32, 32) + assert p[0] == 1.0 + + def test_mask_ideal(self): + im = pyvips.Image.mask_ideal(128, 128, 0.7, + nodc=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + assert pytest.approx(im.min(), 0.01) == 0 + p = im(0, 0) + assert p[0] == 0.0 + + def test_mask_gaussian_ring_2(self): + im = pyvips.Image.mask_ideal_ring(128, 128, 0.7, 0.5, + nodc=True) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + p = im(45, 0) + assert pytest.approx(p[0], 0.001) == 1.0 + + def test_sines(self): + im = pyvips.Image.sines(128, 128) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + @pytest.mark.skipif(pyvips.type_find("VipsOperation", "text") == 0, + reason="no text, skipping test") + def test_text(self): + im = pyvips.Image.text("Hello, world!") + assert im.width > 10 + assert im.height > 10 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.UCHAR + assert im.max() == 255 + assert im.min() == 0 + + def test_tonelut(self): + im = pyvips.Image.tonelut() + assert im.bands == 1 + assert im.format == pyvips.BandFormat.USHORT + assert im.width == 32768 + assert im.height == 1 + assert im.hist_ismonotonic() + + def test_xyz(self): + im = pyvips.Image.xyz(128, 128) + assert im.bands == 2 + assert im.format == pyvips.BandFormat.UINT + assert im.width == 128 + assert im.height == 128 + p = im(45, 35) + assert_almost_equal_objects(p, [45, 35]) + + def test_zone(self): + im = pyvips.Image.zone(128, 128) + assert im.width == 128 + assert im.height == 128 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + @pytest.mark.skipif(pyvips.type_find("VipsOperation", "worley") == 0, + reason="no worley, skipping test") + def test_worley(self): + im = pyvips.Image.worley(512, 512) + assert im.width == 512 + assert im.height == 512 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + @pytest.mark.skipif(pyvips.type_find("VipsOperation", "perlin") == 0, + reason="no perlin, skipping test") + def test_perlin(self): + im = pyvips.Image.perlin(512, 512) + assert im.width == 512 + assert im.height == 512 + assert im.bands == 1 + assert im.format == pyvips.BandFormat.FLOAT + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_draw.py b/test/test-suite/test_draw.py new file mode 100644 index 00000000..d5f6795a --- /dev/null +++ b/test/test-suite/test_draw.py @@ -0,0 +1,101 @@ +# vim: set fileencoding=utf-8 : +import pytest + +import pyvips + + +class TestDraw: + def test_draw_circle(self): + im = pyvips.Image.black(100, 100) + im = im.draw_circle(100, 50, 50, 25) + pixel = im(25, 50) + assert len(pixel) == 1 + assert pixel[0] == 100 + pixel = im(26, 50) + assert len(pixel) == 1 + assert pixel[0] == 0 + + im = pyvips.Image.black(100, 100) + im = im.draw_circle(100, 50, 50, 25, fill=True) + pixel = im(25, 50) + assert len(pixel) == 1 + assert pixel[0] == 100 + pixel = im(26, 50) + assert pixel[0] == 100 + pixel = im(24, 50) + assert pixel[0] == 0 + + def test_draw_flood(self): + im = pyvips.Image.black(100, 100) + im = im.draw_circle(100, 50, 50, 25) + im = im.draw_flood(100, 50, 50) + + im2 = pyvips.Image.black(100, 100) + im2 = im2.draw_circle(100, 50, 50, 25, fill=True) + + diff = (im - im2).abs().max() + assert diff == 0 + + def test_draw_image(self): + im = pyvips.Image.black(51, 51) + im = im.draw_circle(100, 25, 25, 25, fill=True) + + im2 = pyvips.Image.black(100, 100) + im2 = im2.draw_image(im, 25, 25) + + im3 = pyvips.Image.black(100, 100) + im3 = im3.draw_circle(100, 50, 50, 25, fill=True) + + diff = (im2 - im3).abs().max() + assert diff == 0 + + def test_draw_line(self): + im = pyvips.Image.black(100, 100) + im = im.draw_line(100, 0, 0, 100, 0) + pixel = im(0, 0) + assert len(pixel) == 1 + assert pixel[0] == 100 + pixel = im(0, 1) + assert len(pixel) == 1 + assert pixel[0] == 0 + + def test_draw_mask(self): + mask = pyvips.Image.black(51, 51) + mask = mask.draw_circle(128, 25, 25, 25, fill=True) + + im = pyvips.Image.black(100, 100) + im = im.draw_mask(200, mask, 25, 25) + + im2 = pyvips.Image.black(100, 100) + im2 = im2.draw_circle(100, 50, 50, 25, fill=True) + + diff = (im - im2).abs().max() + assert diff == 0 + + def test_draw_rect(self): + im = pyvips.Image.black(100, 100) + im = im.draw_rect(100, 25, 25, 50, 50, fill=True) + + im2 = pyvips.Image.black(100, 100) + for y in range(25, 75): + im2 = im2.draw_line(100, 25, y, 74, y) + + diff = (im - im2).abs().max() + assert diff == 0 + + def test_draw_smudge(self): + im = pyvips.Image.black(100, 100) + im = im.draw_circle(100, 50, 50, 25, fill=True) + + im2 = im.draw_smudge(10, 10, 50, 50) + + im3 = im.crop(10, 10, 50, 50) + + im4 = im2.draw_image(im3, 10, 10) + + diff = (im4 - im).abs().max() + assert diff == 0 + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_foreign.py b/test/test-suite/test_foreign.py new file mode 100644 index 00000000..270d7052 --- /dev/null +++ b/test/test-suite/test_foreign.py @@ -0,0 +1,761 @@ +# vim: set fileencoding=utf-8 : +import gc +import os +import shutil +import tempfile +import pytest + +import pyvips +from helpers import JPEG_FILE, SRGB_FILE, \ + MATLAB_FILE, PNG_FILE, TIF_FILE, OME_FILE, ANALYZE_FILE, \ + GIF_FILE, WEBP_FILE, EXR_FILE, FITS_FILE, OPENSLIDE_FILE, \ + PDF_FILE, SVG_FILE, SVGZ_FILE, SVG_GZ_FILE, GIF_ANIM_FILE, \ + DICOM_FILE, BMP_FILE, temp_filename, assert_almost_equal_objects, have, \ + skip_if_no + + +class TestForeign: + tempdir = None + + @classmethod + def setup_class(cls): + cls.tempdir = tempfile.mkdtemp() + + cls.colour = pyvips.Image.jpegload(JPEG_FILE) + cls.mono = cls.colour.extract_band(1) + # we remove the ICC profile: the RGB one will no longer be appropriate + cls.mono.remove("icc-profile-data") + cls.rad = cls.colour.float2rad() + cls.rad.remove("icc-profile-data") + cls.cmyk = cls.colour.bandjoin(cls.mono) + cls.cmyk = cls.cmyk.copy(interpretation=pyvips.Interpretation.CMYK) + cls.cmyk.remove("icc-profile-data") + + im = pyvips.Image.new_from_file(GIF_FILE) + cls.onebit = im > 128 + + @classmethod + def teardown_class(cls): + shutil.rmtree(cls.tempdir, ignore_errors=True) + + # we have test files for formats which have a clear standard + def file_loader(self, loader, test_file, validate): + im = pyvips.Operation.call(loader, test_file) + validate(im) + im = pyvips.Image.new_from_file(test_file) + validate(im) + + def buffer_loader(self, loader, test_file, validate): + with open(test_file, 'rb') as f: + buf = f.read() + + im = pyvips.Operation.call(loader, buf) + validate(im) + im = pyvips.Image.new_from_buffer(buf, "") + validate(im) + + def save_load(self, format, im): + x = pyvips.Image.new_temp_file(format) + im.write(x) + + assert im.width == x.width + assert im.height == x.height + assert im.bands == x.bands + max_diff = (im - x).abs().max() + assert max_diff == 0 + + def save_load_file(self, format, options, im, thresh): + # yuk! + # but we can't set format parameters for pyvips.Image.new_temp_file() + filename = temp_filename(self.tempdir, format) + + im.write_to_file(filename + options) + x = pyvips.Image.new_from_file(filename) + + assert im.width == x.width + assert im.height == x.height + assert im.bands == x.bands + max_diff = (im - x).abs().max() + assert max_diff <= thresh + x = None + + def save_load_buffer(self, saver, loader, im, max_diff=0, **kwargs): + buf = pyvips.Operation.call(saver, im, **kwargs) + x = pyvips.Operation.call(loader, buf) + + assert im.width == x.width + assert im.height == x.height + assert im.bands == x.bands + assert (im - x).abs().max() <= max_diff + + def save_buffer_tempfile(self, saver, suf, im, max_diff=0): + filename = temp_filename(self.tempdir, suf) + + buf = pyvips.Operation.call(saver, im) + f = open(filename, 'wb') + f.write(buf) + f.close() + + x = pyvips.Image.new_from_file(filename) + + assert im.width == x.width + assert im.height == x.height + assert im.bands == x.bands + assert (im - x).abs().max() <= max_diff + + def test_vips(self): + self.save_load_file(".v", "", self.colour, 0) + + # check we can save and restore metadata + filename = temp_filename(self.tempdir, ".v") + self.colour.write_to_file(filename) + x = pyvips.Image.new_from_file(filename) + before_exif = self.colour.get("exif-data") + after_exif = x.get("exif-data") + + assert len(before_exif) == len(after_exif) + for i in range(len(before_exif)): + assert before_exif[i] == after_exif[i] + + x = None + + @skip_if_no('jpegload') + def test_jpeg(self): + def jpeg_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [6, 5, 3]) + profile = im.get("icc-profile-data") + assert len(profile) == 1352 + assert im.width == 1024 + assert im.height == 768 + assert im.bands == 3 + + self.file_loader("jpegload", JPEG_FILE, jpeg_valid) + self.save_load("%s.jpg", self.mono) + self.save_load("%s.jpg", self.colour) + + self.buffer_loader("jpegload_buffer", JPEG_FILE, jpeg_valid) + self.save_load_buffer("jpegsave_buffer", "jpegload_buffer", + self.colour, 80) + + # see if we have exif parsing: our test image has this field + x = pyvips.Image.new_from_file(JPEG_FILE) + if x.get_typeof("exif-ifd0-Orientation") != 0: + # we need a copy of the image to set the new metadata on + # otherwise we get caching problems + + # can set, save and load new orientation + x = pyvips.Image.new_from_file(JPEG_FILE) + x = x.copy() + x.set("orientation", 2) + filename = temp_filename(self.tempdir, '.jpg') + x.write_to_file(filename) + x = pyvips.Image.new_from_file(filename) + y = x.get("orientation") + assert y == 2 + + # can remove orientation, save, load again, orientation + # has reset + x.remove("orientation") + filename = temp_filename(self.tempdir, '.jpg') + x.write_to_file(filename) + x = pyvips.Image.new_from_file(filename) + y = x.get("orientation") + assert y == 1 + + # autorotate load works + filename = temp_filename(self.tempdir, '.jpg') + x = pyvips.Image.new_from_file(JPEG_FILE) + x = x.copy() + x.set("orientation", 6) + x.write_to_file(filename) + x1 = pyvips.Image.new_from_file(filename) + x2 = pyvips.Image.new_from_file(filename, autorotate=True) + assert x1.width == x2.height + assert x1.height == x2.width + + @pytest.mark.skipif(not have("pngload") or + not os.path.isfile(PNG_FILE), + reason="no png support, skipping test") + def test_png(self): + def png_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [38671.0, 33914.0, 26762.0]) + assert im.width == 290 + assert im.height == 442 + assert im.bands == 3 + + self.file_loader("pngload", PNG_FILE, png_valid) + self.buffer_loader("pngload_buffer", PNG_FILE, png_valid) + self.save_load_buffer("pngsave_buffer", "pngload_buffer", self.colour) + self.save_load("%s.png", self.mono) + self.save_load("%s.png", self.colour) + + @pytest.mark.skipif(not have("tiffload") or + not os.path.isfile(TIF_FILE), + reason="no tiff support, skipping test") + def test_tiff(self): + def tiff_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [38671.0, 33914.0, 26762.0]) + assert im.width == 290 + assert im.height == 442 + assert im.bands == 3 + + self.file_loader("tiffload", TIF_FILE, tiff_valid) + self.buffer_loader("tiffload_buffer", TIF_FILE, tiff_valid) + if pyvips.at_least_libvips(8, 5): + self.save_load_buffer("tiffsave_buffer", + "tiffload_buffer", + self.colour) + self.save_load("%s.tif", self.mono) + self.save_load("%s.tif", self.colour) + self.save_load("%s.tif", self.cmyk) + + self.save_load("%s.tif", self.onebit) + self.save_load_file(".tif", "[squash]", self.onebit, 0) + self.save_load_file(".tif", "[miniswhite]", self.onebit, 0) + self.save_load_file(".tif", "[squash,miniswhite]", self.onebit, 0) + + self.save_load_file(".tif", + "[profile={0}]".format(SRGB_FILE), + self.colour, 0) + self.save_load_file(".tif", "[tile]", self.colour, 0) + self.save_load_file(".tif", "[tile,pyramid]", self.colour, 0) + self.save_load_file(".tif", + "[tile,pyramid,compression=jpeg]", self.colour, 80) + self.save_load_file(".tif", "[bigtiff]", self.colour, 0) + self.save_load_file(".tif", "[compression=jpeg]", self.colour, 80) + self.save_load_file(".tif", + "[tile,tile-width=256]", self.colour, 10) + + filename = temp_filename(self.tempdir, '.tif') + x = pyvips.Image.new_from_file(TIF_FILE) + x = x.copy() + x.set("orientation", 2) + x.write_to_file(filename) + x = pyvips.Image.new_from_file(filename) + y = x.get("orientation") + assert y == 2 + + filename = temp_filename(self.tempdir, '.tif') + x = pyvips.Image.new_from_file(TIF_FILE) + x = x.copy() + x.set("orientation", 2) + x.write_to_file(filename) + x = pyvips.Image.new_from_file(filename) + y = x.get("orientation") + assert y == 2 + x.remove("orientation") + + filename = temp_filename(self.tempdir, '.tif') + x.write_to_file(filename) + x = pyvips.Image.new_from_file(filename) + y = x.get("orientation") + assert y == 1 + + filename = temp_filename(self.tempdir, '.tif') + x = pyvips.Image.new_from_file(TIF_FILE) + x = x.copy() + x.set("orientation", 6) + x.write_to_file(filename) + x1 = pyvips.Image.new_from_file(filename) + x2 = pyvips.Image.new_from_file(filename, autorotate=True) + assert x1.width == x2.height + assert x1.height == x2.width + + # OME support in 8.5 + if pyvips.at_least_libvips(8, 5): + x = pyvips.Image.new_from_file(OME_FILE) + assert x.width == 439 + assert x.height == 167 + page_height = x.height + + x = pyvips.Image.new_from_file(OME_FILE, n=-1) + assert x.width == 439 + assert x.height == page_height * 15 + + x = pyvips.Image.new_from_file(OME_FILE, page=1, n=-1) + assert x.width == 439 + assert x.height == page_height * 14 + + x = pyvips.Image.new_from_file(OME_FILE, page=1, n=2) + assert x.width == 439 + assert x.height == page_height * 2 + + x = pyvips.Image.new_from_file(OME_FILE, n=-1) + assert x(0, 166)[0] == 96 + assert x(0, 167)[0] == 0 + assert x(0, 168)[0] == 1 + + filename = temp_filename(self.tempdir, '.tif') + x.write_to_file(filename) + + x = pyvips.Image.new_from_file(filename, n=-1) + assert x.width == 439 + assert x.height == page_height * 15 + assert x(0, 166)[0] == 96 + assert x(0, 167)[0] == 0 + assert x(0, 168)[0] == 1 + + # pyr save to buffer added in 8.6 + if pyvips.at_least_libvips(8, 6): + x = pyvips.Image.new_from_file(TIF_FILE) + buf = x.tiffsave_buffer(tile=True, pyramid=True) + filename = temp_filename(self.tempdir, '.tif') + x.tiffsave(filename, tile=True, pyramid=True) + with open(filename, 'rb') as f: + buf2 = f.read() + assert len(buf) == len(buf2) + + a = pyvips.Image.new_from_buffer(buf, "", page=2) + b = pyvips.Image.new_from_buffer(buf2, "", page=2) + assert a.width == b.width + assert a.height == b.height + assert a.avg() == b.avg() + + @pytest.mark.skipif(not have("magickload") or + not os.path.isfile(BMP_FILE), + reason="no magick support, skipping test") + def test_magickload(self): + def bmp_valid(im): + a = im(100, 100) + + assert_almost_equal_objects(a, [227, 216, 201]) + assert im.width == 1419 + assert im.height == 1001 + + self.file_loader("magickload", BMP_FILE, bmp_valid) + self.buffer_loader("magickload_buffer", BMP_FILE, bmp_valid) + + # we should have rgba for svg files + im = pyvips.Image.magickload(SVG_FILE) + assert im.bands == 4 + + # density should change size of generated svg + im = pyvips.Image.magickload(SVG_FILE, density='100') + width = im.width + height = im.height + im = pyvips.Image.magickload(SVG_FILE, density='200') + # This seems to fail on travis, no idea why, some problem in their IM + # perhaps + # assert im.width == width * 2 + # assert im.height == height * 2 + + # all-frames should load every frame of the animation + # (though all-frames is deprecated) + im = pyvips.Image.magickload(GIF_ANIM_FILE) + width = im.width + height = im.height + im = pyvips.Image.magickload(GIF_ANIM_FILE, all_frames=True) + assert im.width == width + assert im.height == height * 5 + + # page/n let you pick a range of pages + # 'n' param added in 8.5 + if pyvips.at_least_libvips(8, 5): + im = pyvips.Image.magickload(GIF_ANIM_FILE) + width = im.width + height = im.height + im = pyvips.Image.magickload(GIF_ANIM_FILE, page=1, n=2) + assert im.width == width + assert im.height == height * 2 + page_height = im.get("page-height") + assert page_height == height + + # should work for dicom + im = pyvips.Image.magickload(DICOM_FILE) + assert im.width == 128 + assert im.height == 128 + # some IMs are 3 bands, some are 1, can't really test + # assert im.bands == 1 + + # added in 8.7 + if have("magicksave"): + self.save_load_file(".bmp", "", self.colour, 0) + self.save_load_buffer("magicksave_buffer", "magickload_buffer", + self.colour, 0, format="BMP") + self.save_load("%s.bmp", self.colour) + + @pytest.mark.skipif(not have("webpload") or not os.path.isfile(WEBP_FILE), + reason="no webp support, skipping test") + def test_webp(self): + def webp_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [71, 166, 236]) + assert im.width == 550 + assert im.height == 368 + assert im.bands == 3 + + self.file_loader("webpload", WEBP_FILE, webp_valid) + self.buffer_loader("webpload_buffer", WEBP_FILE, webp_valid) + self.save_load_buffer("webpsave_buffer", "webpload_buffer", + self.colour, 60) + self.save_load("%s.webp", self.colour) + + # test lossless mode + im = pyvips.Image.new_from_file(WEBP_FILE) + buf = im.webpsave_buffer(lossless=True) + im2 = pyvips.Image.new_from_buffer(buf, "") + assert im.avg() == im2.avg() + + # higher Q should mean a bigger buffer + b1 = im.webpsave_buffer(Q=10) + b2 = im.webpsave_buffer(Q=90) + assert len(b2) > len(b1) + + # try saving an image with an ICC profile and reading it back ... if we + # can do it, our webp supports metadata load/save + buf = self.colour.webpsave_buffer() + im = pyvips.Image.new_from_buffer(buf, "") + if im.get_typeof("icc-profile-data") != 0: + # verify that the profile comes back unharmed + p1 = self.colour.get("icc-profile-data") + p2 = im.get("icc-profile-data") + assert p1 == p2 + + # add tests for exif, xmp, ipct + # the exif test will need us to be able to walk the header, + # we can't just check exif-data + + # we can test that exif changes change the output of webpsave + # first make sure we have exif support + z = pyvips.Image.new_from_file(JPEG_FILE) + if z.get_typeof("exif-ifd0-Orientation") != 0: + x = self.colour.copy() + x.set("orientation", 6) + buf = x.webpsave_buffer() + y = pyvips.Image.new_from_buffer(buf, "") + assert y.get("orientation") == 6 + + @pytest.mark.skipif(not have("analyzeload") or + not os.path.isfile(ANALYZE_FILE), + reason="no analyze support, skipping test") + def test_analyzeload(self): + def analyze_valid(im): + a = im(10, 10) + assert pytest.approx(a[0]) == 3335 + assert im.width == 128 + assert im.height == 8064 + assert im.bands == 1 + + self.file_loader("analyzeload", ANALYZE_FILE, analyze_valid) + + @pytest.mark.skipif(not have("matload") or + not os.path.isfile(MATLAB_FILE), + reason="no matlab support, skipping test") + def test_matload(self): + def matlab_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [38671.0, 33914.0, 26762.0]) + assert im.width == 290 + assert im.height == 442 + assert im.bands == 3 + + self.file_loader("matload", MATLAB_FILE, matlab_valid) + + @pytest.mark.skipif(not have("openexrload") or + not os.path.isfile(EXR_FILE), + reason="no openexr support, skipping test") + def test_openexrload(self): + def exr_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [0.124512, 0.159668, + 0.040375, 1.0], + threshold=0.00001) + assert im.width == 610 + assert im.height == 406 + assert im.bands == 4 + + self.file_loader("openexrload", EXR_FILE, exr_valid) + + @pytest.mark.skipif(not have("fitsload") or + not os.path.isfile(FITS_FILE), + reason="no fits support, skipping test") + def test_fitsload(self): + def fits_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [-0.165013, -0.148553, 1.09122, + -0.942242], threshold=0.00001) + assert im.width == 200 + assert im.height == 200 + assert im.bands == 4 + + self.file_loader("fitsload", FITS_FILE, fits_valid) + self.save_load("%s.fits", self.mono) + + @pytest.mark.skipif(not have("openslideload") or # noqa: E501 + not os.path.isfile(OPENSLIDE_FILE), + reason="no openslide support, skipping test") + def test_openslideload(self): + def openslide_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [244, 250, 243, 255]) + assert im.width == 2220 + assert im.height == 2967 + assert im.bands == 4 + + self.file_loader("openslideload", OPENSLIDE_FILE, openslide_valid) + + @pytest.mark.skipif(not have("pdfload") or + not os.path.isfile(PDF_FILE), + reason="no pdf support, skipping test") + def test_pdfload(self): + def pdf_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [35, 31, 32, 255]) + assert im.width == 1133 + assert im.height == 680 + assert im.bands == 4 + + self.file_loader("pdfload", PDF_FILE, pdf_valid) + self.buffer_loader("pdfload_buffer", PDF_FILE, pdf_valid) + + im = pyvips.Image.new_from_file(PDF_FILE) + x = pyvips.Image.new_from_file(PDF_FILE, scale=2) + assert abs(im.width * 2 - x.width) < 2 + assert abs(im.height * 2 - x.height) < 2 + + im = pyvips.Image.new_from_file(PDF_FILE) + x = pyvips.Image.new_from_file(PDF_FILE, dpi=144) + assert abs(im.width * 2 - x.width) < 2 + assert abs(im.height * 2 - x.height) < 2 + + @pytest.mark.skipif(not have("gifload") or + not os.path.isfile(GIF_FILE), + reason="no gif support, skipping test") + def test_gifload(self): + def gif_valid(im): + a = im(10, 10) + assert_almost_equal_objects(a, [33]) + assert im.width == 159 + assert im.height == 203 + assert im.bands == 1 + + self.file_loader("gifload", GIF_FILE, gif_valid) + self.buffer_loader("gifload_buffer", GIF_FILE, gif_valid) + + # 'n' param added in 8.5 + if pyvips.at_least_libvips(8, 5): + x1 = pyvips.Image.new_from_file(GIF_ANIM_FILE) + x2 = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=2) + assert x2.height == 2 * x1.height + page_height = x2.get("page-height") + assert page_height == x1.height + + x2 = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=-1) + assert x2.height == 5 * x1.height + + x2 = pyvips.Image.new_from_file(GIF_ANIM_FILE, page=1, n=-1) + assert x2.height == 4 * x1.height + + @pytest.mark.skipif(not have("svgload") or + not os.path.isfile(SVG_FILE), + reason="no svg support, skipping test") + def test_svgload(self): + def svg_valid(im): + a = im(10, 10) + + # some old rsvg versions are way, way off + assert abs(a[0] - 79) < 2 + assert abs(a[1] - 79) < 2 + assert abs(a[2] - 132) < 2 + assert abs(a[3] - 255) < 2 + + assert im.width == 288 + assert im.height == 470 + assert im.bands == 4 + + self.file_loader("svgload", SVG_FILE, svg_valid) + self.buffer_loader("svgload_buffer", SVG_FILE, svg_valid) + + self.file_loader("svgload", SVGZ_FILE, svg_valid) + self.buffer_loader("svgload_buffer", SVGZ_FILE, svg_valid) + + self.file_loader("svgload", SVG_GZ_FILE, svg_valid) + + im = pyvips.Image.new_from_file(SVG_FILE) + x = pyvips.Image.new_from_file(SVG_FILE, scale=2) + assert abs(im.width * 2 - x.width) < 2 + assert abs(im.height * 2 - x.height) < 2 + + im = pyvips.Image.new_from_file(SVG_FILE) + x = pyvips.Image.new_from_file(SVG_FILE, dpi=144) + assert abs(im.width * 2 - x.width) < 2 + assert abs(im.height * 2 - x.height) < 2 + + def test_csv(self): + self.save_load("%s.csv", self.mono) + + def test_matrix(self): + self.save_load("%s.mat", self.mono) + + @pytest.mark.skipif(not have("ppmload"), + reason="no PPM support, skipping test") + def test_ppm(self): + self.save_load("%s.ppm", self.mono) + self.save_load("%s.ppm", self.colour) + + @pytest.mark.skipif(not have("radload"), + reason="no Radiance support, skipping test") + def test_rad(self): + self.save_load("%s.hdr", self.colour) + self.save_buffer_tempfile("radsave_buffer", ".hdr", + self.rad, max_diff=0) + + @pytest.mark.skipif(not have("dzsave"), + reason="no dzsave support, skipping test") + def test_dzsave(self): + # dzsave is hard to test, there are so many options + # test each option separately and hope they all function together + # correctly + + # default deepzoom layout ... we must use png here, since we want to + # test the overlap for equality + filename = temp_filename(self.tempdir, '') + self.colour.dzsave(filename, suffix=".png") + + # test horizontal overlap ... expect 256 step, overlap 1 + x = pyvips.Image.new_from_file(filename + "_files/10/0_0.png") + assert x.width == 255 + y = pyvips.Image.new_from_file(filename + "_files/10/1_0.png") + assert y.width == 256 + + # the right two columns of x should equal the left two columns of y + left = x.crop(x.width - 2, 0, 2, x.height) + right = y.crop(0, 0, 2, y.height) + assert (left - right).abs().max() == 0 + + # test vertical overlap + assert x.height == 255 + y = pyvips.Image.new_from_file(filename + "_files/10/0_1.png") + assert y.height == 256 + + # the bottom two rows of x should equal the top two rows of y + top = x.crop(0, x.height - 2, x.width, 2) + bottom = y.crop(0, 0, y.width, 2) + assert (top - bottom).abs().max() == 0 + + # there should be a bottom layer + x = pyvips.Image.new_from_file(filename + "_files/0/0_0.png") + assert x.width == 1 + assert x.height == 1 + + # 10 should be the final layer + assert not os.path.isdir(filename + "_files/11") + + # default google layout + filename = temp_filename(self.tempdir, '') + self.colour.dzsave(filename, layout="google") + + # test bottom-right tile ... default is 256x256 tiles, overlap 0 + x = pyvips.Image.new_from_file(filename + "/2/2/3.jpg") + assert x.width == 256 + assert x.height == 256 + assert not os.path.exists(filename + "/2/2/4.jpg") + assert not os.path.exists(filename + "/3") + x = pyvips.Image.new_from_file(filename + "/blank.png") + assert x.width == 256 + assert x.height == 256 + + # google layout with overlap ... verify that we clip correctly + + # overlap 1, 510x510 pixels, 256 pixel tiles, should be exactly 2x2 + # tiles, though in fact the bottom and right edges will be white + filename = temp_filename(self.tempdir, '') + self.colour.crop(0, 0, 510, 510).dzsave(filename, layout="google", + overlap=1, depth="one") + + x = pyvips.Image.new_from_file(filename + "/0/1/1.jpg") + assert x.width == 256 + assert x.height == 256 + assert not os.path.exists(filename + "/0/2/2.jpg") + + # with 511x511, it'll fit exactly into 2x2 -- we we actually generate + # 3x3, since we output the overlaps + # 8.6 revised the rules on overlaps, so don't test earlier than that + if pyvips.base.at_least_libvips(8, 6): + filename = temp_filename(self.tempdir, '') + self.colour.crop(0, 0, 511, 511).dzsave(filename, layout="google", + overlap=1, depth="one") + + x = pyvips.Image.new_from_file(filename + "/0/2/2.jpg") + assert x.width == 256 + assert x.height == 256 + assert not os.path.exists(filename + "/0/3/3.jpg") + + # default zoomify layout + filename = temp_filename(self.tempdir, '') + self.colour.dzsave(filename, layout="zoomify") + + # 256x256 tiles, no overlap + assert os.path.exists(filename + "/ImageProperties.xml") + x = pyvips.Image.new_from_file(filename + "/TileGroup0/2-3-2.jpg") + assert x.width == 256 + assert x.height == 256 + + # test zip output + filename = temp_filename(self.tempdir, '.zip') + self.colour.dzsave(filename) + # before 8.5.8, you needed a gc on pypy to flush small zip output to + # disc + if not pyvips.base.at_least_libvips(8, 6): + gc.collect() + assert os.path.exists(filename) + assert not os.path.exists(filename + "_files") + assert not os.path.exists(filename + ".dzi") + + # test compressed zip output + filename2 = temp_filename(self.tempdir, '.zip') + self.colour.dzsave(filename2, compression=-1) + # before 8.5.8, you needed a gc on pypy to flush small zip output to + # disc + if not pyvips.base.at_least_libvips(8, 6): + gc.collect() + assert os.path.exists(filename2) + assert os.path.getsize(filename2) < os.path.getsize(filename) + + # test suffix + filename = temp_filename(self.tempdir, '') + self.colour.dzsave(filename, suffix=".png") + + x = pyvips.Image.new_from_file(filename + "_files/10/0_0.png") + assert x.width == 255 + + # test overlap + filename = temp_filename(self.tempdir, '') + self.colour.dzsave(filename, overlap=200) + + y = pyvips.Image.new_from_file(filename + "_files/10/1_1.jpeg") + assert y.width == 654 + + # test tile-size + filename = temp_filename(self.tempdir, '') + self.colour.dzsave(filename, tile_size=512) + + y = pyvips.Image.new_from_file(filename + "_files/10/0_0.jpeg") + assert y.width == 513 + assert y.height == 513 + + # test save to memory buffer + if have("dzsave_buffer"): + filename = temp_filename(self.tempdir, '.zip') + base = os.path.basename(filename) + root, ext = os.path.splitext(base) + + self.colour.dzsave(filename) + # before 8.5.8, you needed a gc on pypy to flush small zip + # output to disc + if not pyvips.base.at_least_libvips(8, 6): + gc.collect() + with open(filename, 'rb') as f: + buf1 = f.read() + buf2 = self.colour.dzsave_buffer(basename=root) + assert len(buf1) == len(buf2) + + # we can't test the bytes are exactly equal -- the timestamps will + # be different + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_histogram.py b/test/test-suite/test_histogram.py new file mode 100644 index 00000000..765b33e1 --- /dev/null +++ b/test/test-suite/test_histogram.py @@ -0,0 +1,114 @@ +# vim: set fileencoding=utf-8 : +import pytest + +import pyvips +from helpers import JPEG_FILE + + +class TestHistogram: + def test_hist_cum(self): + im = pyvips.Image.identity() + + sum = im.avg() * 256 + + cum = im.hist_cum() + + p = cum(255, 0) + assert p[0] == sum + + def test_hist_equal(self): + im = pyvips.Image.new_from_file(JPEG_FILE) + + im2 = im.hist_equal() + + assert im.width == im2.width + assert im.height == im2.height + + assert im.avg() < im2.avg() + assert im.deviate() < im2.deviate() + + def test_hist_ismonotonic(self): + im = pyvips.Image.identity() + assert im.hist_ismonotonic() + + def test_hist_local(self): + im = pyvips.Image.new_from_file(JPEG_FILE) + + im2 = im.hist_local(10, 10) + + assert im.width == im2.width + assert im.height == im2.height + + assert im.avg() < im2.avg() + assert im.deviate() < im2.deviate() + + if pyvips.at_least_libvips(8, 5): + im3 = im.hist_local(10, 10, max_slope=3) + + assert im.width == im3.width + assert im.height == im3.height + + assert im3.deviate() < im2.deviate() + + def test_hist_match(self): + im = pyvips.Image.identity() + im2 = pyvips.Image.identity() + + matched = im.hist_match(im2) + + assert (im - matched).abs().max() == 0.0 + + def test_hist_norm(self): + im = pyvips.Image.identity() + im2 = im.hist_norm() + + assert (im - im2).abs().max() == 0.0 + + def test_hist_plot(self): + im = pyvips.Image.identity() + im2 = im.hist_plot() + + assert im2.width == 256 + assert im2.height == 256 + assert im2.format == pyvips.BandFormat.UCHAR + assert im2.bands == 1 + + def test_hist_map(self): + im = pyvips.Image.identity() + + im2 = im.maplut(im) + + assert (im - im2).abs().max() == 0.0 + + def test_percent(self): + im = pyvips.Image.new_from_file(JPEG_FILE).extract_band(1) + + pc = im.percent(90) + + msk = im <= pc + n_set = (msk.avg() * msk.width * msk.height) / 255.0 + pc_set = 100 * n_set / (msk.width * msk.height) + + assert pytest.approx(pc_set, 0.5) == 90 + + def test_hist_entropy(self): + im = pyvips.Image.new_from_file(JPEG_FILE).extract_band(1) + + ent = im.hist_find().hist_entropy() + + assert pytest.approx(ent, 0.01) == 4.37 + + def test_stdif(self): + im = pyvips.Image.new_from_file(JPEG_FILE) + + im2 = im.stdif(10, 10) + + assert im.width == im2.width + assert im.height == im2.height + + # new mean should be closer to target mean + assert abs(im.avg() - 128) > abs(im2.avg() - 128) + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_iofuncs.py b/test/test-suite/test_iofuncs.py new file mode 100644 index 00000000..4d076f0e --- /dev/null +++ b/test/test-suite/test_iofuncs.py @@ -0,0 +1,93 @@ +# vim: set fileencoding=utf-8 : +import pytest + +import pyvips +from helpers import assert_equal_objects + + +class TestIofuncs: + # test the vips7 filename splitter ... this is very fragile and annoying + # code with lots of cases + def test_split7(self): + def split(path): + filename7 = pyvips.path_filename7(path) + mode7 = pyvips.path_mode7(path) + + return [filename7, mode7] + + cases = [ + ["c:\\silly:dir:name\\fr:ed.tif:jpeg:95,,,,c:\\icc\\srgb.icc", + ["c:\\silly:dir:name\\fr:ed.tif", + "jpeg:95,,,,c:\\icc\\srgb.icc"]], + ["I180:", + ["I180", + ""]], + ["c:\\silly:", + ["c:\\silly", + ""]], + ["c:\\program files\\x:hello", + ["c:\\program files\\x", + "hello"]], + ["C:\\fixtures\\2569067123_aca715a2ee_o.jpg", + ["C:\\fixtures\\2569067123_aca715a2ee_o.jpg", + ""]] + ] + + for case in cases: + assert_equal_objects(split(case[0]), case[1]) + + def test_new_from_image(self): + im = pyvips.Image.mask_ideal(100, 100, 0.5, + reject=True, optical=True) + + im2 = im.new_from_image(12) + + assert im2.width == im.width + assert im2.height == im.height + assert im2.interpretation == im.interpretation + assert im2.format == im.format + assert im2.xres == im.xres + assert im2.yres == im.yres + assert im2.xoffset == im.xoffset + assert im2.yoffset == im.yoffset + assert im2.bands == 1 + assert im2.avg() == 12 + + im2 = im.new_from_image([1, 2, 3]) + + assert im2.bands == 3 + assert im2.avg() == 2 + + def test_new_from_memory(self): + s = bytearray(200) + im = pyvips.Image.new_from_memory(s, 20, 10, 1, 'uchar') + + assert im.width == 20 + assert im.height == 10 + assert im.format == 'uchar' + assert im.bands == 1 + assert im.avg() == 0 + + im += 10 + + assert im.avg() == 10 + + @pytest.mark.skipif(not pyvips.at_least_libvips(8, 5), + reason="requires libvips >= 8.5") + def test_get_fields(self): + im = pyvips.Image.black(10, 10) + fields = im.get_fields() + # we might add more fields later + assert len(fields) > 10 + assert fields[0] == 'width' + + def test_write_to_memory(self): + s = bytearray(200) + im = pyvips.Image.new_from_memory(s, 20, 10, 1, 'uchar') + t = im.write_to_memory() + + assert s == t + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_morphology.py b/test/test-suite/test_morphology.py new file mode 100644 index 00000000..09eee9e0 --- /dev/null +++ b/test/test-suite/test_morphology.py @@ -0,0 +1,55 @@ +# vim: set fileencoding=utf-8 : +import pytest + +import pyvips + + +class TestMorphology: + def test_countlines(self): + im = pyvips.Image.black(100, 100) + im = im.draw_line(255, 0, 50, 100, 50) + n_lines = im.countlines(pyvips.Direction.HORIZONTAL) + assert n_lines == 1 + + def test_labelregions(self): + im = pyvips.Image.black(100, 100) + im = im.draw_circle(255, 50, 50, 25, fill=True) + mask, opts = im.labelregions(segments=True) + + assert opts['segments'] == 3 + assert mask.max() == 2 + + def test_erode(self): + im = pyvips.Image.black(100, 100) + im = im.draw_circle(255, 50, 50, 25, fill=True) + im2 = im.erode([[128, 255, 128], + [255, 255, 255], + [128, 255, 128]]) + assert im.width == im2.width + assert im.height == im2.height + assert im.bands == im2.bands + assert im.avg() > im2.avg() + + def test_dilate(self): + im = pyvips.Image.black(100, 100) + im = im.draw_circle(255, 50, 50, 25, fill=True) + im2 = im.dilate([[128, 255, 128], + [255, 255, 255], + [128, 255, 128]]) + assert im.width == im2.width + assert im.height == im2.height + assert im.bands == im2.bands + assert im2.avg() > im.avg() + + def test_rank(self): + im = pyvips.Image.black(100, 100) + im = im.draw_circle(255, 50, 50, 25, fill=True) + im2 = im.rank(3, 3, 8) + assert im.width == im2.width + assert im.height == im2.height + assert im.bands == im2.bands + assert im2.avg() > im.avg() + + +if __name__ == '__main__': + pytest.main() diff --git a/test/test-suite/test_resample.py b/test/test-suite/test_resample.py new file mode 100644 index 00000000..4e4713ae --- /dev/null +++ b/test/test-suite/test_resample.py @@ -0,0 +1,203 @@ +# vim: set fileencoding=utf-8 : +import pytest + +import pyvips +from helpers import JPEG_FILE, all_formats, have + + +# Run a function expecting a complex image on a two-band image +def run_cmplx(fn, image): + if image.format == pyvips.BandFormat.FLOAT: + new_format = pyvips.BandFormat.COMPLEX + elif image.format == pyvips.BandFormat.DOUBLE: + new_format = pyvips.BandFormat.DPCOMPLEX + else: + raise pyvips.Error("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 = pyvips.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 = pyvips.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) + + +class TestResample: + def test_affine(self): + im = pyvips.Image.new_from_file(JPEG_FILE) + + # vsqbs is non-interpolatory, don't test this way + for name in ["nearest", "bicubic", "bilinear", "nohalo", "lbb"]: + x = im + interpolate = pyvips.Interpolate.new(name) + for i in range(4): + x = x.affine([0, 1, 1, 0], interpolate=interpolate) + + assert (x - im).abs().max() == 0 + + def test_reduce(self): + im = pyvips.Image.new_from_file(JPEG_FILE) + # cast down to 0-127, the smallest range, so we aren't messed up by + # clipping + im = im.cast(pyvips.BandFormat.CHAR) + + for fac in [1, 1.1, 1.5, 1.999]: + for fmt in all_formats: + for kernel in ["nearest", "linear", + "cubic", "lanczos2", "lanczos3"]: + x = im.cast(fmt) + r = x.reduce(fac, fac, kernel=kernel) + d = abs(r.avg() - im.avg()) + assert d < 2 + + # try constant images ... should not change the constant + for const in [0, 1, 2, 254, 255]: + im = (pyvips.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()) + assert d == 0 + + def test_resize(self): + im = pyvips.Image.new_from_file(JPEG_FILE) + im2 = im.resize(0.25) + assert im2.width == round(im.width / 4.0) + assert im2.height == round(im.height / 4.0) + + # test geometry rounding corner case + im = pyvips.Image.black(100, 1) + x = im.resize(0.5) + assert x.width == 50 + assert x.height == 1 + + def test_shrink(self): + im = pyvips.Image.new_from_file(JPEG_FILE) + im2 = im.shrink(4, 4) + assert im2.width == round(im.width / 4.0) + assert im2.height == round(im.height / 4.0) + assert abs(im.avg() - im2.avg()) < 1 + + im2 = im.shrink(2.5, 2.5) + assert im2.width == round(im.width / 2.5) + assert im2.height == round(im.height / 2.5) + assert abs(im.avg() - im2.avg()) < 1 + + @pytest.mark.skipif(not pyvips.at_least_libvips(8, 5), + reason="requires libvips >= 8.5") + def test_thumbnail(self): + im = pyvips.Image.thumbnail(JPEG_FILE, 100) + + assert im.width == 100 + assert im.bands == 3 + assert im.bands == 3 + + # the average shouldn't move too much + im_orig = pyvips.Image.new_from_file(JPEG_FILE) + assert abs(im_orig.avg() - im.avg()) < 1 + + # make sure we always get the right width + for width in range(1000, 1, -13): + im = pyvips.Image.thumbnail(JPEG_FILE, width) + assert im.width == width + + # should fit one of width or height + im = pyvips.Image.thumbnail(JPEG_FILE, 100, height=300) + assert im.width == 100 + assert im.height != 300 + im = pyvips.Image.thumbnail(JPEG_FILE, 300, height=100) + assert im.width != 300 + assert im.height == 100 + + # with @crop, should fit both width and height + im = pyvips.Image.thumbnail(JPEG_FILE, 100, + height=300, crop=True) + assert im.width == 100 + assert im.height == 300 + + im1 = pyvips.Image.thumbnail(JPEG_FILE, 100) + with open(JPEG_FILE, 'rb') as f: + buf = f.read() + im2 = pyvips.Image.thumbnail_buffer(buf, 100) + assert abs(im1.avg() - im2.avg()) < 1 + + def test_similarity(self): + im = pyvips.Image.new_from_file(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 + assert (im2 - im3).abs().max() < 50 + + def test_similarity_scale(self): + im = pyvips.Image.new_from_file(JPEG_FILE) + im2 = im.similarity(scale=2) + im3 = im.affine([2, 0, 0, 2]) + assert (im2 - im3).abs().max() == 0 + + # added in 8.7 + def test_rotate(self): + if have("rotate"): + im = pyvips.Image.new_from_file(JPEG_FILE) + im2 = im.rotate(90) + im3 = im.affine([0, -1, 1, 0]) + # rounding in calculating the affine transform from the angle stops + # this being exactly true + assert (im2 - im3).abs().max() < 50 + + def test_mapim(self): + im = pyvips.Image.new_from_file(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) + assert (a - b).abs().max() < 20 + + +if __name__ == '__main__': + pytest.main()