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
* 6/6/20 kleisauke
* - deprecate @centre option, it's now always on
* 22/4/22 kleisauke
* - add @gap option
*/
/*
@ -81,10 +83,16 @@
* Optional arguments:
*
* * @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
* 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
* convenient way to resize images.
*
@ -106,10 +114,16 @@
* Optional arguments:
*
* * @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
* 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
* convenient way to resize images.
*
@ -126,6 +140,7 @@ typedef struct _VipsReduce {
double hshrink; /* Shrink factors */
double vshrink;
double gap; /* Reduce gap */
/* 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,
"kernel", reduce->kernel,
"gap", reduce->gap,
NULL ) ||
vips_reduceh( t[0], &t[1], reduce->hshrink,
"kernel", reduce->kernel,
"gap", reduce->gap,
NULL ) ||
vips_image_write( t[1], resample->out ) )
return( -1 );
@ -203,6 +220,13 @@ vips_reduce_class_init( VipsReduceClass *class )
G_STRUCT_OFFSET( VipsReduce, kernel ),
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.
*/
VIPS_ARG_DOUBLE( class, "xshrink", 8,
@ -233,6 +257,7 @@ vips_reduce_class_init( VipsReduceClass *class )
static void
vips_reduce_init( VipsReduce *reduce )
{
reduce->gap = 0.0;
reduce->kernel = VIPS_KERNEL_LANCZOS3;
}
@ -247,17 +272,23 @@ vips_reduce_init( VipsReduce *reduce )
* Optional arguments:
*
* * @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
* 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
* convenient way to resize images.
*
* This operation does not change xres or yres. The image resolution needs to
* 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
*/

View File

@ -11,6 +11,8 @@
* 6/6/20 kleisauke
* - deprecate @centre option, it's now always on
* - fix pixel shift
* 22/4/22 kleisauke
* - add @gap option
*/
/*
@ -64,6 +66,7 @@ typedef struct _VipsReduceh {
VipsResample parent_instance;
double hshrink; /* Reduce factor */
double gap; /* Reduce gap */
/* The thing we use to make the kernel.
*/
@ -435,23 +438,62 @@ vips_reduceh_build( VipsObject *object )
VipsResample *resample = VIPS_RESAMPLE( object );
VipsReduceh *reduceh = (VipsReduceh *) object;
VipsImage **t = (VipsImage **)
vips_object_local_array( object, 2 );
vips_object_local_array( object, 3 );
VipsImage *in;
double width, extra_pixels;
int width;
int int_hshrink;
double extra_pixels;
if( VIPS_OBJECT_CLASS( vips_reduceh_parent_class )->build( object ) )
return( -1 );
in = resample->in;
if( reduceh->hshrink < 1 ) {
if( reduceh->hshrink < 1.0 ) {
vips_error( object_class->nickname,
"%s", _( "reduce factors should be >= 1" ) );
"%s", _( "reduce factor should be >= 1.0" ) );
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 ) );
reduceh->n_point =
@ -463,18 +505,6 @@ vips_reduceh_build( VipsObject *object )
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
* pixels. We need to move the origin *inside* the input image
* by half that distance so that we discard pixels equally
@ -511,28 +541,25 @@ vips_reduceh_build( VipsObject *object )
/* Unpack for processing.
*/
if( vips_image_decode( in, &t[0] ) )
if( vips_image_decode( in, &t[1] ) )
return( -1 );
in = t[0];
in = t[1];
/* 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,
in->Xsize + reduceh->n_point, in->Ysize,
"extend", VIPS_EXTEND_COPY,
(void *) NULL ) )
return( -1 );
in = t[1];
in = t[2];
if( vips_image_pipelinev( resample->out,
VIPS_DEMAND_STYLE_THINSTRIP, in, (void *) NULL ) )
return( -1 );
/* Size output. We need to always round to nearest, so round(), not
* 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 reduce factor (including the
* fractional part), we just see the integer part here.
*/
@ -583,7 +610,7 @@ vips_reduceh_class_init( VipsReducehClass *reduceh_class )
_( "Horizontal shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsReduceh, hshrink ),
1, 1000000, 1 );
1.0, 1000000.0, 1.0 );
VIPS_ARG_ENUM( reduceh_class, "kernel", 4,
_( "Kernel" ),
@ -592,6 +619,13 @@ vips_reduceh_class_init( VipsReducehClass *reduceh_class )
G_STRUCT_OFFSET( VipsReduceh, kernel ),
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.
*/
VIPS_ARG_DOUBLE( reduceh_class, "xshrink", 3,
@ -599,7 +633,7 @@ vips_reduceh_class_init( VipsReducehClass *reduceh_class )
_( "Horizontal shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED,
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.
*/
@ -615,6 +649,7 @@ vips_reduceh_class_init( VipsReducehClass *reduceh_class )
static void
vips_reduceh_init( VipsReduceh *reduceh )
{
reduceh->gap = 0.0;
reduceh->kernel = VIPS_KERNEL_LANCZOS3;
}

View File

@ -21,6 +21,8 @@
* - deprecate @centre option, it's now always on
* - fix pixel shift
* - 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 {
VipsResample parent_instance;
double vshrink; /* Shrink factor */
double vshrink; /* Reduce factor */
double gap; /* Reduce gap */
/* The thing we use to make the kernel.
*/
@ -734,10 +737,10 @@ vips_reducev_vector_gen( VipsRegion *out_region, void *vseq,
}
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 );
VipsResample *resample = VIPS_RESAMPLE( reducev );
VipsGenerateFn generate;
@ -771,15 +774,11 @@ vips_reducev_raw( VipsReducev *reducev, VipsImage *in, VipsImage **out )
VIPS_DEMAND_STYLE_THINSTRIP, in, (void *) NULL ) )
return( -1 );
/* Size output. We need to always round to nearest, so round(), not
* 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 reduce factor (including the
* fractional part), we just see the integer part here.
*/
(*out)->Ysize = VIPS_ROUND_UINT(
resample->in->Ysize / reducev->vshrink );
(*out)->Ysize = height;
if( (*out)->Ysize <= 0 ) {
vips_error( object_class->nickname,
"%s", _( "image has shrunk to nothing" ) );
@ -808,23 +807,62 @@ vips_reducev_build( VipsObject *object )
VipsObjectClass *object_class = VIPS_OBJECT_GET_CLASS( object );
VipsResample *resample = VIPS_RESAMPLE( 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;
double height, extra_pixels;
int height;
int int_vshrink;
double extra_pixels;
if( VIPS_OBJECT_CLASS( vips_reducev_parent_class )->build( object ) )
return( -1 );
in = resample->in;
if( reducev->vshrink < 1 ) {
if( reducev->vshrink < 1.0 ) {
vips_error( object_class->nickname,
"%s", _( "reduce factor should be >= 1" ) );
"%s", _( "reduce factor should be >= 1.0" ) );
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 ) );
reducev->n_point =
@ -836,18 +874,6 @@ vips_reducev_build( VipsObject *object )
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
* pixels. We need to move the origin *inside* the input image
* by half that distance so that we discard pixels equally
@ -884,24 +910,24 @@ vips_reducev_build( VipsObject *object )
/* Unpack for processing.
*/
if( vips_image_decode( in, &t[0] ) )
if( vips_image_decode( in, &t[1] ) )
return( -1 );
in = t[0];
in = t[1];
/* 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,
in->Xsize, in->Ysize + reducev->n_point,
"extend", VIPS_EXTEND_COPY,
(void *) NULL ) )
return( -1 );
in = t[1];
if( vips_reducev_raw( reducev, in, &t[2] ) )
return( -1 );
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
* generating tile (0, 0), but stalls. thread2 generates tile
* (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 ) ) {
g_info( "reducev sequential line cache" );
if( vips_sequential( in, &t[3],
if( vips_sequential( in, &t[4],
"tile_height", 10,
// "trace", TRUE,
(void *) NULL ) )
return( -1 );
in = t[3];
in = t[4];
}
if( vips_image_write( in, resample->out ) )
@ -954,7 +980,7 @@ vips_reducev_class_init( VipsReducevClass *reducev_class )
_( "Vertical shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsReducev, vshrink ),
1, 1000000, 1 );
1.0, 1000000.0, 1.0 );
VIPS_ARG_ENUM( reducev_class, "kernel", 4,
_( "Kernel" ),
@ -963,6 +989,13 @@ vips_reducev_class_init( VipsReducevClass *reducev_class )
G_STRUCT_OFFSET( VipsReducev, kernel ),
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.
*/
VIPS_ARG_DOUBLE( reducev_class, "yshrink", 3,
@ -970,7 +1003,7 @@ vips_reducev_class_init( VipsReducevClass *reducev_class )
_( "Vertical shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED,
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.
*/
@ -986,6 +1019,7 @@ vips_reducev_class_init( VipsReducevClass *reducev_class )
static void
vips_reducev_init( VipsReducev *reducev )
{
reducev->gap = 0.0;
reducev->kernel = VIPS_KERNEL_LANCZOS3;
}

View File

@ -35,6 +35,8 @@
* - don't let either axis drop below 1px
* 12/7/20
* - much better handling of "nearest"
* 22/4/22 kleisauke
* - add @gap option
*/
/*
@ -92,6 +94,7 @@ typedef struct _VipsResize {
double scale;
double vscale;
double gap;
VipsKernel kernel;
/* Deprecated.
@ -134,7 +137,7 @@ vips_resize_build( VipsObject *object )
VipsResample *resample = VIPS_RESAMPLE( 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;
double hscale;
@ -155,48 +158,46 @@ vips_resize_build( VipsObject *object )
else
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.
*/
if( vips_image_decode( in, &t[5] ) )
if( vips_image_decode( in, &t[0] ) )
return( -1 );
in = t[5];
in = t[0];
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 ||
int_hshrink > 1 ) {
g_info( "subsample by %d, %d",
int_hshrink, int_vshrink );
if( vips_subsample( in, &t[0],
if( vips_subsample( in, &t[1],
int_hshrink, int_vshrink, NULL ) )
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];
hscale *= int_hshrink;
vscale *= int_vshrink;
}
}
@ -211,16 +212,17 @@ vips_resize_build( VipsObject *object )
g_info( "residual reducev by %g", vscale );
if( vips_reducev( in, &t[2], 1.0 / vscale,
"kernel", resize->kernel,
"gap", resize->gap,
NULL ) )
return( -1 );
in = t[2];
}
if( hscale < 1.0 ) {
g_info( "residual reduceh by %g",
hscale );
g_info( "residual reduceh by %g", hscale );
if( vips_reduceh( in, &t[3], 1.0 / hscale,
"kernel", resize->kernel,
"gap", resize->gap,
NULL ) )
return( -1 );
in = t[3];
@ -326,14 +328,14 @@ vips_resize_class_init( VipsResizeClass *class )
_( "Scale image by this factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsResize, scale ),
0, 10000000, 0 );
0.0, 10000000.0, 0.0 );
VIPS_ARG_DOUBLE( class, "vscale", 113,
_( "Vertical scale factor" ),
_( "Vertical scale image by this factor" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsResize, vscale ),
0, 10000000, 0 );
0.0, 10000000.0, 0.0 );
VIPS_ARG_ENUM( class, "kernel", 3,
_( "Kernel" ),
@ -342,6 +344,13 @@ vips_resize_class_init( VipsResizeClass *class )
G_STRUCT_OFFSET( VipsResize, kernel ),
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
* or corner interpolation, but it's not clear this was useful.
*/
@ -350,14 +359,14 @@ vips_resize_class_init( VipsResizeClass *class )
_( "Horizontal input displacement" ),
VIPS_ARGUMENT_OPTIONAL_INPUT | VIPS_ARGUMENT_DEPRECATED,
G_STRUCT_OFFSET( VipsResize, idx ),
-10000000, 10000000, 0 );
-10000000.0, 10000000.0, 0.0 );
VIPS_ARG_DOUBLE( class, "idy", 116,
_( "Input offset" ),
_( "Vertical input displacement" ),
VIPS_ARGUMENT_OPTIONAL_INPUT | VIPS_ARGUMENT_DEPRECATED,
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().
*/
@ -381,6 +390,7 @@ vips_resize_class_init( VipsResizeClass *class )
static void
vips_resize_init( VipsResize *resize )
{
resize->gap = 2.0;
resize->kernel = VIPS_KERNEL_LANCZOS3;
}
@ -395,18 +405,18 @@ vips_resize_init( VipsResize *resize )
*
* * @vscale: %gdouble vertical scale factor
* * @kernel: #VipsKernel to reduce with
* * @gap: reducing gap to use (default: 2.0)
*
* Resize an image.
*
* When downsizing, the
* image is block-shrunk with vips_shrink(),
* then the image is shrunk again to the
* target size with vips_reduce(). How much is done by vips_shrink() vs.
* vips_reduce() varies with the @kernel setting. Downsizing is done with
* centre convention.
* Set @gap to speed up downsizing 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 2.0 (very close to fair resampling
* while still being faster in many cases).
*
* 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
* a #VipsInterpolate selected depending on @kernel. It will use

View File

@ -10,6 +10,8 @@
* 9/2/17
* - use reduce, not affine, for any residual shrink
* - expand cache hint
* 22/4/22 kleisauke
* - add @ceil option
*/
/*
@ -63,6 +65,7 @@ typedef struct _VipsShrink {
double hshrink; /* Shrink factors */
double vshrink;
gboolean ceil; /* Round operation */
} VipsShrink;
@ -76,7 +79,7 @@ vips_shrink_build( VipsObject *object )
VipsResample *resample = VIPS_RESAMPLE( object );
VipsShrink *shrink = (VipsShrink *) object;
VipsImage **t = (VipsImage **)
vips_object_local_array( object, 3 );
vips_object_local_array( object, 2 );
int hshrink_int;
int vshrink_int;
@ -91,20 +94,22 @@ vips_shrink_build( VipsObject *object )
vshrink_int != shrink->vshrink ) {
/* Shrink by int factors, reduce to final size.
*/
double xresidual = shrink->hshrink / hshrink_int;
double yresidual = shrink->vshrink / vshrink_int;
if( vips_shrinkv( resample->in, &t[0], vshrink_int, NULL ) ||
vips_shrinkh( t[0], &t[1], hshrink_int, NULL ) )
return( -1 );
if( vips_reduce( t[1], &t[2], xresidual, yresidual, NULL ) ||
vips_image_write( t[2], resample->out ) )
if( vips_reducev( resample->in, &t[0], shrink->vshrink,
"gap", 1.0,
NULL ) ||
vips_reduceh( t[0], &t[1], shrink->hshrink,
"gap", 1.0,
NULL ) ||
vips_image_write( t[1], resample->out ) )
return( -1 );
}
else {
if( vips_shrinkv( resample->in, &t[0], shrink->vshrink, NULL ) ||
vips_shrinkh( t[0], &t[1], shrink->hshrink, NULL ) ||
if( vips_shrinkv( resample->in, &t[0], shrink->vshrink,
"ceil", shrink->ceil,
NULL ) ||
vips_shrinkh( t[0], &t[1], shrink->hshrink,
"ceil", shrink->ceil,
NULL ) ||
vips_image_write( t[1], resample->out ) )
return( -1 );
}
@ -147,6 +152,13 @@ vips_shrink_class_init( VipsShrinkClass *class )
G_STRUCT_OFFSET( VipsShrink, hshrink ),
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.
*/
VIPS_ARG_DOUBLE( class, "xshrink", 8,
@ -178,6 +190,10 @@ vips_shrink_init( VipsShrink *shrink )
* @vshrink: vertical shrink
* @...: %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
* factors, vips_shrink() will first shrink by the integer part with a box
* filter, then use vips_reduce() to shrink by the

View File

@ -8,6 +8,8 @@
* - rename xshrink -> hshrink for greater consistency
* 6/8/19
* - 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;
int hshrink; /* Shrink factor */
gboolean ceil; /* Round operation */
} VipsShrinkh;
@ -267,15 +270,15 @@ vips_shrinkh_build( VipsObject *object )
VIPS_DEMAND_STYLE_THINSTRIP, in, NULL ) )
return( -1 );
/* Size output. We need to always round to nearest, so round(), not
* rint().
/* Size output.
*
* Don't change xres/yres, leave that to the application layer. For
* example, vipsthumbnail knows the true shrink factor (including the
* fractional part), we just see the integer part here.
*/
resample->out->Xsize = VIPS_ROUND_UINT(
(double) resample->in->Xsize / shrink->hshrink );
resample->out->Xsize = shrink->ceil ?
VIPS_CEIL( (double) resample->in->Xsize / shrink->hshrink ) :
VIPS_ROUND_UINT( (double) resample->in->Xsize / shrink->hshrink );
if( resample->out->Xsize <= 0 ) {
vips_error( class->nickname,
"%s", _( "image has shrunk to nothing" ) );
@ -321,6 +324,13 @@ vips_shrinkh_class_init( VipsShrinkhClass *class )
G_STRUCT_OFFSET( VipsShrinkh, hshrink ),
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.
*/
VIPS_ARG_INT( class, "xshrink", 8,
@ -344,6 +354,10 @@ vips_shrinkh_init( VipsShrinkh *shrink )
* @hshrink: horizontal shrink
* @...: %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
* the average of the corresponding line of @hshrink pixels in the input.
*

View File

@ -47,6 +47,8 @@
* - add a seq line cache
* 6/8/19
* - use a double sum buffer for int32 types
* 22/4/22 kleisauke
* - add @ceil option
*/
/*
@ -99,8 +101,9 @@
typedef struct _VipsShrinkv {
VipsResample parent_instance;
int vshrink;
int vshrink; /* Shrink factor */
size_t sizeof_line_buffer;
gboolean ceil; /* Round operation */
} VipsShrinkv;
@ -372,15 +375,15 @@ vips_shrinkv_build( VipsObject *object )
VIPS_DEMAND_STYLE_SMALLTILE, in, NULL ) )
return( -1 );
/* Size output. We need to always round to nearest, so round(), not
* rint().
/* Size output.
*
* Don't change xres/yres, leave that to the application layer. For
* example, vipsthumbnail knows the true shrink factor (including the
* fractional part), we just see the integer part here.
*/
t[2]->Ysize = VIPS_ROUND_UINT(
(double) resample->in->Ysize / shrink->vshrink );
t[2]->Ysize = shrink->ceil ?
VIPS_CEIL( (double) resample->in->Ysize / shrink->vshrink ) :
VIPS_ROUND_UINT( (double) resample->in->Ysize / shrink->vshrink );
if( t[2]->Ysize <= 0 ) {
vips_error( class->nickname,
"%s", _( "image has shrunk to nothing" ) );
@ -451,9 +454,16 @@ vips_shrinkv_class_init( VipsShrinkvClass *class )
G_STRUCT_OFFSET( VipsShrinkv, vshrink ),
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.
*/
VIPS_ARG_INT( class, "yshrink", 9,
VIPS_ARG_INT( class, "yshrink", 8,
_( "Yshrink" ),
_( "Vertical shrink factor" ),
VIPS_ARGUMENT_REQUIRED_INPUT | VIPS_ARGUMENT_DEPRECATED,
@ -474,6 +484,10 @@ vips_shrinkv_init( VipsShrinkv *shrink )
* @vshrink: vertical shrink
* @...: %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
* 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.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):
im = pyvips.Image.new_from_file(JPEG_FILE)
im2 = im.shrink(4, 4)