An expression for determining the size of a text layer

Moderators: Disciple, zlovatt

Post Reply
nab
Posts: 203
Joined: November 29th, 2005, 3:00 am
Location: Royan
Contact:

Here is a (slow) expression that determines the width and the height of a text layer using the new sampleImage(). It is assumed that the text is horizontal and entirely visible. The expression can probably be optimized (this is a job for you Dan ;) ), but here is the general idea:
  • 1. initialize the left, right, up and down variables of the bounding box we're looking for
    2. sample the pixels of the text layer
    3. if the current pixel has an alpha greater than 0, update the variables
    4. output width=right-left, height=down-up
This can be written like that (in another text layer):

Code: Select all

L = thisComp.layer("MyTextLayer"); 
w = L.width; h = L.height;
lmin = w; rmax = 0; 
umin = h; dmax = 0;

for (i = 0; i < h; i++)
{
    for (j = 0; j < w; j++)
    {
        p = L.sampleImage([j,i], [0.5,0.5]);
        if (p[3] > 0)
        {
            if (i < umin) umin = i;
            if (i > dmax) dmax = i;
            if (j < lmin) lmin = j;
            if (j > rmax) rmax = j;
        }
    }
}
textW = rmax - lmin;
textH = dmax - umin;

"Size of text:\r" + textW + "x" + textH;
nab
Posts: 203
Joined: November 29th, 2005, 3:00 am
Location: Royan
Contact:

Please guys don't let me alone in the playground :mrgreen:

Instead of sampling every pixels, we can directly sample rows or columns by changing the radius parameter in sampleImage(). The number of operations decreases from w*h to w+h which is a huge gain (considering that sampling a larger area doesn't increase that much the computation time).

So here is an optimized version of the previous expression:

Code: Select all

function ExistPixelInCol(layer, colIndex)
{    
    return layer.sampleImage([colIndex,thisComp.height/2], [.5,thisComp.height/2])[3] > 0 ? true : false;
}

function ExistPixelInRow(layer, rowIndex)
{    
    return layer.sampleImage([thisComp.width/2,rowIndex], [thisComp.width/2,.5])[3] > 0 ? true : false;
}

L = thisComp.layer("MyTextLayer");
w = L.width; h = L.height;
lmin = L.width; rmax = 0;
umin = L.height; dmax = 0;

for (i = 0; i < h; i++)
{
    b = ExistPixelInRow(L,i); 
    if (b && i < umin)
        umin = i;
    if (b && i > dmax)
        dmax = i;
}
for (j = 0; j < w; j++)
{
    b = ExistPixelInCol(L,j); 
    if (b && j < lmin)
        lmin = j;
    if (b && j > rmax)
        rmax = j;
}
textW = rmax - lmin;
textH = dmax - umin;

"Size of text:\r" + textW + "x" + textH;
If you change the Vertical or Horizontal scale in the Character panel, you'll see the expression updates in 'real time', which was definitely not the case with the previous expression.
nab
Posts: 203
Joined: November 29th, 2005, 3:00 am
Location: Royan
Contact:

This one is more restrictive as it assumes the text is left aligned and doesn't handle 'fancy text' (experts will understand what the expression considers as fancy text), but it should be a little faster.
The idea is to start the search with a bounding box of zero area from the current position of the text layer. We then expand the box to the right/top until no more pixels are detected in the column/row.

Code: Select all

function getTextPosition(layer)
{
    return layer.toWorld(layer.anchorPoint) - layer.anchorPoint;
}

function ExistPixelInCol(layer, colIndex)
{    
    return layer.sampleImage([colIndex,thisComp.height/2], [.5,thisComp.height/2])[3] > 0 ? true : false;
}

function ExistPixelInRow(layer, rowIndex)
{    
    return layer.sampleImage([thisComp.width/2,rowIndex], [thisComp.width/2,.5])[3] > 0 ? true : false;
}

L = thisComp.layer("MyText");
P = getTextPosition(L);
lmin = P[0]; rmax = lmin;
tmin = P[1]; bmax = tmin;

do { tmin--; } while (ExistPixelInRow(L,tmin));

for (j = P[0]+1; j < L.width; j++)
    if (ExistPixelInCol(L,j))
        rmax = j;

textW = rmax - lmin;
textH = bmax - tmin;

textW + "," + textH;
I don't claim these expressions are extremely useful but it's for sure an interesting training :D
User avatar
lloydalvarez
Enhancement master
Posts: 460
Joined: June 17th, 2004, 9:27 am
Location: New York City, NY
Contact:

Wow! Nicely done! This will defintiely come in handy. I just wrote a script the other day just to use sourceRectAtTime. Thanks!
Dan Ebberts
Posts: 320
Joined: June 26th, 2004, 10:01 am
Location: Folsom, CA
Contact:

Yeah, nice job. It works great for text and shape layers. For solids, you have to use the toWorld() transform to get it to work right. I tried to optimize the code a little, but it really isn't much better than what you have:

Code: Select all

L = thisComp.layer("test text");;
top = bottom = left = right = 0;

gotOne = false;
for (i = 0; i < height; i++){
  if (L.sampleImage([width/2, i], [width/2, 0.5], true)[3] > 0){
    bottom = i;
    if (! gotOne) {
      top = i;
      gotOne = true;
    }
  }
}

gotOne = false;
for (i = 0; i < width; i++){
  if (L.sampleImage([i, height/2], [0.5, height/2], true)[3] > 0){
    right = i;
    if (! gotOne) {
      left = i;
      gotOne = true;
    }
  }
}

"UL = [" + left + "," + top + "]   LR = [" + right + "," + bottom + "]";
Dan
nab
Posts: 203
Joined: November 29th, 2005, 3:00 am
Location: Royan
Contact:

Nice job too, Dan.
For the sake of writing the best code we can, we could add two variables for "width/2" and "height/2" so that the expression doesn't have to recalculate them at each iteration of the loops.
I had a new idea for this expression that I'd like to experiment soon...I don't expose it yet because I'm not sure I will succeed in writing it :)
nab
Posts: 203
Joined: November 29th, 2005, 3:00 am
Location: Royan
Contact:

Okay, I got my monster!

My starting point is the last expression posted by Dan. What is slow in this expression (and in previous expressions as well) is the fact that we sample every row and column even if this row or column is contained in a larger area that does not contain non-zero alpha pixels. In other words, say your text is written at the bottom of the comp, so that the upper half of the layer has no pixels. The expression will still sample each row of this area. This looks like too much work. So if we can determine that we won't find anything in that zone, there is no need to sample there.

This observation is the base idea of my new expression which is a sort of Divide and Conquer approach. We recursively split the layer into smaller parts and when an area is empty, we don't sample further in that area. The process is done vertically and horizontally. For instance, at the beginning, the expression determines whether there are some pixels in the upper part of the layer or not, if it finds some, then it analyzes the upper quarter, and so on until the area is around 1 pixel wide or empty.

I set up a little project to test these two techniques and the divide and conquer expression seems to outperforms the previous one in terms of computation time, while providing a bounding box of good quality. When I rendered the two comps in the project, the "new" comp was 375% faster than the "old" comp.

In both methods, I noticed that when the text is very small compared to the size of the comp, when we sample a row or a column that contains very few pixels, sampleImage() round the result to zero, so we may miss some pixel.

The expression isn't easily readable but if someone wants to improve it, here it is:

Code: Select all

function getParams(area)
{
    var pointX = (area[0][0] + area[1][0]) / 2;
    var pointY = (area[0][1] + area[1][1]) / 2;        
    var radiusX = (area[1][0] - area[0][0]) / 2;
    var radiusY = (area[1][1] - area[0][1]) / 2;
    return [ [pointX,pointY], [radiusX,radiusY] ];	
}
function isAreaEmpty(area)
{
    var params = getParams(area);        
    if (params[1][0] < 0.5 || params[1][1] < 0.5) 
        return true;
    return L.sampleImage(params[0], params[1], true)[3] > 0 ? false : true;
}
function sampleRow(area)
{
    var params = getParams(area);           
    if (L.sampleImage(params[0], params[1], true)[3] > 0)
    {
        if (params[0][1] < top)
            top = params[0][1];
        if (params[0][1] > bottom)
            bottom = params[0][1];
    }
}
function sampleCol(area)
{
    var params = getParams(area);           
    if (L.sampleImage(params[0], params[1], true)[3] > 0)
    {
        if (params[0][0] < left)
            left = params[0][0];
        if (params[0][0] > right)
            right = params[0][0];
    }
}
function sampleAreaHorizontally(area)
{    
    if (isAreaEmpty(area)) 
        return;

    if (area[1][1] - area[0][1] < 1.5)
        sampleRow(area);
        
    var UpperArea = [ area[0], [area[1][0],(area[0][1]+area[1][1])/2] ];
    var LowerArea = [ [area[0][0],(area[0][1]+area[1][1])/2], area[1] ];
    sampleAreaHorizontally(UpperArea);
    sampleAreaHorizontally(LowerArea);    
}
function sampleAreaVertically(area)
{    
    if (isAreaEmpty(area)) 
        return;

    if (area[1][0] - area[0][0] < 1.5)
        sampleCol(area);    
    
    var LeftArea  = [ area[0], [(area[0][0]+area[1][0])/2, area[1][1]] ];
    var RightArea = [ [(area[0][0]+area[1][0])/2, area[0][1]], area[1] ];
    sampleAreaVertically(LeftArea);
    sampleAreaVertically(RightArea);    
}  
var L = thisComp.layer("Text");
var top = L.height, bottom = 0, left = L.width, right = 0;
var halfW = L.width / 2;
var halfH = L.height / 2;
var area = [[0,0], [L.width,L.height]];
sampleAreaHorizontally(area);
sampleAreaVertically(area);
"" + left + "," + top + "," + right + "," + bottom;
I know this looks like overkill but we are in the "Discuss the code" section :mrgreen:
deadrocker
Posts: 1
Joined: March 14th, 2016, 9:24 pm

Wow so this is what I was not knowing about all this time. “textW = rmax - lmin;" and "textH = dmax - umin;” these are the commands that I did not use until now. No wonder I used to get idiotic results all this time. Thanks a ton for helping out.

::::::::::::::::::::::::::::::
Redrocker
sell citi thank you points
::::::::::::::::::::::::::::::
Post Reply