Math evaluator improvements planed for G'MIC 2.9.3

:warning: Beware, that’s a quite “technical” post about G’MIC syntax!

I’d want to describe some advances I’ve made on the math evaluator of G’MIC, for next version 2.9.3.
Tonight, I’ve basically added two new (hopefully useful) functions:

  1. expr('expression',_w,_h,_d,_s) returns a vector of size w*h*d*s whose values are computed just as if one were filling an image of size (w,h,d,s) with the specified expression (e.g. with command fill). That’s useful, if you want to initialize vectors with expressions, e.g you can write:
foo_293 : 
  eval "
    V = expr('x + y',3,3);
    print(V);
  "

in remplacement of

foo_292 : 
  eval "
    V = vector9();
    ind = 0;
    for (y = 0, y<3, ++y, 
      for (x = 0, x<3, ++x, 
        V[ind++] = x + y;
      )
    )
    print(V);
  "

This leads to

$ gmic foo_293
[gmic]-0./ Start G'MIC interpreter.
[gmic_math_parser] V = [ 0,1,2,1,2,3,2,3,4 ] (size: 9)
[gmic]-0./ End G'MIC interpreter.

So, a quite useful function to initialize of fill vectors without having to write explicit loops for doing it.

  1. get('varname',_size) returns a vector of size size whose values come from the content of the G’MIC named variable varname. If size==0 (default), then a scalar is returned.
    Example:
foo :
  var1=1976
  var2=10,20,30,40
  sp lena,512 store. var3
  eval "
    v1 = get('var1');
    print(v1);
    v2 = get('var2',4);
    print(v2);
    v3 = get('var3',512*512*3);
    print(v3);
  "

leads to:

$ ./gmic foo
[gmic]-0./ Start G'MIC interpreter.
[gmic_math_parser] v1 = 1976
[gmic_math_parser] v2 = [ 10,20,30,40 ] (size: 4)
[gmic_math_parser] v3 = [ 225,225,223,223,225,225,225,223,225,223,223,223,223,225,223,223,223,223,225,223,223,224,224,223,223,223,223,224,223,223,223,223,224,223,224,223,223,225,228,234,234,235,234,234,234,236,237,234,237,237,234,236,234,234,231,231,228,223,223,212,204,206,185,173,...,78,89,78,77,79,93,89,104,89,89,89,89,77,93,79,77,79,89,91,80,88,96,108,108,110,108,119,119,119,118,108,118,107,91,104,77,70,65,69,69,70,64,61,61,56,56,56,55,55,61,63,63,78,78,78,77,91,80,79,89,77,79,79,82 ] (size: 786432)
[gmic]-0./ End G'MIC interpreter.

As you see, even image-encoded variables (with command or function store) are managed correctly. This way, it’s now possible to pass a vector-valued variable encoding an image, into a G’MIC pipeline, modify it within the pipeline, and get it back in the math evaluator, like this:

foo :
  sp portrait1,512
  eval "
    V = crop(200,200,128,128);           # Get a 128x128 color patch
    store(V,'V',128,128,1,3);            # Store it as an image-encoded variable 'V'
    run('$V rotate. 90 b. 3 store. V');  # Modify it in a G'MIC pipeline
    W = get('V',size(V));                # Get the modified patch as a new vector 'W'
    draw(W,200,200,0,0,128,128,1,3);     # Draw 'W' back on the image.
  "

which leads to the following result:
test_portrait1

Here again, this opens new possibilities to pass data from/to a run() call.

Well that’s all for now, 11:24pm, time to go to bed :slight_smile:

2 Likes

Hmm, I think I will use that in my latest work. It is already possible to do expr with images, and access with i(#…). For some reason, I am thinking this can be used to speed up my diffusion limited aggregation filter. You’d note that I am using a do while loop to create a dynamic array from existing image, and a do while loop to move coordinates by 1, as well as deleting array based on condition. So, that is pretty slow. I hope I’m not wrong about that.

I’ve made some improvements this morning, and everything looks good so far.
I’m currently building binary packages for Windows, Ubuntu 20.04 and 19.10. It should be available in the ‘pre-release’ download page in one hour or so (check the date) : https://gmic.eu/files/prerelease/

Would this be appropriate for a moving window? That would mean using store() and run() as many times as there are pixels on their neighbourhoods. E.g. I have

ref(crop(x-R,y-R,0,c,D,D,1,1),N)

Yes, this means you can use it to apply a G’MIC pipeline to a moving window, just like this:

foo :
  sp colorful,128
  f "
    const boundary = 1;
    V = crop(x-5,y-5,0,c,11,11,1,1);
    store(V,'V',11,11);
    run('$V n. 0,255 store. V');
    V = get('V',11*11);
    V[size(V)/2];
  "

Beware, the run() function is computationally intensive (it runs a complete G’MIC interpreter), so don’t expect such loops to be efficient.

That was what I wanted to know. :+1: I guess the proper way would be to have G’MIC and math interpreter versions of everything. :stuck_out_tongue:

It always better to find a solution that doesn’t involve run() on each pixels. I do have to ask if it can be avoided at all costs?

I must ask: what is the best approach? {}, fill and eval all use the math interpreter, right? Right now, I tend to use the G’MIC intepreter (less precision but faster) and the other 3 when convenient. Is it expensive to constantly be switching among them? I take the convenient approach because I am not fluent in coding or math (i.e. I am not quick or natural at them).

I use fill if the filter utilize the traditional for x, for y, for z, i(x,y,z)=result; and if the target breaks away from traditional, then use eval. You can use fill under eval. However, if you are looping run fill per pixel, it will be very expensive. The reverse is naturally expensive.

That’s an interesting question indeed.
What I think is that when possible, it’s better to use “classical” pipelined commands when performing image processing operations, particularly if no explicit image loops are involved (and if you can use mostly native commands in your pipeline, then it’s even better). If you think about writing nested repeat...done loops for analyzing or rendering an image, then it’s probably you have to switch to the math evaluator, through a math expression to a fill or eval command.
So forget about:

foo1 : 
  1024,1024
  repeat h,y
    repeat w,x
      =. {u(30,255)},$x,$y
    done
  done 

the math expression:

foo2 : 
  1024,1024
  fill "u(30,255)"

is better.

The math evaluator has been specifically designed for this use case : Sometimes you really need to do very specific pixel-to-pixel operations, which are not easily done with existing pipelined commands. In this case, the use of fill and eval is justified. You can see them as a bit like opengl shaders for doing custom local operations. But don’t forget that a mathematical expression has first to be compiled as a sequence of “bytecodes” before being evaluated by fill or eval, and this step induces additional computation time.
So conversely, using fills or evals to do things that can be done easily by commands is not a good idea either. So, for instance, the example above is finally better with:

foo3 : 
   1024,1024 rand. 30,255

(rand is a native command in G’MIC!).

Moreover if we look at the timings of these 3 foo commands, which generate the kind of random image, we see that :

$ gmic tic foo1 toc tic foo2 toc tic foo3 toc
[gmic]-0./ Start G'MIC interpreter.
[gmic]-0./ Initialize timer.
[gmic]-1./ Elapsed time: 11.273 s.
[gmic]-1./ Initialize timer.
[gmic]-2./ Elapsed time: 0.014 s.
[gmic]-2./ Initialize timer.
[gmic]-3./ Elapsed time: 0.004 s.
[gmic]-3./ Display images [0,1,2] = '[unnamed], [unnamed], [unnamed]'.
...

and it is clear foo3 is better than foo2.

Of course, the algorithms you want to write in practice are not as simple to analyze, and sometimes you find yourself in a “gray area” where you want to use the best of both worlds (pipelined commands and math evaluator). I don’t think there are general rules for deciding what’s the best approach then. Only experience, and tictoc commands to help understanding what the best approach to use.

I tend to choose one of this two approach by defining first what I expect from my command, in terms of speed and flexibility. When you write a new command, you first constrain yourself to make it work before adding new options to make it more flexible. It is important to keep the goal in mind to be sure you won’t get stuck in the approach that won’t allow you to add the flexibility you want afterwards. Knowing exactly what you expect from your new command (and what you won’t expect) is a great helper to choose between one of the two approaches.

One more tip to add in addition to what @David_Tschumperle said: Do not be afraid to revisit your filters. You may find a faster solution. My example is fragment blur. The first version was unbearably slow. A rewrite made it over 5000 times faster.

This year has been figuring out this aspect of scripting. Indeed, there is a grey area. E.g. I discovered so many ways to do rgb2jzazbz but none of them ended up as efficient as @snibgo’s which you adopted.

Could you elaborate on your example?

Putting it to words, are you
– storing V as a vector of sets of 121 pixels;
– normalizing each set; and
– then averaging the first half of values of each set;
– where get() is like accessing the original neighbourhoods (now normalized)?

How can I use display() to show the whole image between the steps (not just individual sets of 121)?

LOL, all of my rewrites add more time because they are more complex. (And I often doubt they are better.)

I tried expr as I finally saw the use for it, I’m going to assume it haven’t arrived yet or I need to update gmic besides doing gmic up.

EDIT: Yep, had to delete the old gmic programs, and updated from gmic.eu. That fixes it.

  • V is a variable that stores a 11x11x1x1 image (i.e. an image patch).
  • In the call to run() , the patch is inserted at the end of the image list, then normalized, and stored as a variable with the same name V.
  • The call to get() allows to get back the content of the modified patch in the math parser, as a vector121.
  • Finally, the middle value of this vector is returned in the expression, so that fill set its as the new value in the image, at position (x,y,0,c).
  • For this, display(#0) can be used, but in the case of this fill you probably won’t see the image [0] updating, because the expression contains a call to crop(), which is detected by the math expression compiler, and prevent the image [0] to be modified ‘in-place’. Instead, the result of the fill is first computed as a new temporary image (that cannot be accessed inside the math expression), which replaces image [0] only when all the pixels have been processed.

I ended up using expr successfully. What I really like about it is that I don’t have to use a constant.

Chirikov-Taylor Standard Map
rep_chirikov_taylor_map:
skip ${2=5000},${3=1.52},${4=100},${5=.25}
#$1==size#
#$2==loop#
#$3==k#
#$4==lines#
$1,$1,1,1
eval ${-math_lib}"
const pts=$2;
const k=$3;
const dpi=2*pi;
const lines=$4;
fmod(a)=a-dpi*floor(a/dpi);
xi=0;
iy=expr('x/w*2*pi',lines,1);
for(n=0,n<lines,n++,
 yi=iy[n];
 for(ptn=0,ptn<pts,ptn++,
  yi=fmod(yi+k*sin(xi));
  xi=fmod(xi+yi);
  i(#-1,round(xi/dpi*w#-1),round(yi/dpi*h#-1))=n;
 );
);
"

What I don’t like is having to use 2*pi instead of the constant dpi.

image

EDIT: More beautiful image using the command.

1 Like

That’s because the math expression provided to expr() is something totally independent on the current math expression being written (in a fill or eval command). Function expr() just instantiate a new math parser.

Actually, I’m not sure I’d like to have const variables shared between the two math parsers.
I imagine something as:

fill "
  const x = 1976;
  const y = 3210;
  V = expr('x + y',3,3);
"

could lead to V being a vector9() with the same value 5186 everywhere, rather than the expected vector [ 0,1,2,1,2,3,2,3,4 ].

What if you could put constants outside of ‘something’?

That’s exactly what I discussed above. This is not desired.

@David_Tschumperle I would like to let you know that filters that utilizes expr and get will have a error on G’MIC-QT 2.9.2. Right now, PDN users will have to not touch these filters meanwhile.