Crop multiple images to any (quad) shape and place them side by side filling the canvas (comic strip)

Hi all, I have been struggling with this for days and I can’t find an answer anywhere. Perhaps I am overlooking something obvious or perhaps its just not possible.

I am trying to write a code that takes images from my data set and puts them onto an ‘a4’ structuring randomly them like a comic or collage without any overlap. Now getting the full size images to do this was pretty easy, but this leaves a lot of white space between the images/on the side. So I used a the get() function to load in a image, get a piece of it (cropping the image) and drawing that so it fits better. But the next step is where I got stuck, I want more flexibility in the layout.

The first thing I thought, why not get() a different shape, a circle or a quad. This doens’t seem possible.
Then I thought I could draw negative shapes and put the image behind it, like a shape cut out of paper. This how ever poses the problem that when I put images next to each other they overlap ‘behind the paper’ hiding part of the first image.
Then I looked at masks, but that had the same problem as the previous one. It even had a bigger problem, masks dont seem to work if the image doesn’t have the same size as the entire canvas. So that doesn’t really work with multiple images or with images that don’t have dimensions of an a4
The last option I looked at was to use texture(). This seemed perfect, I could just draw a quad to begin with and put an image on there as a texture. At first I thought it worked great, but the texture function warps the texture and doesn’t crop it.

If any one has any ideas on how to tackle this (even if the aswer is don’t use processing use …) I would love to know. I just came back to processing because I have used it a lot in the past. Here is a simple layout that could be generated.

2 Likes

In my opinion a Desktop Publishing Program such as Indesign or canva would be more suitable to do this.

In processing, I would work with transparency.

When you store Comic_layout as a png (or use PGraphics, you might have the actual “windows” (comic panes) transparent and the lines in between (the frames) opaque white.

Similarly when you have the images of the actual comic as png you can cut of a triangle in the corner (for you 2nd row for example) and make the triangle transparent.

I haven’t achieved this at all.


//

int[] listImagesPerRow = {
  2,
  3,
  2,
  2
};

PVector[] c1= {
  new PVector(20, 174),
  new PVector(209, 174),
  new PVector(129, 313),
  new PVector(20, 313)
};

PVector[] c2= {
  new PVector(228, 174),
  new PVector(373, 174),
  new PVector(292, 313),
  new PVector(148, 313)
};

ArrayList<PImage> images = new ArrayList<PImage>();

PImage pageLayout ;

// --------------------------------------------------------------------------------------------

void setup() {
  size(512, 640);

  String[] lines;

  File file = new File(dataPath(""));
  if (file.isDirectory()) {
    lines = file.list();
  } else {
    lines = null;
  }

  if (lines == null) {
    println("NO");
    exit();
    return;
  }

  //  printArray(lines);

  for (String line : lines) {
    if (line.equals("Comic_layout.png"))
      continue; // skip
    PImage img = loadImage(line);
    img.resize(int(width/2.0)-20, height/listImagesPerRow.length);
    images.add(img);
  }//for

  pageLayout = loadImage ("Comic_layout.png");
  println(pageLayout.width);
  println(pageLayout.height);
  //
  println ("end of setup()");
}

void draw() {
  // noLoop();
  // background(0);

  int k=0;
  float x=10, y=10;
  for (int y22 : listImagesPerRow ) {

    x=10;
    for (int x1 = 0; x1 < y22; x1++ ) {

      if (y22 == 3)
        images.get(k).resize(int(width/3.0)-20, height/listImagesPerRow.length);

      image ( images.get(k), x, y  );

      k++;
      if (y22 == 2)
        x+=width/2.0;
      else if (y22 == 3)
        x+=width/3.0;
    }
    //
    y+=height/listImagesPerRow.length + 10;
  }

  // -----------------------------------------------------------------------
  // image ( pageLayout, 0, 0);
  showList(c1);
  showList(c2);

  fill(0);
  text(mouseX+" "+mouseY, 12, 12);
}//func

void mousePressed() {
  println(mouseX+","+mouseY);
}

void showList (PVector [] list ) {
  noFill();
  stroke(0);
  beginShape();
  for (PVector pv : list)
    vertex(pv.x, pv.y);
  endShape(CLOSE);
}
// --------------------------------------------------------------------------------------------
//

3 Likes

Normaly I would also just do this by hand in Indesign, but I am trying to write a code that can loop though an big data set and export them as comic pages with a randomly generated layout so every page is different. My first thought was also to make an opague white frame first and put the image behind it, this way the images dont have to be stretched. But the problem comes when you try to put a second image next to it and it overlaps, so some how I need to cut out a triangle (or other shape) from an image. The get() solves this for squares, but I would love to have more flexibility in shapes. Perhaps this is just not possible in Processing. Here is an image for illustration:

Did you look at mask()?

1 Like

Hi @Twooze,

Welcome to the forum! :wink:

This is an interesting problem, I tried to solve it in my free time so thanks for that!

As pointed by @Chrisir, you can achieve that by using the mask() function. It uses grayscale data to mask parts of an image: dark for deleting parts and white for keeping them.

Let’s suppose you have an image and you want to crop it with a quad that has a shear angle of 20 degrees:

You can compute the coordinates of the quad to draw and display that on an empty image in white (to mask the rest):

PImage maskImageWithQuad(PImage img, float shearAngle) {
  // Use the same dimensions for mask()
  PGraphics quadMask = createGraphics(img.width, img.height); 
  
  // Compute the distance for the coordinates
  float shearDist = img.height * tan(radians(shearAngle));
  
  // Draw the quad on an off-screen canvas
  quadMask.beginDraw();
  quadMask.fill(255);
  quadMask.quad(0, 0, shearDist, img.height, img.width, img.height, img.width - shearDist, 0);
  quadMask.endDraw();
  
  // Make a copy of the image and apply the mask
  PImage maskedImage = img.copy();
  maskedImage.mask(quadMask);
  
  return maskedImage;
}

And use it like this:

PImage img = loadImage(...);
PImage masked = maskImageWithQuad(img, 20); // in degrees

You need to handle the edge cases where the first and last image are not masked with the same quad (the x coordinates of some points are not offset).

Now this is nice but you also need to randomly create the layout with rows of same height but different number of images / orientation. For each row, choose a random orientation (left, vertical or right) and a random number of images to display.

I won’t give the full code now but here are the different collage I am getting (using images from Lorem Picsum):

Tell me if you have more questions!

4 Likes

extremely nice.

remarks for the OP

I think one thing is whether the initial image is landscape or portrait. You can determine it with checking if(img.width>img.height) I guess.

So maybe you want 2 layouts for a page, one with only landscape, the other with one or two portrait images possible.

Also, when you use resize: you can use 0 for x OR y to keep aspect ratio when you set the other (x or y).
Maybe you still need to crop, but I think the 0 is helpful.

3 Likes

Wow this looks really promissing! Thanks a lot for your time.
I had something similar to this in my early tests, but the problem I had with the mask() function was that it kept telling me that to mask an image the image must have the same width and height as the window. I am still not sure how you got around that problem, but I’ll give your sollution a good try sometime next week and come back with my results!

The 0 tip is a good one aswell! Thanks!

1 Like

From the mask() reference page:

The mask image needs to be the same size as the image to which it is applied.

So you need to construct a mask that has the same size as your image which is not too expensive and complicated :wink:

3 Likes

… Well I figured out why my previous code didn’t work. I wasn’t using a PGraphics to first fit the mask to the resized image, so the mask always took the width and height of the entire window. It was as simple as creating a graphic and resizing that to mask the image. Or simply just doing the image scaling after the mask is applied… then draw. I just had to switch one line of code hahaha, well thats coding in nutshell.
Poeh thank you so much, I probably wouldn’t have figured this out without you!

2 Likes

Could you please be so kind and post your solution?

By the way I had a way to distribute images over the page with 2/3/2…

2 Likes

Here it is :innocent: (click to generate a new comic):

int seed = int(random(1000));

PImage getRandomImage(float w, float h) {
  String url = "https://picsum.photos/seed/" + seed + "/" + int(w) + "/" + int(h) + ".jpg";
  seed += 1;
  return loadImage(url);
}

PImage getRandomImageWithShear(float w, float h, float shearAngle, boolean flip, boolean shearLeft, boolean shearRight) {
  float shearDist = h * tan(radians(shearAngle));
  float imgFullWidth = w + shearDist;

  PImage img = getRandomImage(imgFullWidth, h);

  PGraphics quadMask = createGraphics(img.width, img.height);

  quadMask.beginDraw();
  if (flip) {
    quadMask.quad(
      0, 0,
      shearLeft ? shearDist : 0, img.height,
      img.width, img.height,
      img.width - (shearRight ? shearDist : 0), 0
      );
  } else {
    quadMask.quad(
      shearLeft ? shearDist : 0, 0,
      0, img.height,
      img.width - (shearRight ? shearDist : 0), img.height,
      img.width, 0
      );
  }
  quadMask.endDraw();

  img.mask(quadMask);
  return img;
}

int rows = 4;
int gap = 15;
int shearAngle = 20;
int maxImagesPerRow = 4;

void generateRandomComic() {
  background(255);

  float rowHeight = (height - ((rows + 1) * gap)) / rows;
  float rowWidth = width - 2 * gap;

  strokeWeight(2);
  noFill();
  translate(gap, gap);

  for (int row = 0; row < rows; row++) {
    float rowY = row * (rowHeight + gap);
    int divisions = int(random(1, maxImagesPerRow + 1));
    int orientation = int(random(3)) - 1;

    pushMatrix();
    translate(0, rowY);

    float pieceWidth = (rowWidth - (divisions - 1) * gap) / divisions;
    boolean shearFlip = orientation == 1;

    float angle = orientation == 0 ? 0 : shearAngle;
    float shearDist = rowHeight * tan(radians(angle));
    float pieceOffset = 0;

    for (int i = 0; i < divisions; i++) {
      boolean isFirst = i == 0;
      boolean isLast = i == divisions - 1;

      float pieceRealWidth = isFirst ? pieceWidth - shearDist : pieceWidth;
      PImage img = getRandomImageWithShear(pieceRealWidth, rowHeight, angle, shearFlip, !isFirst, !isLast);

      image(img, pieceOffset, 0);

      pieceOffset += pieceRealWidth + gap;
    }
    popMatrix();
  }
}

void setup() {
  size(560, 800);
  generateRandomComic();
}

void draw() {}

void mousePressed() {
  generateRandomComic();
}

It’s not commented and might be obscure without explanations thought :smirk:

You can also randomize the rows, gap and shearAngle global parameters to get interesting results!

2 Likes

Thanks for that.

Still puzzles me that we crop big images.

In the format of the OP the images must be very wide and not very high…


fullscreen comic page as a screensaver

(not a screensaver as such, you need to start the Sketch)


// fullscreen comic page as a screen saver

int seed = int(random(1000));

int rows = 4;
int gap = 15;
int shearAngle = 20;
int maxImagesPerRow = 4;

// -------------------------------------------------------------------------------
// Two core functions

void setup() {
  // size(560, 800);
  fullScreen();
  // generateRandomComic();
  background(255);
}

void draw() {
  generateRandomComic();
}

// -------------------------------------------------------------------------------
// Input functions

void mousePressed() {
  generateRandomComic();
}

// -------------------------------------------------------------------------------
// 3 functions

void generateRandomComic() {

  // generate one page

  background(255);

  float rowHeight = (height - ((rows + 1) * gap)) / rows;
  float rowWidth = width - 2 * gap;

  strokeWeight(2);
  noFill();
  translate(gap, gap);

  for (int row = 0; row < rows; row++) {
    float rowY = row * (rowHeight + gap);
    int divisions = int(random(1, maxImagesPerRow + 1));
    int orientation = int(random(3)) - 1;

    pushMatrix();
    translate(0, rowY);

    float pieceWidth = (rowWidth - (divisions - 1) * gap) / divisions;
    boolean shearFlip = orientation == 1;

    float angle = orientation == 0 ? 0 : shearAngle;
    float shearDist = rowHeight * tan(radians(angle));
    float pieceOffset = 0;

    for (int i = 0; i < divisions; i++) {
      boolean isFirst = i == 0;
      boolean isLast = i == divisions - 1;

      float pieceRealWidth = isFirst ? pieceWidth - shearDist : pieceWidth;
      PImage img = getRandomImageWithShear(pieceRealWidth, rowHeight, angle, shearFlip, !isFirst, !isLast);

      image(img, pieceOffset, 0);

      pieceOffset += pieceRealWidth + gap; // x value
    }
    popMatrix();
  }
}

//

PImage getRandomImageWithShear(float w, float h,
  float shearAngle,
  boolean flip,
  boolean shearLeft, boolean shearRight) {
  //
  float shearDist = h * tan(radians(shearAngle));
  float imgFullWidth = w + shearDist;

  PImage img = getRandomImage(imgFullWidth, h);

  PGraphics quadMask = createGraphics(img.width, img.height);

  quadMask.beginDraw();
  if (flip) {
    quadMask.quad(
      0, 0,
      shearLeft ? shearDist : 0, img.height,
      img.width, img.height,
      img.width - (shearRight ? shearDist : 0), 0
      );
  } else {
    quadMask.quad(
      shearLeft ? shearDist : 0, 0,
      0, img.height,
      img.width - (shearRight ? shearDist : 0), img.height,
      img.width, 0
      );
  }
  quadMask.endDraw();

  img.mask(quadMask);
  return img;
}

//

PImage getRandomImage(float w, float h) {
  String url = "https://picsum.photos/seed/" + seed + "/" + int(w) + "/" + int(h) + ".jpg";
  // println(url);
  seed += 1;
  return loadImage(url);
}
//


stripped it down to display just one image full screen for 4 seconds

unsing the picsum.photos/seed/... idea.


// fullscreen image as a screen saver
// from https://discourse.processing.org/t/crop-multiple-images-to-any-quad-shape-and-place-them-side-by-side-filling-the-canvas/41326

int seed = int(random(1000));
PImage img;
int timer;

// -------------------------------------------------------------------------------
// Two core functions

void setup() {
  // size(560, 800);
  fullScreen();

  background(0);
  getRandomImage(width, height);
  timer=millis();
}

void draw() {
  background(0);

  if (millis()-timer>3999) {
    getRandomImage(width, height);
    timer=millis();
  }

  image(img, 0, 0);
}

// -------------------------------------------------------------------------------
// Input functions

void mousePressed() {
  getRandomImage(width, height);
}

// -------------------------------------------------------------------------------
// functions

void getRandomImage(float w, float h) {
  String url = "https://picsum.photos/seed/"
    + seed
    + "/"
    + int(w)
    + "/"
    + int(h)
    + ".jpg";
  // println(url);
  seed += 1;
  img = loadImage(url);
}
//

2 Likes

Yess it works quite nice in fullscreen!! :star_struck:

Yes there should be another way to crop the images without creating PGraphics of the same image size (it does not scale…)

Haha you two just keep going!

I have one small question then I am set to finnish this project (I will also let you now when I do!)
Would there be a way to have the output size(5000,6000) or something but display it size(500,600) so I can see it but export the image at full dpi without needing to buy an 8K minitor to display it xD

Yes, you can use an offscreen PGraphics just like we did earlier with the mask :wink: :

void setup() {
  size(500, 600);
  PGraphics buffer = createGraphics(5000, 6000);
  // Do your thing
  image(buffer, 0, 0, width, height); // Display it using the canvas size
}

(note that with my current implementation, doing a 5000x6000 view might take a while since it’s loading the images and croping them by creating large PGraphics buffer)

1 Like

You could make 2 parallel
PGraphics for the same images
in 2 different sizes, one for the screen,
one for the Hard Drive

1 Like

Yh I thought as much, starting to understand the whole PGraphics now.
Making the whole thing in a ‘buffer’. Exporting that buffer to a png or pdf and drawing a scaled down version of the buffer on the canvas to check what it has made. That would work right and then just start and end pdf recording within the PGraphics, I’ll give it a try thx again!

2 Likes