GNOME Bugzilla – Bug 549999
scale: nearest neighbour, linear, cubic and lanczos have incorrect behavior in gegl svn 2581
Last modified: 2012-01-22 18:02:01 UTC
Please describe the problem: When upscaling---I have not checked downscaling---all the scaling methods available give unwanted behaviour. Specifically: a) When used with the same scales, the four methods give images with different dimensions (no two method give the same size for the same image and scaling factors). Note that I understand that this may be a good thing: For example, nearest neighbour is more compatible with the "center" image size convention, and linear is more compatible with the "corner" image size convention. However, the fact that each and every method produces its own dimensions is very disorienting. I suspect that there is something fishy in the way the output image dimensions are computed from the scales? I could not figure out where this is done in the code. b) Every method produces transparent pixels at the boundary, with the thickness of this incorrect boundary different for each method. Nearest and linear only do so at the bottom and right boundaries, cubic and lanczos at all four. This may be related to the current simple minded abyss policy, but if the policy is not changed (to smear, say), the code for the methods need to be changed, or the sampled pixels need to be set so that they are far enough away from the boundary that the abyss is not used in the computation (quick and dirty fix, which has the unfortunate side effect of cropping the image slightly for cubic and lanczos, and still requires careful handling of the bottom and right boundaries for nearest and linear). This is related to the "area-sampler" border pixel values issue. Steps to reproduce: I do not know if this bug exists in earlier versions of gegl, so you need to have a recent svn version installed (I used 2581, the current one as of today). http://web.cs.laurentian.ca/robidoux/misc/ constains a gzipped tar file called gallery.tgz Download it. In the following, "..." refers to the folder in which the gegl source is located. I assume that you have rwx permission to this folder. If no, su or sudo, and chmod if needed. Basically, you will replace the "stock" gallery folder by one which exhibits the problem (you may, of course, mv the stock gallery folder elsewhere, so you can copy it back later). cd .../gegl/docs rm -rf gallery cp where_ever_you_downloaded_it_into/gallery.tgz . tar xvzf gallery.tgz cd gallery make clean make The test images are courtesy of the excellent http://www.cambridgeincolour.com/tutorials/digital-photo-enlargement.htm Now: If you point your browser to file:///.../gegl/docs/gallery/index.html you will see the page generated by the various xml files, which differ only in the input image used (flag or cat, found in the data subfolter) and the method used to upscale (nearest, linear, cubic or lanczos). In order to easily view the enlargement's dimensions, and see the erroneous transparent borders, do this: cd .../gegl/docs/gallery gimp *.png This should make the above assertions obvious. Actual results: The most likely incorrectly method dependent distinct output image dimensions, as well as the incorrect transparent borders, are seen. Expected results: Image dimensions which are more "consistent" across methods ("center" or "corner" or both, but not the current hard to figure one). No transparent borders. Does this happen every time? Yes. Other information: If someone can point out to me where the numbers of pixels of the enlargements, as well as the coordinates (relative to the buffer's origin, which is, I understand, top/left=(0,0)) of the sampled pixels, I may be able to fix the bug. Apologies if this bug report is redundant. "Nicolas Robidoux" <nrobidoux@cs.laurentian.ca>
The bug is manifest because the different interpolation methods have different sampling footprints. The resulting image size is computed based on the size of the kernel, this is similar in behaviour to the gaussian blur which increases the size of the input image as well by smearing it out. The upper left corner of the bounding box is stored to be in negative coordinats so sampling from 0,0 would yield the upper left sample anyways. One way to work around this would be to crop the image to the original bounding box. Not completely sure if this is a bug or not, I guess it depends on what types of behavior we want to support for the edges of transformed buffers.
I noticed this also when working on the samplers and indeed it has to do with the width of the interpolation functions. When scaling (affine op) The get_required_for_output/get_bounding_box and get_invalidated_by_change are extended by the context_rect of the sampler. In the case of a real edge (not a tile edge) the pixels could depending on the edge strategy be copied/initialized or mirrored. This prevents the need of handling the edge situation in the sampler. However I think meanwhile the edge conditions are handled by using gegl_sampler_get_ptr.
IMHO, I think that boundaries should be handled inside the samplers, not with abyss policies which cannot be made to do the right thing for all samplers/operations. This is not as much of a big deal as it seems: The operation/sampler designer is the most qualified to know what to do with "missing data"/"computed values for points outside the image/region." The information which is needed is mostly passed to the gegl-sampler-***.c already: We already know that the left top pixel is at 0, 0. In order to know where the boundary is all around, all that is needed is the pixel width and pixel height of the input image. If this info was passed, I could immediately fix yafr so that it never tries to access information beyond the boundary (that is, the answer would be independent of the abyss policy), so that the pixel values near boundary "are as accurate/good looking" as possible, AND so that if the sampler is asked for pixel values at points outside of [0,width-1]x[0xheight-1] (for example, if rotation is performed), then the values which are extrapolated as opposed to interpolated are reasonable (in the case of yafr, I'd fix them so that the transition is continuous from inside to outside, and so that values are constant when one goes "straight out" of the original area, which if you think about it, is pretty much the most reasonable choice). ------- There are additional possible benefit to passing this information: If one wants to resample then crop, this could be done in one merged operation which uses all the appropriate pixel values, but does not compute pixel values which would be cropped. Actually, it would be easy to specify the exact locations of the corner pixels of the resampled area, making extend beyond the original image area even if one is upsampling or downsampling, without ill effect. This would allow easy switching between "corner" image size conventions (good for interpolatory methods, like nearest neighour, bilinear, cubic, lanczos and yafr) and "center" conventions (good for exact area methods, like box filtering, and Exact Area Natural BiQuadratic Histosplines, for which a plug-in will appear for Gimp classic soon). I feel pretty strongly about this: If this could be implemented into GEGL, some of my medical imaging friends would use it. Without tight control of which image size convention is used (they need "center"), they won't, and I'll have to produce code versions just for their purpose. The other thing I would like is the possibility to precisely control the numbers of pixels in the resampled image, as opposed to trusting the rounding behavior of the vague "scale." ----------- It is possible that I can get what I'd like by picking it out of the already existing objects, but it would be nice if I could get it in a transparent way. Nicolas Robidoux Laurentian University/Universite Laurentienne Nicolas Robidoux Laurentian University/Universite Laurentienne
I am not sure I stand by my previous solutions anymore. In GPUs, this is often handled by "clamping" coordinates during input pixel lookup. VIPS has a solution to this problem---based on somehow extending the input image---which is consistent with demand-driven and does not affect performance. In summary: I'm not sure how this should be fixed. I just think that it should be fixed. Nicolas Robidoux Universite Laurentienne
Could I get some feedback on the following? Looking at the way Adam Turcotte is trying to adaptively change the footprint of the (GSoC new) downsampling samplers, it occured to me that a hack which would most likely take care of the sampler dependent border issue would be, for example, to change gegl_sampler_cubic_init (GeglSamplerCubic *self) { GEGL_SAMPLER (self)->context_rect.x = -1; GEGL_SAMPLER (self)->context_rect.y = -1; GEGL_SAMPLER (self)->context_rect.width = 4; GEGL_SAMPLER (self)->context_rect.height = 4; to gegl_sampler_cubic_init (GeglSamplerCubic *self) { GEGL_SAMPLER (self)->context_rect.x = 0; GEGL_SAMPLER (self)->context_rect.y = 0; GEGL_SAMPLER (self)->context_rect.width = 0; /* or 1? */ GEGL_SAMPLER (self)->context_rect.height = 4; /* or 1? */ and then add the following lines GEGL_SAMPLER (self)->context_rect.x = -1; GEGL_SAMPLER (self)->context_rect.y = -1; GEGL_SAMPLER (self)->context_rect.width = 4; GEGL_SAMPLER (self)->context_rect.height = 4; to the top of gegl_sampler_cubic_get (GeglSampler *self, gdouble x, gdouble y, void *output) I have not looked at things carefully. I have the impression that the extra borders may be so that operations can safely be chained: This way, whatever previous node is feeding data to the sampler is made aware that a larger area is needed. So, I am a bit concerned that the above hackish solution may break chains of operations. A less hackish, more much more labor intensive, would consist of separating more clearly the footprint from the computation bounding box (I may have the wrong terminology here: no time to look at the source code just now). But, again, if "enlarged" bounding boxes are needed in order to move information upstream about needed pixel values, this may break things. Nicolas Robidoux Universite Laurentienne
Typo in my last post: GEGL_SAMPLER (self)->context_rect.width = 0; /* or 1? */ GEGL_SAMPLER (self)->context_rect.height = 4; /* or 1? */ should have been GEGL_SAMPLER (self)->context_rect.width = 0; /* or 1? */ GEGL_SAMPLER (self)->context_rect.height = 0; /* or 1? */ Apologies.
Created attachment 140621 [details] Hack partially removing transparent border around outputted images Applies to Linear and Cubic samplers. Initialize values to: GEGL_SAMPLER (self)->context_rect.x = 0; GEGL_SAMPLER (self)->context_rect.y = 0; GEGL_SAMPLER (self)->context_rect.width = 1; GEGL_SAMPLER (self)->context_rect.height = 1; Then set the context rect to sampler-specific values when calling "get".
Created attachment 140697 [details] Hack partially removing transparent border around outputted images Removed unnecessary assigning of context_rect values in linear sampler, also cosmetic changes fixing some text alignment.
This hacks succeeds in reducing the size of the extra (partially) transparent border when resizing and resampling, and apparently does not introduce other problems. Unfortunately, it does not remove it completely for methods with large footprint (stencil), because the transparent black abyss creeps in. IMHO, the abyss/implied image size issues will require a good amount of careful code rewrite, so this may be a good temporary measure. On the other hand, keeping the transparent border there may make it clear that something drastic needs to be done (instead of "hiding most of it away"). In other words: This is ready to commit if you are OK with this hack. Nicolas Robidoux Laurentian University
I have investigated the default abyss color problem, and it was determined to be too time consuming for GSoC and would require a large code change at various levels throughout GEGL because this would require passing parameters from the xml test file to the operation, which is different from passing parameters to the samplers. In affine.c, I found a line of code that is suspicious. I do not know exactly what it does, but it seems that it may be the line that passes information to buffer-access (where abyss stuff is done): output = g_object_new (GEGL_TYPE_BUFFER, "source", input, "shift-x", (int)-affine->matrix[0][2], "shift-y", (int)-affine->matrix[1][2], "abyss-width", -1, /* turn of abyss (relying on abyss of source) */ NULL);
(In reply to comment #10) > output = g_object_new (GEGL_TYPE_BUFFER, > "source", input, > "shift-x", (int)-affine->matrix[0][2], > "shift-y", (int)-affine->matrix[1][2], > "abyss-width", -1, /* turn of abyss > (relying on abyss > of source) */ > NULL); For the record, when using homogeneous coordinates, and when matrix[2][2]=1 (which is always the case for affine operations in gegl?), matrix[0][2] is the x-translation component of the transformation, and matrix[1][2] is the y-translation component. Nicolas Robidoux Universite Laurentienne
Does this issue still exist?
Nicolas, can you answer the question in comment #12?
All this code has changed so much that the best thing to do is to mark as RESOLVED. If I find it is still a problem when I get back into GEGL programming, I'll open a new bug report with specific references to the current code.