After an evaluation, GNOME has moved from Bugzilla to GitLab. Learn more about GitLab.
No new issues can be reported in GNOME Bugzilla anymore.
To report an issue in a GNOME project, go to GNOME GitLab.
Do not go to GNOME Gitlab for: Bluefish, Doxygen, GnuCash, GStreamer, java-gnome, LDTP, NetworkManager, Tomboy.
Bug 433436 - transformation tools cause offsets
transformation tools cause offsets
Status: RESOLVED FIXED
Product: GIMP
Classification: Other
Component: Tools
git master
Other All
: High critical
: 2.8
Assigned To: GIMP Bugs
GIMP Bugs
: 363775 636853 (view as bug list)
Depends on:
Blocks:
 
 
Reported: 2007-04-25 21:04 UTC by Sven Neumann
Modified: 2016-08-29 14:52 UTC
See Also:
GNOME target: ---
GNOME version: ---


Attachments
four , 15 deg rotations with NONE interpolation (297 bytes, image/png)
2007-04-25 22:57 UTC, gg
  Details
six , 15 deg rotations with NONE interpolation (294 bytes, image/png)
2007-04-25 23:02 UTC, gg
  Details
screenshot of previous in gimp at 800% (7.74 KB, image/png)
2007-04-25 23:08 UTC, gg
  Details
test image - 10x10 black square on a 20x20 white canvas (179 bytes, image/png)
2007-05-08 19:45 UTC, Raphaël Quinet
  Details
Quick hack (2.24 KB, patch)
2009-08-24 16:12 UTC, Massimo
rejected Details | Review
Regression test (2.40 KB, application/octet-stream)
2010-06-27 17:09 UTC, Massimo
  Details
Patch with a regression test (7.85 KB, patch)
2010-06-30 19:17 UTC, Massimo
needs-work Details | Review
More regression tests (4.83 KB, patch)
2010-10-22 15:33 UTC, Massimo
none Details | Review
Fixing samplers so that the relationship between pixel indices and physical location matches GIMP's (28.54 KB, patch)
2012-11-18 03:15 UTC, Nicolas Robidoux
committed Details | Review
proposed patch (1.67 KB, patch)
2012-11-18 17:59 UTC, Massimo
committed Details | Review
test output (39.91 KB, image/png)
2012-11-19 18:00 UTC, Massimo
  Details
transform-core.c with attempt at systematic use of absolute and index coordinate systems (39.39 KB, text/x-csrc)
2012-11-22 02:30 UTC, Nicolas Robidoux
  Details
transform-core.c with attempt at systematic use of absolute and index coordinate systems (39.39 KB, text/x-csrc)
2012-11-22 02:34 UTC, Nicolas Robidoux
  Details

Description Sven Neumann 2007-04-25 21:04:34 UTC
As explained in bug #167956, there is a (-1,-1) drift per transform measured in
destination coords.  This should be investigated and fixed.
Comment 1 Sven Neumann 2007-04-25 21:11:50 UTC
This bug report specifically deals with the transformation tools and the code in gimpdrawable-transform.c. There's another bug report (bug #433241) about scaling an image.

According to gg, the problem can be reproduced with all interpolation types. At the centre of a blank canvas, create a 10x10 "circle" - actually a stepped octogon - and rotate 6 x 15 degrees. The result is that it drops precisely one pixel.

More importantly the interpolation type None is behaving badly. It girates about some slightly off-centre point and ends up +4px in X after the 6 rotations. There is heavy and asymetrical distortion of the basic shape. Clearly any distortion should be symetrical.
Comment 2 Sven Neumann 2007-04-25 21:16:11 UTC
It would be nice to have an example image that can be used to reproduce the problem attached to this bug report.
Comment 3 gg 2007-04-25 22:57:58 UTC
Created attachment 87041 [details]
four , 15 deg rotations with NONE interpolation

ugh
Comment 4 gg 2007-04-25 23:02:59 UTC
Created attachment 87042 [details]
six , 15 deg rotations with NONE interpolation

This image started as a 10 x 10 spot centred in the canvas.

It "centre" now as far as that can still have any meaning seems to be at 15,14 ie. a drop of 4px in y.

At this point in the repetition (180 degrees) the x offset has wheeled back on itself and is currently zero. See previous image to see that it is generally non-zero.
Comment 5 gg 2007-04-25 23:08:18 UTC
Created attachment 87043 [details]
screenshot of previous in gimp at 800%
Comment 6 Sven Neumann 2007-04-26 14:40:35 UTC
Could you please attach the test image. The results can be easily reproduced, the test image is more important.
Comment 7 Sven Neumann 2007-05-08 09:57:09 UTC
See also bug #317853 about artefacts introduced when scaling a drawable. This might be a similar or even the same problem. Note that bug #317853 talks about scaling, but the code path ends up using gimp_drawable_transform_affine(), not gimp_drawable_scale().
Comment 8 Raphaël Quinet 2007-05-08 19:45:49 UTC
Created attachment 87836 [details]
test image - 10x10 black square on a 20x20 white canvas

I don't know if gg intends to attach his test image, but any image would do so here is mine.  It is a 10x10 black square in the middle of a 20x20 white canvas.  I also added very light green and red diagonals to make it obvious that the original square is well centered.

Applying several rotations (optionally holding Ctrl for multiples of 15 degrees) shows an offset in the center of rotation.
Comment 9 Sven Neumann 2007-05-09 08:21:35 UTC
*** Bug 363775 has been marked as a duplicate of this bug. ***
Comment 10 Massimo 2009-08-24 16:12:47 UTC
Created attachment 141571 [details] [review]
Quick hack

It seems that the code for affine tranformations
makes use of a coordinate system with origin at 
the top left corner of the top left pixel, whereas
the code performing pixels interpolation has the
origin at the center of that same pixel.

patch against file 'app/core/gimp-transform-region.c'
of release 2.6.7
Comment 11 Martin Nordholts 2010-06-23 04:56:20 UTC
Review of attachment 141571 [details] [review]:

We could keep doing quick hacks that seems to work, or we can built up a thorough set of regression tests so things stays fixed. Voting for the latter, setting to needs-work until the patch comes with a regression test. Thanks for the patch though, Massimo, you rock.
Comment 12 Massimo 2010-06-27 17:09:26 UTC
Created attachment 164756 [details]
Regression test

Here is my take for a regression test related to this bug.

The starting point is gimp has two ways to downscale a layer: 
gimp-layer-scale-full and gimp-drawable-transform-scale.

That is: one based on the affine transformations framework (scale tool)
and the other scaling the dimensions integer to integer
(menus "<Image>/Scale image" or "<Layer>/Scale layer").

Today the two methods produce different results even when asked 
to do the same work and with interpolation mode 'None'.

The objective is to have similar results and to choose which
one is correct.

So starting from a flat white image I scale it up of the factor I will
downscale it later. On the big image I draw the borders of the pixels
of the small version and then downscale it.

Today gimp-layer-scale-full samples from the white 
(the center of the area covered by the destination pixels) as expected,
whereas gimp-drawable-transform-scale samples from somewhere on 
the borders.

After applying the patch above, both behave similarly.



Put the attached script in its directory and launch gimp passing
on the command line

app/gimp-2.7 -b "(regression-test-000)"

Opening the undo history window you can rewind the steps executed.

<Ctrl>Q, apply the patch, make, and repeat.

HTH
Comment 13 Martin Nordholts 2010-06-29 15:09:24 UTC
Would it be possible to get this regression test in the form of a patch that will also run when doing make check?
Comment 14 Massimo 2010-06-30 19:17:27 UTC
Created attachment 164988 [details] [review]
Patch with a regression test

I added a new file test1.c and updated Makefile.am accordingly.

It is derived from test-ui.c, because it seems that to 
instantiate a GimpContext the initialization performed
in test-core.c is not enough.

Comments are appreciated.
Comment 15 Martin Nordholts 2010-09-16 18:49:18 UTC
Review of attachment 164988 [details] [review]:

A few initial comments:
* 'test1' is not a good name for a test program
* You should make the necessary changes to test-core to be able to add the test there
* You should use the helper macros to add tests
* Maybe there is no way, but isn't there a way to make a simpler test? Seems like a lot of code to test this, but maybe it's necessary
Comment 16 Massimo 2010-10-22 15:33:00 UTC
Created attachment 173016 [details] [review]
More regression tests

In the attached tar.gz there are a few new fixes with their 
regression tests added to test-core.c. 

The idea is to recycle the great amount of code among few tests 
verifying the basics of the code for affine transformations.
Comment 17 Paul Sladen 2010-12-10 00:59:57 UTC
Please dup: Bug #636853 - Gimp: rotate 90 using tool off-by-one boundary error
and ideally tweak the subject line of this bug (I didn't find it during filing the other one).

Note easier replication:

Forwarded: https://launchpad.net/bugs/687737

  1. Open an image, select a 48x48 region
  2. Use the rotate tool and input "90.0 degrees"
  3. Note that the _preview_ is correct
  4. Click [Rotate]

Now the region actually rotated is a 47x47 selection, aligned to the
bottom-right of the original region. A pair of thin 1-pixel strips are left
empty.

https://bugs.launchpad.net/ubuntu/+source/gimp/+bug/687737/+attachment/1760582/+files/gimp-rotate90-off-by-one.png
Comment 18 Michael Schumacher 2010-12-10 18:06:44 UTC
*** Bug 636853 has been marked as a duplicate of this bug. ***
Comment 19 Michael Natterer 2012-01-08 22:02:52 UTC
Massimo, what about the patches here?
Comment 20 Akhil Laddha 2012-02-22 07:14:26 UTC
Massimo, ping, will you please update about patches ?
Comment 21 Massimo 2012-02-22 18:01:33 UTC
Tested today: the bug's still there, but, to avoid introduce regressions that
could delay the release, I'm waiting after 2.8 release before pushing  
these patches.
Comment 22 Michael Natterer 2012-10-07 01:04:10 UTC
I lost track here. What about these patches? Does this also happen
with GEGL in master?
Comment 23 Massimo 2012-10-09 10:32:53 UTC
The fixes should still apply to the branch gimp-2-8, and while fixing
several minor issues, they do not touch the majors, the regression tests 
are useless (smoke in the eyes of naif contributors), and GEGL is affected
by the problem described here, comparing the output of:

<gegl>
  <gegl:crop width='20' height='20'/>
  <gegl:vector-stroke color='#2dd' width='1'
    d='M 8.5,5.5 L 11.5,5.5 L 14.5,8.5 L 14.5,11.5 L 11.5,14.5 L 8.5,14.5 L 5.5,11.5 L 5.5,8.5 Z'/>
  <gegl:color value='#d22'/>
</gegl>

with that of:

<gegl>
  <gegl:crop width='20' height='20'/>

  <gegl:rotate filter='linear' origin-x='10' origin-y='10' degrees='15'/><gegl:nop/>
  <gegl:rotate filter='linear' origin-x='10' origin-y='10' degrees='15'/><gegl:nop/>
  <gegl:rotate filter='linear' origin-x='10' origin-y='10' degrees='15'/><gegl:nop/>
  <gegl:rotate filter='linear' origin-x='10' origin-y='10' degrees='15'/><gegl:nop/>
  <gegl:rotate filter='linear' origin-x='10' origin-y='10' degrees='15'/><gegl:nop/>
  <gegl:rotate filter='linear' origin-x='10' origin-y='10' degrees='15'/>

  <gegl:crop width='20' height='20'/>
  <gegl:vector-stroke color='#2dd' width='1'
    d='M 8.5,5.5 L 11.5,5.5 L 14.5,8.5 L 14.5,11.5 L 11.5,14.5 L 8.5,14.5 L 5.5,11.5 L 5.5,8.5 Z'/>
  <gegl:color value='#d22'/>
</gegl>

it is possible to notice the one pixel drop described in comment 1

The scale tool and <Tools>/Gegl operation.../{scaleratio,scalesize} in                   
GIMP from master are probably still WIP: after halving the size of an opaque layer,
the projection is not completely cleared and remnants of the original size layer
are left on screen and anyway if you look at the first/last row/column of the
alpha channel you see that the layer is not extended/resampled symmetrically,
GEGL transforms pixel top-left corners, not pixel centers.
Comment 24 Michael Natterer 2012-10-09 13:05:31 UTC
You seem to know what to do here, can you take care of fixing 2.8 please?
Master is obviously different.

The projection artifacts are unrelated.
Comment 25 Massimo 2012-10-13 13:37:23 UTC
Comment on attachment 173016 [details] [review]
More regression tests

Pushed something similar to these patches, except for the regression tests
Comment 26 Nicolas Robidoux 2012-11-17 00:30:54 UTC
Probably stating something everybody knows, but here is, just in case. (I've written this many times and in many places.)

The way to fix these issues to make a choice of image geometry convention, an apply consistently with everything and everywhere.

The old GIMP, if I remember correctly, used the "align corners" convention. There are many ways of explaining the convention, but when resizing, what this means is that the four outer corners of the four corner pixels of the image before and after resizing are mapped to each other by the geometrical transformation. Another way to explain the convention is that an MxN pixels is understood to have an area of MxN. If it is decided that pixel locations are defined by their indices, so that the top left corner has index and location (0,0) and the bottom right one is at index (M-1,N-1), this means that the image extends from (-1/2,-1/2) to (M-1/2,N-1/2), with two additional outer corners at (-1/2,N-1/2) and (M-1/2,-1/2). (It is also possible to implement align corners so that the top left corner is at (0,0), but then pixel indices do not match their location, which IMHO is more of a hassle than having to start at (-.5,-.5) and stopping at (M-.5,N-.5).)

This convention is better when reducing the size of an image because it reduces the contribution of the abyss, and it is also better if mipmap levels are used to zoom in and zoom out, because integer box filtering implicitly uses this convention.

What happens with affine and perspective transformations (including rotations) must then be set to that when these transformations can be understood as resizes, they are in agreement the above prescriptions.

Looking quickly, massimo's patch seems to be a step in the right direction.
This being said, the only way to really do things is to make sure that every single piece follows one single convention, which I am pretty sure should be "align corners" for GIMP (and GEGL).
Comment 27 Nicolas Robidoux 2012-11-17 01:55:06 UTC
Exchange on #gimp:

<massimo> I once suggested a one-liner fix for gegl:transform op
...
<massimo> I was told they had to ask nicolas, if you want to test it it is just a matter of add
<massimo> gegl_matrix3_originate (&inverse, -.5, -.5); 
<massimo> at line 688 after gegl_matrix3_invert
<massimo> in file operations/transform/transform-core.c

Now, I have not had time to look into how this links to the GIMP GUIs and other operations, but having looked at the code for gegl_matrix_originate this exactly sets things up so that the align corners convention with pixel locations at their indices holds.

The only thing that may be better is actually applying this to the original matrix (not the inverse) and letting things be propagated this way. The "magick" of using matrices to implement affine transforms together with the facts that (-.5,-.5) is the top left corner in both the input and output images allows that. But to make sure this does not break anything I'd have to follow the toolchain all the way back to the GUI and I don't have time for that now.

You also may have to change the GUI a bit so that it explains what's happening.

But in any case, the Grand and Highly Soapboxed Image Geometry Wizard and Poobah blesses this one line change.
Comment 28 Massimo 2012-11-17 09:24:50 UTC
If we do agree that the convention adopted by 'linear', 'cubic' and 
'lohalo' gegl samplers is valid (and the one to follow). 
The next step is to agree that the 'nearest' gegl-sampler is not 
following it:

when sampling at x = j + .75, and y = i (i and j integers)

with 'linear' should result

in a quarter of pixel[i][j] and 3 quarters of pixel[i][j+1]

whereas at x = j + .25, and y = i should result

in 3 quarters of pixel[i][j] and a quarter of pixel[i][j+1]


with 'nearest' at x = j + .75, and y = i (i and j integers)

it should be pixel[i][j+1]

whereas at x = j + .25, and y = i

it should be pixel[i][j]

that is (pseudocode)

1)   i = lrint (y); and j = lrint (x); 
  or i = floor (y + 0.5); and j = floor (x + 0.5);

2) *sampler->context_rect = { 0, 0, 2, 2 };



http://git.gnome.org/browse/gegl/tree/gegl/buffer/gegl-sampler-nearest.c#n72
Comment 29 Michael Natterer 2012-11-17 09:48:20 UTC
The assumption that GIMP makes globally is that pixels are small rectangles,
so the top-left pixel would cover the area from (0, 0) to (1, 1),
and the bottom-right pixels covers the area from
(width-1, height-1) to (width, height).

GIMP passes the coordinates to GEGL like that.

(writing this is probably redundant, I just wanted to be sure).
Comment 30 Nicolas Robidoux 2012-11-17 13:14:56 UTC
Michael: I wrote the samplers I wrote based on the assumption that pixels are small squares of unit area, just like GIMP assumes. (Actually, this is not quite the issue: the issue is how geometrical transformations are adjusted to account for implicit image geometry.)

However, my understanding was that the GEGL convention was that the location of a pixel's center matches its C-style index. So, the center of the top left pixel is located at (0,0), so that the top left corner of the top left pixel is located at (-.5,-.5).

In the GIMP convention as you state it (and it matches my experience in terms of what I see in menus), the top left corner of the top left pixel is located at (0,0), so that the center of the top left pixel is located at (+.5,+.5). In other words, pixels are not centered at their index within the GIMP convention.

Flaming wars have been fought over which one's better.

The only difference between the two conventions is a shift. No more an no less. It's not that GIMP uses the "align corner pixels' corner" (image resizing) convention and GEGL uses the "align corner pixels' centers" (image resizing) convention. They use the same convention. It's just that the correspondence between indexes and position is different.

Now, this mismatch is not the end of the world, provided developers are aware of this difference: As long as GEGL is internally consistent, things will work as desired, because although I forgot to mention it earlier, when properly set up, the goal truly is to put the centers of the images in correct correspondence, and this just happens to be achieved, in the case of affine transformations, by Massimo's one line fix.

But of course GEGL could be rewritten to match the GIMP convention. So, what do you guys want: GEGL rewritten so that it uses the same convention w.r.t. pixel location and index as GIMP, or GEGL slightly fixed so it's consistent w.r.t. its own convention (position of pixel center = index)?

Now: It is possible that I misunderstood things when I started programming things into GEGL (and guiding my students doing the same), and that actually GEGL follows the GIMP convention everywhere EXCEPT in Lohalo and Linear, which I based on the existing Linear and Cubic. This is simple to fix, and I certainly can do that quickly.
Comment 31 Nicolas Robidoux 2012-11-17 13:17:37 UTC
... EXCEPT in Lohalo and Linear and Cubic ...
Comment 32 Nicolas Robidoux 2012-11-17 13:30:17 UTC
Massimo:

(In reply to comment #28)
> 1)   i = lrint (y); and j = lrint (x); 
>   or i = floor (y + 0.5); and j = floor (x + 0.5);
> 
> 2) *sampler->context_rect = { 0, 0, 2, 2 };

Indeed, nearest does not follow the same convention as lohalo, linear and cubic. The correct indices are indeed obtained by rounding, not by flooring.

If I wanted the rounding to be cheap, I'd use FAST_PSEUDO_FLOOR(x+0.5) instead of the above lrint and floor based calls. It was faster last I checked, and I assume it's still the case. (If not please let me know.) Exactly how midpoint values are rounded is not that important: There are pros and cons to pretty much every choice.

However, there is no need enlarge the number of pixels within the context rect to 2x2 unless there is something broken somewhere: Nearest neighbour definitely only needs one input pixel value per output pixel value.
Comment 33 Nicolas Robidoux 2012-11-17 14:03:13 UTC
Massimo:

It sounds like the program is actually to have every sampler follow the same convention as nearest. So, the nearest bug was a feature. And the other samplers' features were bugs.
Comment 34 Massimo 2012-11-17 16:19:00 UTC
I think that now all the work is done in gegl:transform,
GIMP creates a graph and then passes around GeglBuffers.

Both conventions are equally good (it is a matter of moving 
the second + (-.5, -.5) implied in gegl_matrix3_originate from 
the caller to the callee or v.v.).

I started assuming that I did not want to touch 'lohalo', and so
I assumed it was better to adjust 'nearest' to the others. The point
is that the meaning of context_rect and the coordinate system origin 
is not consistent for all samplers and it is therefore impossible to 
correctly fix 'get_bounding_box', 'get_required_for_output' and
'invalidated_by_change' gegl:transform members. 

It is important to know which convention is right, because the work
is split in many places and they must agree.
Comment 35 Nicolas Robidoux 2012-11-18 03:15:36 UTC
Created attachment 229271 [details] [review]
Fixing samplers so that the relationship between pixel indices and physical location matches GIMP's
Comment 36 Nicolas Robidoux 2012-11-18 03:17:04 UTC
RE: Fixing samplers so that the relationship between pixel indices and physical location matches GIMP's (28.54 KB, patch)

I have set things up in every single sampler (nearest, linear, cubic, lohalo) so that they all use the "pixel index is physical location of its top left corner" convention used, TTBOMK, by GIMP. Although there are still abyss and probably transformations issues, I am passing the torch to Massimo on those.
I moved FAST_PSEUDO_FLOOR to gegl-buffer-private.h so that it not be copied all over the place.
I did a partial clean up of the cubic sampler without changing it's behavior. I don't foresee doing a full clean up any time soon.
Cosmetic stuff: I also cleaned up the formatting and edited comments.
-----
Massimo: Note that the nearest and lohalo samplers anchor the context_rect at the input pixel closest to the sampling point, while the linear and cubic samplers anchor the context_rect at the nearest input pixel TO THE TOP LEFT. This does not mean that the samplers use different conventions. Just that their computation are normalized using context_rects that are set up with different relationships to the sampling points.
Trust that the context_rects I put there are correct. Feel free to contact me by email if things are not clear. (Use nicolas.robidoux@gmail.com)
It is not a coincidence that nearest and lohalo have widths and heights are are odd (1 and 5) and linear and cubic have widths and heights that are even: Because of this, the choices of anchor point stated above are expedient.
Comment 37 Massimo 2012-11-18 17:59:46 UTC
Created attachment 229296 [details] [review]
proposed patch

Following GIMP convention, the fix necessary to transform pixel
centres and not corners is in the attached patch.

Re: context_rect.
All samplers have to agree on where it is anchored, because
it is also used in gegl:transform in a generic way, that is
without knowing which sampler belongs to which group. The
alternative is to enlarge always the required source for
a given output rectangle.

Example: the alpha channel produced by the following gegl
composition. To compare with that obtained after setting
linear sampler context_rect to  { -1, -1, 3, 3 }

<gegl>
  <gegl:crop x='-512' y='-512' width='1024' height='1024'/>

  <gegl:add value='0.5' />
  <gegl:subtract>
    <gegl:checkerboard x='128' y='128' />
  </gegl:subtract>

  <gegl:scale filter='linear' x='2' y='-2'/>
  <gegl:nop/>
  <gegl:scale filter='linear' x='-2' y='2'/>
  <gegl:crop x='-260' y='-260' width='520' height='520'/>
  <gegl:checkerboard x='32' y='32'/>
</gegl>
Comment 38 Nicolas Robidoux 2012-11-18 18:09:01 UTC
Massimo:

If it is necessary to use { -1, -1, 3, 3 } for bilinear something is broken somehwere.

My guess would be that the "rectangle of needed pixels" is not aligned correctly or fattened enough. There is absolutely no need to use a 3x3 data square to compute bilinear.

It's going to take me a while sort through what you added.
Comment 39 Massimo 2012-11-18 18:55:48 UTC
(In reply to comment #38)
> Massimo:
> 
> If it is necessary to use { -1, -1, 3, 3 } for bilinear something is broken
> somehwere.
> 

It is simply the number of pixels whose tee-pee is non zero over
the range [j,j+1) and it is 3, when tee-pees are centred on the 
half integer and it is two when tee when tee-pees are centred
on the integers.
Comment 40 Nicolas Robidoux 2012-11-18 20:06:48 UTC
Massimo:

I do not understand your comment:

The 1D bilinear teepee is nonzero for at most two input pixels, never more (and sometimes less: only one right at an input pixel).

The 2D bilinear "teepee" is nonzero for at most four input pixels, never more (and sometimes less).

The only somewhat reasonable reason I can think of why using { -1, -1, 3, 3 } may give better results for bilinear assuming there is no bug or poor implementation would be that this ensures that the added abyss is almost symmetrical left to right and top to bottom. But this is a dirty-hacky way of making this happen, assuming it is even desirable.

If you'd rather not discuss this in public, my email is easy to find.

This looks like a miscommunication to me, because your comments RE: context_rect choices make little sense, and it's clear you know what you're doing.

However, an implementation of transform that requires { -1, -1, 3, 3 } for bilinear is something that, IMHO, should be written differently.
Comment 41 Nicolas Robidoux 2012-11-18 20:56:16 UTC
Review of attachment 229271 [details] [review]:

I redid the patch and pushed to master.
Comment 42 Nicolas Robidoux 2012-11-18 22:14:53 UTC
Review of attachment 229296 [details] [review]:

TTBOMK, this patch is correct.
I imagine that roi's are rectangles of pixel indices. The inverse transform has to use the right locations, however, and converting an index of a location is done by adding .5.
Thumbs up.
Comment 43 Nicolas Robidoux 2012-11-18 22:17:15 UTC
Review of attachment 229296 [details] [review]:

If Massimo's patch breaks something, it's something that needs fixing.
Comment 44 Nicolas Robidoux 2012-11-19 00:21:17 UTC
Review of attachment 141571 [details] [review]:

Obsolete.
Comment 45 Massimo 2012-11-19 12:16:59 UTC
Ok, now in masters gegl:transform transforms pixel centres
and samplers 'get' member functions work consistently.

(In reply to comment #40)
> Massimo:
> 
> I do not understand your comment:
> 
> The 1D bilinear teepee is nonzero for at most two input pixels, never more (and
> sometimes less: only one right at an input pixel).
> 

When you start from a function sampled on the integers and a half
   F[k] == f (k +.5)    it is:

double Teepee (double x, int k)
{
  return MAX (0., 1. - fabs (k + 0.5 - x));
}

and the piecewise linear reconstructed function is:
  
f(x) = sum/k  F[k] * Teepee (x, k)
  
  
in [j, j+1)

Teepee (j + 0.25, j-1) = MAX (0.0, 1 - |j - 1 + 0.5 - (j + .25)|) = 0.25
Teepee (j + 0.50, j  ) = MAX (0.0, 1 - |j     + 0.5 - (j + 0.5)|) = 1.0
Teepee (j + 0.75, j+1) = MAX (0.0, 1 - |j + 1 + 0.5 - (j + .75)|) = 0.25

whereas Teepee (x, j +/- n) == 0    for x in [j, j+1] and n > 1

so if I'm asked how many samples I need in order to resample somewhere
in [j, j+1), I answer at least 3: those indexed by j-1, j, j+1.


This is what 'get_required_for_output' is using 'context_rect' for, and
yes it is possible to always fatten there the required rectangle, or to
add there the knowledge of which anchoring convention each sampler is
using, but I think it is easier if each sampler rounds its context to
the nearest (integer sized and positioned) bounding rectangle, following
the same convention used in 'get'.
 
> This looks like a miscommunication to me, because your comments RE:
> context_rect choices make little sense, and it's clear you know what you're
> doing.
> 


Anyway, it is what the program outputs that should make sense.

<gegl> 
  <gegl:crop x='-512' y='-512' width='1024' height='1024'/>
  <gegl:scale filter='linear' x='-4' y='-4'/> 
  <gegl:crop x='-260' y='-260' width='520' height='520'/>
  <gegl:color value='#777' />
</gegl>
Comment 46 Nicolas Robidoux 2012-11-19 13:09:06 UTC
Massimo:
Could you please read http://en.wikipedia.org/wiki/Bilinear_interpolation and let me know if I need to explain more?

If this helps: The (cardinal) basis function of bilinear interpolation, namely the "Mexican hat function", has a support which is an OPEN interval of length 2. You cannot fit three integers in an open interval of length 2. The interval is open because the support is the set of points where the basis function is not zero. We do not need point values for locations with vanishing weight.

-----

The results do not necessarily speak for themselves. What may be going on (and what I think is going on) is that the information contained in context_rect is not propagated back through the system quite right.

Seeing that apparently there are edge issues, I did think of fixing things with the hack which consists of enlarging all the context rects by 1 all around (most likely, we can't fix things only at the right and bottom, B/C sooner or later transformations that "flip things" probably will break things at the other ends).
So, I'd rather fix the interaction between context_rects and buffers than artificially enlarge them to get a margin of safety.

(From memory,) John Cupitt of VIPS internally uses seven different coordinate systems to handle the geometry involved in making sure that tiles have just enough data (and a bit, to account for round off) to cover the need of an inverse transformed rectangle. It's really easy to lose half a pixel along the way. And one needs to account for the fact that round off error may "suddenly" push things just a nudge over where they would be safe in exact arithmetic.

Again: I may be wrong. I've not looked at things from beginning to end and top to bottom. But this is my hunch.
Comment 47 Nicolas Robidoux 2012-11-19 13:16:33 UTC
Some parts of GEGL used the "pixel index is location of its top left corner" and other parts used the "pixel index is location of its center". My guess is that this is still the case in parts I have not touched yet. To me, this is a much more likely explanation for the boundary artifacts.
In addition, I am pretty sure (still double checking: depends on what this is used for) that the bounding box computation in transform-core drops a pixel when max_x happens to land right at an integer.

-----

So yes: Adding extra pixels to context_rect is a quick hack that will paper over problems. I'd rather do things "right".
Comment 48 Nicolas Robidoux 2012-11-19 13:28:28 UTC
Massimo:

Now I understand what the misunderstanding is, and it is possible that I programmed things too fast. I'm going to double check ASAP.

Yes, if you put anchor points at the center of the pixels, to reconstruct the bilinear interpolant within the (1D) pixel you need three input pixels. You only use two at a time (the leftmost two on the left half of the pixel, and the rightmost two on the right half---the pixel center is in both groups).
However, the idea is to choose the "patch" over which the sampling point is deemed to belong to and which defines the context_rect so as to minimize the width of the context_rect (because the width of the context_rect impacts the number of requested pixels, and less requested pixels implies more tile reuse).

With (bi)linear, if you center the patch at a pixel corner instead of at a pixel center, you only need two values, not three.
Same with (bi)cubic: You then need four instead of five.
So, linear and cubic use patches centered at pixel corners, and nearest and lohalo use patches centered at pixel centers. And, as mentioned earlier, the context_rect with even widths are centered at pixel corners, and the context_rect with odd widths are centered at pixel centers.

Now, I'm going to make sure the code reflects my words.
Comment 49 Nicolas Robidoux 2012-11-19 13:44:52 UTC
The current code reflects my words.
Comment 50 Nicolas Robidoux 2012-11-19 16:05:47 UTC
Massimo:

If what I've written is not clear, please look at your own

Teepee (j + 0.25, j-1) = MAX (0.0, 1 - |j - 1 + 0.5 - (j + .25)|) = 0.25
Teepee (j + 0.50, j  ) = MAX (0.0, 1 - |j     + 0.5 - (j + 0.5)|) = 1.0
Teepee (j + 0.75, j+1) = MAX (0.0, 1 - |j + 1 + 0.5 - (j + .75)|) = 0.25

The fact that you are using MAX (0.0, ... says that you are sending weights to zero. Why not simply exclude the corresponding pixel values?
Comment 51 Massimo 2012-11-19 18:00:20 UTC
Created attachment 229390 [details]
test output

I don't know if we are seeing the same artifacts when 
rendering the composition above, so I'm attaching the
output that I obtain. I save that xml in a file
named test-scale.xml and run

gegl test-scale.xml -o tmp.png

The discontinuities in the alpha channel that I see are
simply caused by the fact that gegl:transform 
get_required_for_output is using context_rect to extend
the rectangle requested. Obviously since upsizing 4x we are
going to put 2 destination pixels at the left of the leftmost
source pixel centre and 2 pixels to the right of the rightmost
source pixel, we need to extend in both direction of the same
amount.

The only misunderstanding is that not existing a specification
of what context_rect is, we infer its meaning by its usage,
and I considered its usage in 'get_required_for_output', you probably
its usage between the sampler base class and each derived class.

I'm not denying that with bilinear you always use at most 2x2 pixels,
I'm only saying that its usage in 'get_required_for_ouput'
is incompatible with the tight and anchored definition { 0, 0, 2, 2 }.
Comment 52 Nicolas Robidoux 2012-11-21 22:10:38 UTC
massimo:
I ran your test and indeed there is something really fishy going on.
:(
Comment 53 Nicolas Robidoux 2012-11-22 01:07:28 UTC
I'm pretty sure I tracked the problem. Now to fix it cleanly.
Comment 54 Nicolas Robidoux 2012-11-22 02:27:42 UTC
massimo's bug is fixed by the following hack:

  output->x = (gint) floor ((double) min_x) - (gint) 1;
  output->y = (gint) floor ((double) min_y) - (gint) 1;
  output->width  = (gint) ceil ((double) max_x) - output->x + (gint) 2;
  output->height = (gint) ceil ((double) max_y) - output->y + (gint) 2;

in gegl_transform_bounding_box.

I do not know if this hack breaks something else.

I have also tried to systematize the use of the convention that says
that integer ranges are indices and floating point ranges are absolute
positions, which are shifted by 0.5 w.r.t. indices. The result of this
attempt is found in the attached transform-core.c.

However, I have the impression that whatever "cuts the image to size"
needs to be fixed, and I don't know where this is. Also, this may
break the interaction with GIMP.

I also suspect that round off error is not explicitly taken into
account in the system that manages buffers. Just remember that pixel
indices that fix when computed one way may stick out when computed
another way. This is a well known issue with floating point
computations of this type. For this reason, 1 extra pixel all around
HAS to be added on purpose to the source buffers. I just don't know
where this should be done (maybe it was done already).

Finally, a comment RE: context_rect.width and height:

If the width is, say, 3 pixels, making sure that there is enough
pixels in an elarged region adds 2 pixels to the width, not three,
because one of the 3 has to already belong to the region. Again, this
may have been taken care of earlier, but maybe not.

I am not pushing this yet because the first fix is a hack, and the
second may be the right thing to do, but does not fit, for some
reason, with what's there.

I'm seeing more strange stuff, which I'll discuss later.
Comment 55 Nicolas Robidoux 2012-11-22 02:30:47 UTC
Created attachment 229612 [details]
transform-core.c with attempt at systematic use of absolute and index coordinate systems
Comment 56 Nicolas Robidoux 2012-11-22 02:34:00 UTC
Created attachment 229613 [details]
transform-core.c with attempt at systematic use of absolute and index coordinate systems
Comment 57 Nicolas Robidoux 2012-11-22 03:53:05 UTC
I am pretty sure that what massimo's test shows is that the toolchain does not account for variations by 1 that arise from round off error going different ways 

This is weakly confirmed by tweaking things: The results are way too sensitive to changes in individual steps' values that are of the size of typical round off error. I'm sorry I can't be much more precise: this is a hard thing to pin down (obviously).

I can fix "massimo's bug example" a number of non-contrived ways, but then other results don't fall in line. For example, there are breaks of symmetry with respect to x and y.

I unfortunately do not grasp the entire toolchain from start to finish. (And need to move on.)
Comment 58 Nicolas Robidoux 2012-11-22 06:01:29 UTC
The minimal change that will fix "massimo's bug example" and (apparently) not break resizing in GIMP is the following:

Replace lines 377-380 in transform-core.c (the last four lines of gegl_transform_bounding_box) by

  output->x = (gint) floor ((double) min_x) - (gint) 1;
  output->y = (gint) floor ((double) min_y) - (gint) 1;
  output->width  = (gint) ceil ((double) max_x) - output->x + (gint) 1;
  output->height = (gint) ceil ((double) max_y) - output->y + (gint) 1;

I would push this change if it was not for the fact that it changes the results of the gegl composition tests in ways that suggest that the results not be as intended. In particular, the size of the result of transform.xml is not 400x400 anymore, it is 401x401 if the above change is done.

Since I can't really tell, I'll just leave this as my best suggestion so far. And won't push unless told to do so anyway.

-----

I see signs that resize and affine transformations in GEGL are not robust w.r.t. round off. I also would be quite surprised if the change to the GIMP's coordinate system does not throw a wrench into non-affine transformations, because the mapping is not translation invariant with, for example, perspective transformations, which means that you can't fix a bounding box computed with a shifted set of points by translating the result.
Comment 59 Nicolas Robidoux 2012-11-22 14:37:07 UTC
I believe that GIMP is better off with the above "minimal change".
For this reason, I am planning to push it sometime soon, unless told not to.
As mentioned, this "minimal change" leads to somewhat undesirable results in the GEGL composition regression tests. However, I'd change the references to these "undesirable results" in the hope that this spurs changes for the better.
At least at the surface, adding one extra pixel of marging of safety at the top and left, which is what the "minimal change" does, should not have some of the side effects I see.
I would guess that the code that "cuts" the result should be changed. But I don't know where this code is.
Comment 60 Massimo 2012-11-22 17:09:39 UTC
I think that to properly implement 'get_required_for_output'
a fixed 'context_rect' is not sufficient. I mean, for instance,
implementing a correct lanczos3 sampler, a fixed context_rect
with width 6 is correct only upscaling, when downscaling
it requires the source pixels in the area covered by 6x6 
destination pixels, that is, when downscaling 10:1 you'll need
~60x60 pixels surrounding the projected destination pixel center.

I mean interpolators like 'linear' and 'cubic', are not suitable to
downsample, unless you low pass the source in advance, and so it is 
not correct to design gegl-sampler's interface around these.

'lohalo', for example, has a context_rect with width set to 27, but 
probably when upsampling each width greater than 5 does not change 
anything. and 27 is only a reasonable upper bound.


IMO this means that each sampler should implement a member function
that answers the question: what is the source rectangle required
to sample at 'x', 'y', with a given 'inverse_jacobian'?

For now, either leave the situation as is, or the "minimal change"
is equally fine
Comment 61 Nicolas Robidoux 2012-11-22 18:19:46 UTC
(In reply to comment #60)
> I think that to properly implement 'get_required_for_output'
> a fixed 'context_rect' is not sufficient. I mean, for instance,
> implementing a correct lanczos3 sampler, a fixed context_rect
> with width 6 is correct only upscaling, when downscaling
> it requires the source pixels in the area covered by 6x6 
> destination pixels, that is, when downscaling 10:1 you'll need
> ~60x60 pixels surrounding the projected destination pixel center.

Oyvind (I believe) decided that context_rects would be constant, and I can imagine the programming nightmare + speed hit of not making them so. (Although I think that maybe VIPS has machinery that would accomodate that.)

The way to access large swatches of data is to use mipmaps.

> I mean interpolators like 'linear' and 'cubic', are not suitable to
> downsample, unless you low pass the source in advance, and so it is 
> not correct to design gegl-sampler's interface around these.

I am planning to remedy this fairly soon.

> 'lohalo', for example, has a context_rect with width set to 27, but 
> probably when upsampling each width greater than 5 does not change 
> anything. and 27 is only a reasonable upper bound.

As you may know already, it's more complicated than that: There are two different context_rects, one for each of the two mipmap levels that are used.
The context_rect width for the level 1 mipmap level is currently set to 47, which means that it will downsample reasonably well down to about a ratio of 1/100, at which point it will start resembling box filtering by a ratio of about 50 followed by nearest neighbour decimation.

On my further down TO DO list there is adding more mipmap levels to improve large ratio downsampling and speed up not so large ratio.

But yes: When upsampling, having a width of 27 for Lohalo, which only needs 5, is a waste, and it comes with a performance hit. This particular compromise comes from wanting small ratio downsampling to be fairly accurate. (I may have overdone it.)

> IMO this means that each sampler should implement a member function
> that answers the question: what is the source rectangle required
> to sample at 'x', 'y', with a given 'inverse_jacobian'?

In theory, this is the way to go. Not so sure in practice.
I think we need to fix the "fixed context_rect" system before thinking about that. I think this much simpler set up still needs quite a bit of love.
Comment 62 Nicolas Robidoux 2012-11-22 18:24:43 UTC
The reason why non-constant context_rects are a pain is that if you are doing non-affine transformations, they vary on a point by point basis, and the added accounting needed to keep track of this stuff will not be worth the gain.
I agree with Oyvind that grabbing more data through mipmaps when needed is a much more reasonable approach.
I kind of wish that I could access level 0 through more than one context_rect (one for upsampling and one for downsampling, say), but I again understand that this is adding complications with, most likely, little benefit.
Comment 63 Nicolas Robidoux 2012-11-22 18:28:37 UTC
correction: ... box filtering by a ratio of about 100 ...
Comment 64 Nicolas Robidoux 2012-11-22 19:36:47 UTC
massimo:
If you happen to know what would make it better (without using non-fixed context_rect), could you please state it or provided patches or push commits?
Comment 65 Nicolas Robidoux 2013-02-09 18:52:25 UTC
I fixed the offset problem in late November and/or December of 2012, when I brought GEGL's image geometry convention in line with "classic GIMP"'s.
Comment 66 gg 2013-02-10 08:10:42 UTC
Excellent work Nicolas, thanks for the effort. This kind of small low level bug looks negligible but can cause notable deterioration after multiple operations that are part of lengthy and complex image processing.