Resize improvements; add ceil and gap options (#1769)

* Add a round-up option to shrink

* Only leave the final 200% to reduce

* Add gap option to reduce and resize

* Add unit tests

* Incorporate feedback
This commit is contained in:
Kleis Auke Wolthuizen 2022-05-07 13:45:38 +02:00 committed by GitHub
parent cb55fdcfd8
commit 894ed1cb13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 304 additions and 129 deletions

View File

@ -8,6 +8,8 @@
* - add @centre option * - add @centre option
* 6/6/20 kleisauke * 6/6/20 kleisauke
* - deprecate @centre option, it's now always on * - deprecate @centre option, it's now always on
* 22/4/22 kleisauke
* - add @gap option
*/ */
/* /*
@ -81,10 +83,16 @@
* Optional arguments: * Optional arguments:
* *
* * @kernel: #VipsKernel to use to interpolate (default: lanczos3) * * @kernel: #VipsKernel to use to interpolate (default: lanczos3)
* * @gap: reducing gap to use (default: 0.0)
* *
* Reduce @in vertically by a float factor. The pixels in @out are * Reduce @in vertically by a float factor. The pixels in @out are
* interpolated with a 1D mask generated by @kernel. * interpolated with a 1D mask generated by @kernel.
* *
* Set @gap to speed up reducing by having vips_shrinkv() to shrink
* with a box filter first. The bigger @gap, the closer the result
* to the fair resampling. The smaller @gap, the faster resizing.
* The default value is 0.0 (no optimization).
*
* This is a very low-level operation: see vips_resize() for a more * This is a very low-level operation: see vips_resize() for a more
* convenient way to resize images. * convenient way to resize images.
* *
@ -106,10 +114,16 @@
* Optional arguments: * Optional arguments:
* *
* * @kernel: #VipsKernel to use to interpolate (default: lanczos3) * * @kernel: #VipsKernel to use to interpolate (default: lanczos3)
* * @gap: reducing gap to use (default: 0.0)
* *
* Reduce @in horizontally by a float factor. The pixels in @out are * Reduce @in horizontally by a float factor. The pixels in @out are
* interpolated with a 1D mask generated by @kernel. * interpolated with a 1D mask generated by @kernel.
* *
* Set @gap to speed up reducing by having vips_shrinkh() to shrink
* with a box filter first. The bigger @gap, the closer the result
* to the fair resampling. The smaller @gap, the faster resizing.
* The default value is 0.0 (no optimization).
*
* This is a very low-level operation: see vips_resize() for a more * This is a very low-level operation: see vips_resize() for a more
* convenient way to resize images. * convenient way to resize images.
* *
@ -126,6 +140,7 @@ typedef struct _VipsReduce {
double hshrink; /* Shrink factors */ double hshrink; /* Shrink factors */
double vshrink; double vshrink;
double gap; /* Reduce gap */
/* The thing we use to make the kernel. /* The thing we use to make the kernel.
*/ */
@ -154,9 +169,11 @@ vips_reduce_build( VipsObject *object )
if( vips_reducev( resample->in, &t[0], reduce->vshrink, if( vips_reducev( resample->in, &t[0], reduce->vshrink,
"kernel", reduce->kernel, "kernel", reduce->kernel,
"gap", reduce->gap,
NULL ) || NULL ) ||
vips_reduceh( t[0], &t[1], reduce->hshrink, vips_reduceh( t[0], &t[1], reduce->hshrink,
"kernel", reduce->kernel, "kernel", reduce->kernel,
"gap", reduce->gap,
NULL ) || NULL ) ||
vips_image_write( t[1], resample->out ) ) vips_image_write( t[1], resample->out ) )
return( -1 ); return( -1 );
@ -203,6 +220,13 @@ vips_reduce_class_init( VipsReduceClass *class )
G_STRUCT_OFFSET( VipsReduce, kernel ), G_STRUCT_OFFSET( VipsReduce, kernel ),
VIPS_TYPE_KERNEL, VIPS_KERNEL_LANCZOS3 ); VIPS_TYPE_KERNEL, VIPS_KERNEL_LANCZOS3 );
VIPS_ARG_DOUBLE( class, "gap", 4,
_( "Gap" ),
_( "Reducing gap" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsReduce, gap ),
0.0, 1000000.0, 0.0 );
/* The old names .. now use h and v everywhere. /* The old names .. now use h and v everywhere.
*/ */
VIPS_ARG_DOUBLE( class, "xshrink", 8, VIPS_ARG_DOUBLE( class, "xshrink", 8,
@ -233,6 +257,7 @@ vips_reduce_class_init( VipsReduceClass *class )
static void static void
vips_reduce_init( VipsReduce *reduce ) vips_reduce_init( VipsReduce *reduce )
{ {
reduce->gap = 0.0;
reduce->kernel = VIPS_KERNEL_LANCZOS3; reduce->kernel = VIPS_KERNEL_LANCZOS3;
} }
@ -247,17 +272,23 @@ vips_reduce_init( VipsReduce *reduce )
* Optional arguments: * Optional arguments:
* *
* * @kernel: #VipsKernel to use to interpolate (default: lanczos3) * * @kernel: #VipsKernel to use to interpolate (default: lanczos3)
* * @gap: reducing gap to use (default: 0.0)
* *
* Reduce @in by a pair of factors with a pair of 1D kernels. This * Reduce @in by a pair of factors with a pair of 1D kernels. This
* will not work well for shrink factors greater than three. * will not work well for shrink factors greater than three.
* *
* Set @gap to speed up reducing by having vips_shrink() to shrink
* with a box filter first. The bigger @gap, the closer the result
* to the fair resampling. The smaller @gap, the faster resizing.
* The default value is 0.0 (no optimization).
*
* This is a very low-level operation: see vips_resize() for a more * This is a very low-level operation: see vips_resize() for a more
* convenient way to resize images. * convenient way to resize images.
* *
* This operation does not change xres or yres. The image resolution needs to * This operation does not change xres or yres. The image resolution needs to
* be updated by the application. * be updated by the application.
* *
* See also: vips_resize(), vips_affine(). * See also: vips_shrink(), vips_resize(), vips_affine().
* *
* Returns: 0 on success, -1 on error * Returns: 0 on success, -1 on error
*/ */

View File

@ -11,6 +11,8 @@
* 6/6/20 kleisauke * 6/6/20 kleisauke
* - deprecate @centre option, it's now always on * - deprecate @centre option, it's now always on
* - fix pixel shift * - fix pixel shift
* 22/4/22 kleisauke
* - add @gap option
*/ */
/* /*
@ -64,6 +66,7 @@ typedef struct _VipsReduceh {
VipsResample parent_instance; VipsResample parent_instance;
double hshrink; /* Reduce factor */ double hshrink; /* Reduce factor */
double gap; /* Reduce gap */
/* The thing we use to make the kernel. /* The thing we use to make the kernel.
*/ */
@ -435,23 +438,62 @@ vips_reduceh_build( VipsObject *object )
VipsResample *resample = VIPS_RESAMPLE( object ); VipsResample *resample = VIPS_RESAMPLE( object );
VipsReduceh *reduceh = (VipsReduceh *) object; VipsReduceh *reduceh = (VipsReduceh *) object;
VipsImage **t = (VipsImage **) VipsImage **t = (VipsImage **)
vips_object_local_array( object, 2 ); vips_object_local_array( object, 3 );
VipsImage *in; VipsImage *in;
double width, extra_pixels; int width;
int int_hshrink;
double extra_pixels;
if( VIPS_OBJECT_CLASS( vips_reduceh_parent_class )->build( object ) ) if( VIPS_OBJECT_CLASS( vips_reduceh_parent_class )->build( object ) )
return( -1 ); return( -1 );
in = resample->in; in = resample->in;
if( reduceh->hshrink < 1 ) { if( reduceh->hshrink < 1.0 ) {
vips_error( object_class->nickname, vips_error( object_class->nickname,
"%s", _( "reduce factors should be >= 1" ) ); "%s", _( "reduce factor should be >= 1.0" ) );
return( -1 ); return( -1 );
} }
if( reduceh->hshrink == 1 ) /* Output size. We need to always round to nearest, so round(), not
* rint().
*/
width = VIPS_ROUND_UINT(
(double) in->Xsize / reduceh->hshrink );
/* How many pixels we are inventing in the input, -ve for
* discarding.
*/
extra_pixels = width * reduceh->hshrink - in->Xsize;
if( reduceh->gap > 0.0 &&
reduceh->kernel != VIPS_KERNEL_NEAREST ) {
if( reduceh->gap < 1.0 ) {
vips_error( object_class->nickname,
"%s", _( "reduce gap should be >= 1.0" ) );
return( -1 );
}
/* The int part of our reduce.
*/
int_hshrink = VIPS_MAX( 1,
VIPS_FLOOR( (double) in->Xsize / width / reduceh->gap ) );
if( int_hshrink > 1 ) {
g_info( "shrinkh by %d", int_hshrink );
if( vips_shrinkh( in, &t[0], int_hshrink,
"ceil", TRUE,
NULL ) )
return( -1 );
in = t[0];
reduceh->hshrink /= int_hshrink;
extra_pixels /= int_hshrink;
}
}
if( reduceh->hshrink == 1.0 )
return( vips_image_write( in, resample->out ) ); return( vips_image_write( in, resample->out ) );
reduceh->n_point = reduceh->n_point =
@ -463,18 +505,6 @@ vips_reduceh_build( VipsObject *object )
return( -1 ); return( -1 );
} }
/* Output size. We need to always round to nearest, so round(), not
* rint().
*/
width = VIPS_ROUND_UINT(
(double) resample->in->Xsize / reduceh->hshrink );
/* How many pixels we are inventing in the input, -ve for
* discarding.
*/
extra_pixels =
width * reduceh->hshrink - resample->in->Xsize;
/* If we are rounding down, we are not using some input /* If we are rounding down, we are not using some input
* pixels. We need to move the origin *inside* the input image * pixels. We need to move the origin *inside* the input image
* by half that distance so that we discard pixels equally * by half that distance so that we discard pixels equally
@ -511,28 +541,25 @@ vips_reduceh_build( VipsObject *object )
/* Unpack for processing. /* Unpack for processing.
*/ */
if( vips_image_decode( in, &t[0] ) ) if( vips_image_decode( in, &t[1] ) )
return( -1 ); return( -1 );
in = t[0]; in = t[1];
/* Add new pixels around the input so we can interpolate at the edges. /* Add new pixels around the input so we can interpolate at the edges.
*/ */
if( vips_embed( in, &t[1], if( vips_embed( in, &t[2],
VIPS_CEIL( reduceh->n_point / 2.0 ) - 1, 0, VIPS_CEIL( reduceh->n_point / 2.0 ) - 1, 0,
in->Xsize + reduceh->n_point, in->Ysize, in->Xsize + reduceh->n_point, in->Ysize,
"extend", VIPS_EXTEND_COPY, "extend", VIPS_EXTEND_COPY,
(void *) NULL ) ) (void *) NULL ) )
return( -1 ); return( -1 );
in = t[1]; in = t[2];
if( vips_image_pipelinev( resample->out, if( vips_image_pipelinev( resample->out,
VIPS_DEMAND_STYLE_THINSTRIP, in, (void *) NULL ) ) VIPS_DEMAND_STYLE_THINSTRIP, in, (void *) NULL ) )
return( -1 ); return( -1 );
/* Size output. We need to always round to nearest, so round(), not /* Don't change xres/yres, leave that to the application layer. For
* rint().
*
* Don't change xres/yres, leave that to the application layer. For
* example, vipsthumbnail knows the true reduce factor (including the * example, vipsthumbnail knows the true reduce factor (including the
* fractional part), we just see the integer part here. * fractional part), we just see the integer part here.
*/ */
@ -583,7 +610,7 @@ vips_reduceh_class_init( VipsReducehClass *reduceh_class )
_( "Horizontal shrink factor" ), _( "Horizontal shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT, VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsReduceh, hshrink ), G_STRUCT_OFFSET( VipsReduceh, hshrink ),
1, 1000000, 1 ); 1.0, 1000000.0, 1.0 );
VIPS_ARG_ENUM( reduceh_class, "kernel", 4, VIPS_ARG_ENUM( reduceh_class, "kernel", 4,
_( "Kernel" ), _( "Kernel" ),
@ -592,6 +619,13 @@ vips_reduceh_class_init( VipsReducehClass *reduceh_class )
G_STRUCT_OFFSET( VipsReduceh, kernel ), G_STRUCT_OFFSET( VipsReduceh, kernel ),
VIPS_TYPE_KERNEL, VIPS_KERNEL_LANCZOS3 ); VIPS_TYPE_KERNEL, VIPS_KERNEL_LANCZOS3 );
VIPS_ARG_DOUBLE( reduceh_class, "gap", 5,
_( "Gap" ),
_( "Reducing gap" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsReduceh, gap ),
0.0, 1000000.0, 0.0 );
/* Old name. /* Old name.
*/ */
VIPS_ARG_DOUBLE( reduceh_class, "xshrink", 3, VIPS_ARG_DOUBLE( reduceh_class, "xshrink", 3,
@ -599,7 +633,7 @@ vips_reduceh_class_init( VipsReducehClass *reduceh_class )
_( "Horizontal shrink factor" ), _( "Horizontal shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED, VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED,
G_STRUCT_OFFSET( VipsReduceh, hshrink ), G_STRUCT_OFFSET( VipsReduceh, hshrink ),
1, 1000000, 1 ); 1.0, 1000000.0, 1.0 );
/* We used to let people pick centre or corner, but it's automatic now. /* We used to let people pick centre or corner, but it's automatic now.
*/ */
@ -615,6 +649,7 @@ vips_reduceh_class_init( VipsReducehClass *reduceh_class )
static void static void
vips_reduceh_init( VipsReduceh *reduceh ) vips_reduceh_init( VipsReduceh *reduceh )
{ {
reduceh->gap = 0.0;
reduceh->kernel = VIPS_KERNEL_LANCZOS3; reduceh->kernel = VIPS_KERNEL_LANCZOS3;
} }

View File

@ -21,6 +21,8 @@
* - deprecate @centre option, it's now always on * - deprecate @centre option, it's now always on
* - fix pixel shift * - fix pixel shift
* - speed up the mask construction for uchar/ushort images * - speed up the mask construction for uchar/ushort images
* 22/4/22 kleisauke
* - add @gap option
*/ */
/* /*
@ -102,7 +104,8 @@ typedef struct {
typedef struct _VipsReducev { typedef struct _VipsReducev {
VipsResample parent_instance; VipsResample parent_instance;
double vshrink; /* Shrink factor */ double vshrink; /* Reduce factor */
double gap; /* Reduce gap */
/* The thing we use to make the kernel. /* The thing we use to make the kernel.
*/ */
@ -734,10 +737,10 @@ vips_reducev_vector_gen( VipsRegion *out_region, void *vseq,
} }
static int static int
vips_reducev_raw( VipsReducev *reducev, VipsImage *in, VipsImage **out ) vips_reducev_raw( VipsReducev *reducev, VipsImage *in, int height,
VipsImage **out )
{ {
VipsObjectClass *object_class = VIPS_OBJECT_GET_CLASS( reducev ); VipsObjectClass *object_class = VIPS_OBJECT_GET_CLASS( reducev );
VipsResample *resample = VIPS_RESAMPLE( reducev );
VipsGenerateFn generate; VipsGenerateFn generate;
@ -771,15 +774,11 @@ vips_reducev_raw( VipsReducev *reducev, VipsImage *in, VipsImage **out )
VIPS_DEMAND_STYLE_THINSTRIP, in, (void *) NULL ) ) VIPS_DEMAND_STYLE_THINSTRIP, in, (void *) NULL ) )
return( -1 ); return( -1 );
/* Size output. We need to always round to nearest, so round(), not /* Don't change xres/yres, leave that to the application layer. For
* rint().
*
* Don't change xres/yres, leave that to the application layer. For
* example, vipsthumbnail knows the true reduce factor (including the * example, vipsthumbnail knows the true reduce factor (including the
* fractional part), we just see the integer part here. * fractional part), we just see the integer part here.
*/ */
(*out)->Ysize = VIPS_ROUND_UINT( (*out)->Ysize = height;
resample->in->Ysize / reducev->vshrink );
if( (*out)->Ysize <= 0 ) { if( (*out)->Ysize <= 0 ) {
vips_error( object_class->nickname, vips_error( object_class->nickname,
"%s", _( "image has shrunk to nothing" ) ); "%s", _( "image has shrunk to nothing" ) );
@ -808,23 +807,62 @@ vips_reducev_build( VipsObject *object )
VipsObjectClass *object_class = VIPS_OBJECT_GET_CLASS( object ); VipsObjectClass *object_class = VIPS_OBJECT_GET_CLASS( object );
VipsResample *resample = VIPS_RESAMPLE( object ); VipsResample *resample = VIPS_RESAMPLE( object );
VipsReducev *reducev = (VipsReducev *) object; VipsReducev *reducev = (VipsReducev *) object;
VipsImage **t = (VipsImage **) vips_object_local_array( object, 4 ); VipsImage **t = (VipsImage **) vips_object_local_array( object, 5 );
VipsImage *in; VipsImage *in;
double height, extra_pixels; int height;
int int_vshrink;
double extra_pixels;
if( VIPS_OBJECT_CLASS( vips_reducev_parent_class )->build( object ) ) if( VIPS_OBJECT_CLASS( vips_reducev_parent_class )->build( object ) )
return( -1 ); return( -1 );
in = resample->in; in = resample->in;
if( reducev->vshrink < 1 ) { if( reducev->vshrink < 1.0 ) {
vips_error( object_class->nickname, vips_error( object_class->nickname,
"%s", _( "reduce factor should be >= 1" ) ); "%s", _( "reduce factor should be >= 1.0" ) );
return( -1 ); return( -1 );
} }
if( reducev->vshrink == 1 ) /* Output size. We need to always round to nearest, so round(), not
* rint().
*/
height = VIPS_ROUND_UINT(
(double) in->Ysize / reducev->vshrink );
/* How many pixels we are inventing in the input, -ve for
* discarding.
*/
extra_pixels = height * reducev->vshrink - in->Ysize;
if( reducev->gap > 0.0 &&
reducev->kernel != VIPS_KERNEL_NEAREST ) {
if( reducev->gap < 1.0 ) {
vips_error( object_class->nickname,
"%s", _( "reduce gap should be >= 1.0" ) );
return( -1 );
}
/* The int part of our reduce.
*/
int_vshrink = VIPS_MAX( 1,
VIPS_FLOOR( (double) in->Ysize / height / reducev->gap ) );
if( int_vshrink > 1 ) {
g_info( "shrinkv by %d", int_vshrink );
if( vips_shrinkv( in, &t[0], int_vshrink,
"ceil", TRUE,
NULL ) )
return( -1 );
in = t[0];
reducev->vshrink /= int_vshrink;
extra_pixels /= int_vshrink;
}
}
if( reducev->vshrink == 1.0 )
return( vips_image_write( in, resample->out ) ); return( vips_image_write( in, resample->out ) );
reducev->n_point = reducev->n_point =
@ -836,18 +874,6 @@ vips_reducev_build( VipsObject *object )
return( -1 ); return( -1 );
} }
/* Output size. We need to always round to nearest, so round(), not
* rint().
*/
height = VIPS_ROUND_UINT(
(double) resample->in->Ysize / reducev->vshrink );
/* How many pixels we are inventing in the input, -ve for
* discarding.
*/
extra_pixels =
height * reducev->vshrink - resample->in->Ysize;
/* If we are rounding down, we are not using some input /* If we are rounding down, we are not using some input
* pixels. We need to move the origin *inside* the input image * pixels. We need to move the origin *inside* the input image
* by half that distance so that we discard pixels equally * by half that distance so that we discard pixels equally
@ -884,24 +910,24 @@ vips_reducev_build( VipsObject *object )
/* Unpack for processing. /* Unpack for processing.
*/ */
if( vips_image_decode( in, &t[0] ) ) if( vips_image_decode( in, &t[1] ) )
return( -1 ); return( -1 );
in = t[0]; in = t[1];
/* Add new pixels around the input so we can interpolate at the edges. /* Add new pixels around the input so we can interpolate at the edges.
*/ */
if( vips_embed( in, &t[1], if( vips_embed( in, &t[2],
0, VIPS_CEIL( reducev->n_point / 2.0 ) - 1, 0, VIPS_CEIL( reducev->n_point / 2.0 ) - 1,
in->Xsize, in->Ysize + reducev->n_point, in->Xsize, in->Ysize + reducev->n_point,
"extend", VIPS_EXTEND_COPY, "extend", VIPS_EXTEND_COPY,
(void *) NULL ) ) (void *) NULL ) )
return( -1 ); return( -1 );
in = t[1];
if( vips_reducev_raw( reducev, in, &t[2] ) )
return( -1 );
in = t[2]; in = t[2];
if( vips_reducev_raw( reducev, in, height, &t[3] ) )
return( -1 );
in = t[3];
/* Large reducev will throw off sequential mode. Suppose thread1 is /* Large reducev will throw off sequential mode. Suppose thread1 is
* generating tile (0, 0), but stalls. thread2 generates tile * generating tile (0, 0), but stalls. thread2 generates tile
* (0, 1), 128 lines further down the output. After it has done, * (0, 1), 128 lines further down the output. After it has done,
@ -915,12 +941,12 @@ vips_reducev_build( VipsObject *object )
if( vips_image_get_typeof( in, VIPS_META_SEQUENTIAL ) ) { if( vips_image_get_typeof( in, VIPS_META_SEQUENTIAL ) ) {
g_info( "reducev sequential line cache" ); g_info( "reducev sequential line cache" );
if( vips_sequential( in, &t[3], if( vips_sequential( in, &t[4],
"tile_height", 10, "tile_height", 10,
// "trace", TRUE, // "trace", TRUE,
(void *) NULL ) ) (void *) NULL ) )
return( -1 ); return( -1 );
in = t[3]; in = t[4];
} }
if( vips_image_write( in, resample->out ) ) if( vips_image_write( in, resample->out ) )
@ -954,7 +980,7 @@ vips_reducev_class_init( VipsReducevClass *reducev_class )
_( "Vertical shrink factor" ), _( "Vertical shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT, VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsReducev, vshrink ), G_STRUCT_OFFSET( VipsReducev, vshrink ),
1, 1000000, 1 ); 1.0, 1000000.0, 1.0 );
VIPS_ARG_ENUM( reducev_class, "kernel", 4, VIPS_ARG_ENUM( reducev_class, "kernel", 4,
_( "Kernel" ), _( "Kernel" ),
@ -963,6 +989,13 @@ vips_reducev_class_init( VipsReducevClass *reducev_class )
G_STRUCT_OFFSET( VipsReducev, kernel ), G_STRUCT_OFFSET( VipsReducev, kernel ),
VIPS_TYPE_KERNEL, VIPS_KERNEL_LANCZOS3 ); VIPS_TYPE_KERNEL, VIPS_KERNEL_LANCZOS3 );
VIPS_ARG_DOUBLE( reducev_class, "gap", 5,
_( "Gap" ),
_( "Reducing gap" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsReducev, gap ),
0.0, 1000000.0, 0.0 );
/* Old name. /* Old name.
*/ */
VIPS_ARG_DOUBLE( reducev_class, "yshrink", 3, VIPS_ARG_DOUBLE( reducev_class, "yshrink", 3,
@ -970,7 +1003,7 @@ vips_reducev_class_init( VipsReducevClass *reducev_class )
_( "Vertical shrink factor" ), _( "Vertical shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED, VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED,
G_STRUCT_OFFSET( VipsReducev, vshrink ), G_STRUCT_OFFSET( VipsReducev, vshrink ),
1, 1000000, 1 ); 1.0, 1000000.0, 1.0 );
/* We used to let people pick centre or corner, but it's automatic now. /* We used to let people pick centre or corner, but it's automatic now.
*/ */
@ -986,6 +1019,7 @@ vips_reducev_class_init( VipsReducevClass *reducev_class )
static void static void
vips_reducev_init( VipsReducev *reducev ) vips_reducev_init( VipsReducev *reducev )
{ {
reducev->gap = 0.0;
reducev->kernel = VIPS_KERNEL_LANCZOS3; reducev->kernel = VIPS_KERNEL_LANCZOS3;
} }

View File

@ -35,6 +35,8 @@
* - don't let either axis drop below 1px * - don't let either axis drop below 1px
* 12/7/20 * 12/7/20
* - much better handling of "nearest" * - much better handling of "nearest"
* 22/4/22 kleisauke
* - add @gap option
*/ */
/* /*
@ -92,6 +94,7 @@ typedef struct _VipsResize {
double scale; double scale;
double vscale; double vscale;
double gap;
VipsKernel kernel; VipsKernel kernel;
/* Deprecated. /* Deprecated.
@ -134,7 +137,7 @@ vips_resize_build( VipsObject *object )
VipsResample *resample = VIPS_RESAMPLE( object ); VipsResample *resample = VIPS_RESAMPLE( object );
VipsResize *resize = (VipsResize *) object; VipsResize *resize = (VipsResize *) object;
VipsImage **t = (VipsImage **) vips_object_local_array( object, 7 ); VipsImage **t = (VipsImage **) vips_object_local_array( object, 5 );
VipsImage *in; VipsImage *in;
double hscale; double hscale;
@ -155,48 +158,46 @@ vips_resize_build( VipsObject *object )
else else
vscale = resize->scale; vscale = resize->scale;
/* The int part of our scale. Leave the final 200 - 300% to reduce.
*/
int_hshrink = VIPS_MAX( 1, VIPS_FLOOR( 1.0 / (hscale * 2) ) );
int_vshrink = VIPS_MAX( 1, VIPS_FLOOR( 1.0 / (vscale * 2) ) );
/* Unpack for processing. /* Unpack for processing.
*/ */
if( vips_image_decode( in, &t[5] ) ) if( vips_image_decode( in, &t[0] ) )
return( -1 ); return( -1 );
in = t[5]; in = t[0];
if( resize->kernel == VIPS_KERNEL_NEAREST ) { if( resize->kernel == VIPS_KERNEL_NEAREST ) {
int target_width;
int target_height;
/* The int part of our scale.
*/
if( resize->gap < 1.0 ) {
int_hshrink = VIPS_FLOOR( 1.0 / hscale );
int_vshrink = VIPS_FLOOR( 1.0 / vscale );
}
else {
target_width = VIPS_ROUND_UINT( in->Xsize * hscale );
target_height = VIPS_ROUND_UINT( in->Ysize * vscale );
int_hshrink =
VIPS_FLOOR( (double) in->Xsize / target_width / resize->gap );
int_vshrink =
VIPS_FLOOR( (double) in->Ysize / target_height / resize->gap );
}
int_hshrink = VIPS_MAX( 1, int_hshrink );
int_vshrink = VIPS_MAX( 1, int_vshrink );
if( int_vshrink > 1 || if( int_vshrink > 1 ||
int_hshrink > 1 ) { int_hshrink > 1 ) {
g_info( "subsample by %d, %d", g_info( "subsample by %d, %d",
int_hshrink, int_vshrink ); int_hshrink, int_vshrink );
if( vips_subsample( in, &t[0], if( vips_subsample( in, &t[1],
int_hshrink, int_vshrink, NULL ) ) int_hshrink, int_vshrink, NULL ) )
return( -1 ); return( -1 );
in = t[0];
hscale *= int_hshrink;
vscale *= int_vshrink;
}
}
else {
if( int_vshrink > 1 ) {
g_info( "shrinkv by %d", int_vshrink );
if( vips_shrinkv( in, &t[0], int_vshrink, NULL ) )
return( -1 );
in = t[0];
vscale *= int_vshrink;
}
if( int_hshrink > 1 ) {
g_info( "shrinkh by %d", int_hshrink );
if( vips_shrinkh( in, &t[1], int_hshrink, NULL ) )
return( -1 );
in = t[1]; in = t[1];
hscale *= int_hshrink; hscale *= int_hshrink;
vscale *= int_vshrink;
} }
} }
@ -211,16 +212,17 @@ vips_resize_build( VipsObject *object )
g_info( "residual reducev by %g", vscale ); g_info( "residual reducev by %g", vscale );
if( vips_reducev( in, &t[2], 1.0 / vscale, if( vips_reducev( in, &t[2], 1.0 / vscale,
"kernel", resize->kernel, "kernel", resize->kernel,
"gap", resize->gap,
NULL ) ) NULL ) )
return( -1 ); return( -1 );
in = t[2]; in = t[2];
} }
if( hscale < 1.0 ) { if( hscale < 1.0 ) {
g_info( "residual reduceh by %g", g_info( "residual reduceh by %g", hscale );
hscale );
if( vips_reduceh( in, &t[3], 1.0 / hscale, if( vips_reduceh( in, &t[3], 1.0 / hscale,
"kernel", resize->kernel, "kernel", resize->kernel,
"gap", resize->gap,
NULL ) ) NULL ) )
return( -1 ); return( -1 );
in = t[3]; in = t[3];
@ -326,14 +328,14 @@ vips_resize_class_init( VipsResizeClass *class )
_( "Scale image by this factor" ), _( "Scale image by this factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT, VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsResize, scale ), G_STRUCT_OFFSET( VipsResize, scale ),
0, 10000000, 0 ); 0.0, 10000000.0, 0.0 );
VIPS_ARG_DOUBLE( class, "vscale", 113, VIPS_ARG_DOUBLE( class, "vscale", 113,
_( "Vertical scale factor" ), _( "Vertical scale factor" ),
_( "Vertical scale image by this factor" ), _( "Vertical scale image by this factor" ),
VIPS_ARGUMENT_OPTIONAL_INPUT, VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsResize, vscale ), G_STRUCT_OFFSET( VipsResize, vscale ),
0, 10000000, 0 ); 0.0, 10000000.0, 0.0 );
VIPS_ARG_ENUM( class, "kernel", 3, VIPS_ARG_ENUM( class, "kernel", 3,
_( "Kernel" ), _( "Kernel" ),
@ -342,6 +344,13 @@ vips_resize_class_init( VipsResizeClass *class )
G_STRUCT_OFFSET( VipsResize, kernel ), G_STRUCT_OFFSET( VipsResize, kernel ),
VIPS_TYPE_KERNEL, VIPS_KERNEL_LANCZOS3 ); VIPS_TYPE_KERNEL, VIPS_KERNEL_LANCZOS3 );
VIPS_ARG_DOUBLE( class, "gap", 4,
_( "Gap" ),
_( "Reducing gap" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsResize, gap ),
0.0, 1000000.0, 2.0 );
/* We used to let people set the input offset so you could pick centre /* We used to let people set the input offset so you could pick centre
* or corner interpolation, but it's not clear this was useful. * or corner interpolation, but it's not clear this was useful.
*/ */
@ -350,14 +359,14 @@ vips_resize_class_init( VipsResizeClass *class )
_( "Horizontal input displacement" ), _( "Horizontal input displacement" ),
VIPS_ARGUMENT_OPTIONAL_INPUT | VIPS_ARGUMENT_DEPRECATED, VIPS_ARGUMENT_OPTIONAL_INPUT | VIPS_ARGUMENT_DEPRECATED,
G_STRUCT_OFFSET( VipsResize, idx ), G_STRUCT_OFFSET( VipsResize, idx ),
-10000000, 10000000, 0 ); -10000000.0, 10000000.0, 0.0 );
VIPS_ARG_DOUBLE( class, "idy", 116, VIPS_ARG_DOUBLE( class, "idy", 116,
_( "Input offset" ), _( "Input offset" ),
_( "Vertical input displacement" ), _( "Vertical input displacement" ),
VIPS_ARGUMENT_OPTIONAL_INPUT | VIPS_ARGUMENT_DEPRECATED, VIPS_ARGUMENT_OPTIONAL_INPUT | VIPS_ARGUMENT_DEPRECATED,
G_STRUCT_OFFSET( VipsResize, idy ), G_STRUCT_OFFSET( VipsResize, idy ),
-10000000, 10000000, 0 ); -10000000.0, 10000000.0, 0.0 );
/* It's a kernel now we use vips_reduce() not vips_affine(). /* It's a kernel now we use vips_reduce() not vips_affine().
*/ */
@ -381,6 +390,7 @@ vips_resize_class_init( VipsResizeClass *class )
static void static void
vips_resize_init( VipsResize *resize ) vips_resize_init( VipsResize *resize )
{ {
resize->gap = 2.0;
resize->kernel = VIPS_KERNEL_LANCZOS3; resize->kernel = VIPS_KERNEL_LANCZOS3;
} }
@ -395,18 +405,18 @@ vips_resize_init( VipsResize *resize )
* *
* * @vscale: %gdouble vertical scale factor * * @vscale: %gdouble vertical scale factor
* * @kernel: #VipsKernel to reduce with * * @kernel: #VipsKernel to reduce with
* * @gap: reducing gap to use (default: 2.0)
* *
* Resize an image. * Resize an image.
* *
* When downsizing, the * Set @gap to speed up downsizing by having vips_shrink() to shrink
* image is block-shrunk with vips_shrink(), * with a box filter first. The bigger @gap, the closer the result
* then the image is shrunk again to the * to the fair resampling. The smaller @gap, the faster resizing.
* target size with vips_reduce(). How much is done by vips_shrink() vs. * The default value is 2.0 (very close to fair resampling
* vips_reduce() varies with the @kernel setting. Downsizing is done with * while still being faster in many cases).
* centre convention.
* *
* vips_resize() normally uses #VIPS_KERNEL_LANCZOS3 for the final reduce, you * vips_resize() normally uses #VIPS_KERNEL_LANCZOS3 for the final reduce, you
* can change this with @kernel. * can change this with @kernel. Downsizing is done with centre convention.
* *
* When upsizing (@scale > 1), the operation uses vips_affine() with * When upsizing (@scale > 1), the operation uses vips_affine() with
* a #VipsInterpolate selected depending on @kernel. It will use * a #VipsInterpolate selected depending on @kernel. It will use

View File

@ -10,6 +10,8 @@
* 9/2/17 * 9/2/17
* - use reduce, not affine, for any residual shrink * - use reduce, not affine, for any residual shrink
* - expand cache hint * - expand cache hint
* 22/4/22 kleisauke
* - add @ceil option
*/ */
/* /*
@ -63,6 +65,7 @@ typedef struct _VipsShrink {
double hshrink; /* Shrink factors */ double hshrink; /* Shrink factors */
double vshrink; double vshrink;
gboolean ceil; /* Round operation */
} VipsShrink; } VipsShrink;
@ -76,7 +79,7 @@ vips_shrink_build( VipsObject *object )
VipsResample *resample = VIPS_RESAMPLE( object ); VipsResample *resample = VIPS_RESAMPLE( object );
VipsShrink *shrink = (VipsShrink *) object; VipsShrink *shrink = (VipsShrink *) object;
VipsImage **t = (VipsImage **) VipsImage **t = (VipsImage **)
vips_object_local_array( object, 3 ); vips_object_local_array( object, 2 );
int hshrink_int; int hshrink_int;
int vshrink_int; int vshrink_int;
@ -91,20 +94,22 @@ vips_shrink_build( VipsObject *object )
vshrink_int != shrink->vshrink ) { vshrink_int != shrink->vshrink ) {
/* Shrink by int factors, reduce to final size. /* Shrink by int factors, reduce to final size.
*/ */
double xresidual = shrink->hshrink / hshrink_int; if( vips_reducev( resample->in, &t[0], shrink->vshrink,
double yresidual = shrink->vshrink / vshrink_int; "gap", 1.0,
NULL ) ||
if( vips_shrinkv( resample->in, &t[0], vshrink_int, NULL ) || vips_reduceh( t[0], &t[1], shrink->hshrink,
vips_shrinkh( t[0], &t[1], hshrink_int, NULL ) ) "gap", 1.0,
return( -1 ); NULL ) ||
vips_image_write( t[1], resample->out ) )
if( vips_reduce( t[1], &t[2], xresidual, yresidual, NULL ) ||
vips_image_write( t[2], resample->out ) )
return( -1 ); return( -1 );
} }
else { else {
if( vips_shrinkv( resample->in, &t[0], shrink->vshrink, NULL ) || if( vips_shrinkv( resample->in, &t[0], shrink->vshrink,
vips_shrinkh( t[0], &t[1], shrink->hshrink, NULL ) || "ceil", shrink->ceil,
NULL ) ||
vips_shrinkh( t[0], &t[1], shrink->hshrink,
"ceil", shrink->ceil,
NULL ) ||
vips_image_write( t[1], resample->out ) ) vips_image_write( t[1], resample->out ) )
return( -1 ); return( -1 );
} }
@ -147,6 +152,13 @@ vips_shrink_class_init( VipsShrinkClass *class )
G_STRUCT_OFFSET( VipsShrink, hshrink ), G_STRUCT_OFFSET( VipsShrink, hshrink ),
1.0, 1000000.0, 1.0 ); 1.0, 1000000.0, 1.0 );
VIPS_ARG_BOOL( class, "ceil", 10,
_( "Ceil" ),
_( "Round-up output dimensions" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsShrink, ceil ),
FALSE );
/* The old names .. now use h and v everywhere. /* The old names .. now use h and v everywhere.
*/ */
VIPS_ARG_DOUBLE( class, "xshrink", 8, VIPS_ARG_DOUBLE( class, "xshrink", 8,
@ -178,6 +190,10 @@ vips_shrink_init( VipsShrink *shrink )
* @vshrink: vertical shrink * @vshrink: vertical shrink
* @...: %NULL-terminated list of optional named arguments * @...: %NULL-terminated list of optional named arguments
* *
* Optional arguments:
*
* * @ceil: round-up output dimensions
*
* Shrink @in by a pair of factors with a simple box filter. For non-integer * Shrink @in by a pair of factors with a simple box filter. For non-integer
* factors, vips_shrink() will first shrink by the integer part with a box * factors, vips_shrink() will first shrink by the integer part with a box
* filter, then use vips_reduce() to shrink by the * filter, then use vips_reduce() to shrink by the

View File

@ -8,6 +8,8 @@
* - rename xshrink -> hshrink for greater consistency * - rename xshrink -> hshrink for greater consistency
* 6/8/19 * 6/8/19
* - use a double sum buffer for int32 types * - use a double sum buffer for int32 types
* 22/4/22 kleisauke
* - add @ceil option
*/ */
/* /*
@ -60,6 +62,7 @@ typedef struct _VipsShrinkh {
VipsResample parent_instance; VipsResample parent_instance;
int hshrink; /* Shrink factor */ int hshrink; /* Shrink factor */
gboolean ceil; /* Round operation */
} VipsShrinkh; } VipsShrinkh;
@ -267,15 +270,15 @@ vips_shrinkh_build( VipsObject *object )
VIPS_DEMAND_STYLE_THINSTRIP, in, NULL ) ) VIPS_DEMAND_STYLE_THINSTRIP, in, NULL ) )
return( -1 ); return( -1 );
/* Size output. We need to always round to nearest, so round(), not /* Size output.
* rint().
* *
* Don't change xres/yres, leave that to the application layer. For * Don't change xres/yres, leave that to the application layer. For
* example, vipsthumbnail knows the true shrink factor (including the * example, vipsthumbnail knows the true shrink factor (including the
* fractional part), we just see the integer part here. * fractional part), we just see the integer part here.
*/ */
resample->out->Xsize = VIPS_ROUND_UINT( resample->out->Xsize = shrink->ceil ?
(double) resample->in->Xsize / shrink->hshrink ); VIPS_CEIL( (double) resample->in->Xsize / shrink->hshrink ) :
VIPS_ROUND_UINT( (double) resample->in->Xsize / shrink->hshrink );
if( resample->out->Xsize <= 0 ) { if( resample->out->Xsize <= 0 ) {
vips_error( class->nickname, vips_error( class->nickname,
"%s", _( "image has shrunk to nothing" ) ); "%s", _( "image has shrunk to nothing" ) );
@ -321,6 +324,13 @@ vips_shrinkh_class_init( VipsShrinkhClass *class )
G_STRUCT_OFFSET( VipsShrinkh, hshrink ), G_STRUCT_OFFSET( VipsShrinkh, hshrink ),
1, 1000000, 1 ); 1, 1000000, 1 );
VIPS_ARG_BOOL( class, "ceil", 10,
_( "Ceil" ),
_( "Round-up output dimensions" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsShrinkh, ceil ),
FALSE );
/* The old name .. now use h and v everywhere. /* The old name .. now use h and v everywhere.
*/ */
VIPS_ARG_INT( class, "xshrink", 8, VIPS_ARG_INT( class, "xshrink", 8,
@ -344,6 +354,10 @@ vips_shrinkh_init( VipsShrinkh *shrink )
* @hshrink: horizontal shrink * @hshrink: horizontal shrink
* @...: %NULL-terminated list of optional named arguments * @...: %NULL-terminated list of optional named arguments
* *
* Optional arguments:
*
* * @ceil: round-up output dimensions
*
* Shrink @in horizontally by an integer factor. Each pixel in the output is * Shrink @in horizontally by an integer factor. Each pixel in the output is
* the average of the corresponding line of @hshrink pixels in the input. * the average of the corresponding line of @hshrink pixels in the input.
* *

View File

@ -47,6 +47,8 @@
* - add a seq line cache * - add a seq line cache
* 6/8/19 * 6/8/19
* - use a double sum buffer for int32 types * - use a double sum buffer for int32 types
* 22/4/22 kleisauke
* - add @ceil option
*/ */
/* /*
@ -99,8 +101,9 @@
typedef struct _VipsShrinkv { typedef struct _VipsShrinkv {
VipsResample parent_instance; VipsResample parent_instance;
int vshrink; int vshrink; /* Shrink factor */
size_t sizeof_line_buffer; size_t sizeof_line_buffer;
gboolean ceil; /* Round operation */
} VipsShrinkv; } VipsShrinkv;
@ -372,15 +375,15 @@ vips_shrinkv_build( VipsObject *object )
VIPS_DEMAND_STYLE_SMALLTILE, in, NULL ) ) VIPS_DEMAND_STYLE_SMALLTILE, in, NULL ) )
return( -1 ); return( -1 );
/* Size output. We need to always round to nearest, so round(), not /* Size output.
* rint().
* *
* Don't change xres/yres, leave that to the application layer. For * Don't change xres/yres, leave that to the application layer. For
* example, vipsthumbnail knows the true shrink factor (including the * example, vipsthumbnail knows the true shrink factor (including the
* fractional part), we just see the integer part here. * fractional part), we just see the integer part here.
*/ */
t[2]->Ysize = VIPS_ROUND_UINT( t[2]->Ysize = shrink->ceil ?
(double) resample->in->Ysize / shrink->vshrink ); VIPS_CEIL( (double) resample->in->Ysize / shrink->vshrink ) :
VIPS_ROUND_UINT( (double) resample->in->Ysize / shrink->vshrink );
if( t[2]->Ysize <= 0 ) { if( t[2]->Ysize <= 0 ) {
vips_error( class->nickname, vips_error( class->nickname,
"%s", _( "image has shrunk to nothing" ) ); "%s", _( "image has shrunk to nothing" ) );
@ -451,9 +454,16 @@ vips_shrinkv_class_init( VipsShrinkvClass *class )
G_STRUCT_OFFSET( VipsShrinkv, vshrink ), G_STRUCT_OFFSET( VipsShrinkv, vshrink ),
1, 1000000, 1 ); 1, 1000000, 1 );
VIPS_ARG_BOOL( class, "ceil", 10,
_( "Ceil" ),
_( "Round-up output dimensions" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsShrinkv, ceil ),
FALSE );
/* The old name .. now use h and v everywhere. /* The old name .. now use h and v everywhere.
*/ */
VIPS_ARG_INT( class, "yshrink", 9, VIPS_ARG_INT( class, "yshrink", 8,
_( "Yshrink" ), _( "Yshrink" ),
_( "Vertical shrink factor" ), _( "Vertical shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED, VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED,
@ -474,6 +484,10 @@ vips_shrinkv_init( VipsShrinkv *shrink )
* @vshrink: vertical shrink * @vshrink: vertical shrink
* @...: %NULL-terminated list of optional named arguments * @...: %NULL-terminated list of optional named arguments
* *
* Optional arguments:
*
* * @ceil: round-up output dimensions
*
* Shrink @in vertically by an integer factor. Each pixel in the output is * Shrink @in vertically by an integer factor. Each pixel in the output is
* the average of the corresponding column of @vshrink pixels in the input. * the average of the corresponding column of @vshrink pixels in the input.
* *

View File

@ -115,6 +115,27 @@ class TestResample:
assert x.width == 50 assert x.width == 50
assert x.height == 1 assert x.height == 1
# test whether we use double-precision calculations in reduce{h,v}
im = pyvips.Image.black(1600, 1000)
x = im.resize(10.0 / im.width)
assert x.width == 10
assert x.height == 6
# test round-up option of shrink
im = pyvips.Image.black(2049 - 2, 2047 - 2, bands=3)
im = im.embed(1, 1, 2049, 2047,
extend=pyvips.Extend.BACKGROUND,
background=[255, 0, 0])
for scale in [8, 9.4, 16]:
x = im.resize(1 / scale, vscale=1 / scale)
for point in ([(round(x.width / 2), 0),
(x.width - 1, round(x.height / 2)),
(round(x.width / 2), x.height - 1),
(0, round(x.height / 2))]):
y = x(*point)[0]
assert y != 0
def test_shrink(self): def test_shrink(self):
im = pyvips.Image.new_from_file(JPEG_FILE) im = pyvips.Image.new_from_file(JPEG_FILE)
im2 = im.shrink(4, 4) im2 = im.shrink(4, 4)