Auto-masking several similar photos

Hello Community,

Thank you in advance for reading.

The children in our free school have been given coloring book with animal drawings in it. While children are coloring the books, I am planning to live-project these colored animals in form of a simple story by launching them in processing and making them move/run/hide (un-animated).

Is there a way, I can programming processing to auto-mask/crop these animal pictures (scanned from phone) to give only the animal shape while deleting all white space around the drawing.

Am not sure where to start looking. Any help highly welcome and appreciated.

Raj

Sampling paper example - https://goo.gl/24jQZg

Answers

  • Answer ✓

    I think it is doable. If all the animals(pictures) have a well defined contour, then I can imagine applying the magic wand example provided by @jeremydouglass to the 4 corners of the picture.

    What is the magic wand (flood fill) algorithm? It ensures all pixels around certain pixel is painted certain color

    Why the four corners of the picture? I am assuming that the image of interest is at the center of the picture. Then I am assuming (again) that if I flood fill all the four corners (tips of the picture), then I can attack the background with the same color.

    So what happened to the background of the picture from your student? The background of the photo gets flood fill with this color: 0x00000000 which means that the background is set to transparent (More in the reference here: Alpha + Transparency)

    Now, @jeremydouglass has shared a demo using the magic wand here: https://forum.processing.org/two/discussion/comment/101833#Comment_101833

    Next, for your case, and because we are going to be working with transparencies, we are forced to work with PGraphics objects as this kind of objects honors the transparency attribute. Check more here: https://processing.org/reference/createGraphics_.html

    However, we still need one more trick. To save the file in Processing and when working with PGraphics, we need to work with @pandanautinspace and @GoToLoop's trick as mentioned here in this post https://forum.processing.org/two/discussion/17320/likely-a-bug-processing-can-t-save-different-pgraphics-of-different-sizes. In a nutshell, if you call PGraphics.save(), it is very likely you will get a null point exception. The trick in the above post solves the issue.

    Finally, here I present my demo for you to use. First, I downloaded three images and I put my Picasso's hat. Then I saved the images. You can access the images below. Make sure you save those images in the data folder of the sketch (I am assuming you know how Processing works. If you have any doubts, just ask). Then run the script. The script will load each image one at the time, show it to you for half second and then it will process it as described above. The image is saved in the sketch folder. After it finishes processing all three images, it goes into a second stage where it shows all three images randomly in the sketch to give it a "collage" look. Any click event on the canvas randomly places the images in a new position with a different shaded background.

    Kf

    cat

    dolphin

    monkey

    import java.util.Queue;
    import java.util.ArrayDeque;
    Queue<PVector> queue = new ArrayDeque<PVector>();
    
    PImage img;
    
    PGraphics pg;
    
    String[] studentsImgs = {"dolphin.png", "monkey.png", "cat.jpg"};
    int index=0;
    
    
    void setup() {
      size(600, 600);
      fill(255, 0, 0);
    
      pg = createGraphics(width, height);
    
      ////https://forum.processing.org/two/discussion/17320/likely-a-bug-processing-can-t-save-different-pgraphics-of-different-sizes
      //NEEDS to call this line so pg.save() works.
      hint(DISABLE_ASYNC_SAVEFRAME);  
    
      // img = loadImage ("https://" + "upload.wikimedia.org/wikipedia/commons/thumb/4/40/Sunflower_sky_backdrop.jpg/693px-Sunflower_sky_backdrop.jpg");
      img.resize(width, height);
      frameRate(2);
    }
    
    void draw() {
      //FIRST STEP: Shows every image and then it processes them by removing their background
      //SECOND STEP: Shows images all together in random background
    
      //FIRST STEP
      if (index<studentsImgs.length) {
        img = loadImage (studentsImgs[index]);
        img.resize(width, height);
        image(img, 0, 0);
    
        removeBackgroundAndSave();
        index++;
      } else {
    
        //SECOND STEP
        imageMode(CENTER);
        background(random(92,172));  
        for(int i=0;i<studentsImgs.length;i++){
          pushMatrix();
    
          //Next line places the image distributed evenly along the width and randomly along the height
          translate(((i+1)*1.0/(studentsImgs.length+1))*width,random(0.1,0.9)*height);
    
          //Random rotation between +45 to -45 degrees
          rotate(random(-PI/4,PI/4));
    
          img = loadImage ("image"+i+".png");
    
          //Resize image randomly between 10% and 35% of it original size
          img.resize((int)(random(0.2,0.65)*img.width),0);
          image(img,0,0);
          popMatrix();
        }
        noLoop();
        frameRate(30); //Make canvas responsve
    
      }
    
    
    }
    
    void removeBackgroundAndSave() {
      //Attack four corners
      int x=0, y=0;
      floodFill(x, y, get(x, y), 96);
    
      x=width-1;
      y=0;
      floodFill(x, y, get(x, y), 96);
    
      x=width-1;
      y=height-1;
      floodFill(x, y, get(x, y), 96);
    
      x=0;
      y=height-1;
      floodFill(x, y, get(x, y), 96);
      pg.save("image"+index+".png");
    }
    
    void mouseClicked() {
      //floodFill(mouseX, mouseY, get(mouseX, mouseY), 96);
      redraw();
    }
    
    //DEBUGGING & first prototype
    //void keyPressed() {
    //  if (key=='r') {
    //    img = loadImage ("dolphin.png");
    //    img.resize(width, height);
    //    image(img, 0, 0);
    //  }
    
    //  if (key=='s') {
    //    pg.save("image.png");
    //  }
    //  if (key=='l') {
    //    background(random(255));
    //    img = loadImage ("image.png");
    //    image(img, 0, 0);
    //  }
    //}
    
    void floodFill(int xSeed, int ySeed, color c_selected, int sensitivity) {
      pg.beginDraw();
      pg.image(img, 0, 0);
      pg.loadPixels();
    
      boolean[][] flooded  = new boolean[width][height];
      queue.clear();
      queue.add(new PVector(xSeed, ySeed));
    
      while (!queue.isEmpty()) {
        PVector point = queue.remove();
        int x = (int)point.x;
        int y = (int)point.y;
    
        // skip point that is out of bounds
        if (x >= 0 && x < width && y >= 0 && y < height) {
          // skip point that was previously flooded
          if (flooded[x][y]) {
            continue;
          }
          // skip and mark point that is out of color range
          if ( colorDist(get(x, y), c_selected) > sensitivity) {
            flooded[x][y] = true;
            continue;
          }
          //set(x, y, 0x0000f700);
          pg.pixels[x+y*width]=0x00000000;
          flooded[x][y] = true;
          // queue flooding in four directions
          queue.add(new PVector(x - 1, y));
          queue.add(new PVector(x + 1, y));
          queue.add(new PVector(x, y - 1));
          queue.add(new PVector(x, y + 1));
        }
      }
      pg.updatePixels();
      pg.endDraw();
    }
    
    float colorDist(color c1, color c2) {
      return dist(red(c1), green(c1), blue(c1), red(c2), green(c2), blue(c2));
    }
    
  • @krafzer - gratitude from Cambodia. Promise to share pics how its used. in couple of weeks.

  • rajraj
    edited August 2017

    @krafzer - I found a video of what I imagined already done here : https://www.facebook.com/teamLab.inc/videos/10154570445785940 . Ofcourse not at this scale. We don't have such resources :) .. Just wanted to share with you. I am working and making progress albeit very slowly ..

  • @raj Nice and very Interesting... all of it can be done in Processing. I guess the moving figures need to be known beforehand. For example, the flying bird... kids will paint a standard flying figure. When you scan the figure, you will have a sprite, a set of figures that together represents some motion, and you map the scan main figure to the sprite version.... and voila, there you have, your figure moving using a provided pattern... the end result will be like a gif file. The last effect can be accomplish using PGraphics objects overlaid with decreasing alpha values.

    Kf

  • Atleast someone else believes my crazy vision :) Honestly - I didn't figure until this far yet. But thanks for knitting it together. I would like to try and see how far I can go :)

  • See also for example

    That spider is just a single, simple mask, but you could map the legs separately (rather than creating a single cut-out) and then you would have a simple illusion of painted limbs -- not unlike like the more complex lizards and birds etc. in your reference video.

  • @jeremydouglass - thank you. i like the spider movement. PGraphics looks like way to go about it. Yes with very subtle and simple animation plus numbers of such pre-determined animals/insects colored by many kids will fill the wall.

  • Hi @kfrajer - hope you are doing well. With your example above, I extended it to work on multiple folders with different set of images. To make the process easy. Works very well. However pg.save gives a run time error.
    "Waiting 5000ms ...... "

    I have attached the folder for your reference. In the attached link in file floodfill.pde - if I remove the comment on line #127, I get the error.

    line 127# //pg.save("/data/Rockets/image"+imagesInFolder+".png");

    Been trying for few weeks and also asked my friends but no luck. So reaching our to you directly. Any help or direction is highly appreciated. thank you a lot.

  • Hey, I saw some posts that you might find interesting:

    https://www.giraffest.ca/brandon-hearty
    http://www.ivanguaderrama.com/blog/our-new-app-is-out

    Related to your question, if you comment out all the references to spout, the error goes away. I am not familiar with spout. What are you using it for? Calling it and sending data in your draw() function is the culprit. You are firing spout and sending data at a rate of 30 fps. If you tell us more of what you want to do, I am sure there is a more convenient way to do this. After I remove your spout references, I am getting sporadic NPE (See below) but they do not halt your program. It could be related to accessing the movie object.

    Notice that after I am able to run your program, the carriages and rockets seems to be drawn in the same image. You might want to revise that part of the code.

    Kf

    
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/BoatsInstagram              index =0         =====studentsImgs{imagesInFolder] ==B1.jpg
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/BoatsInstagram              index =1         =====studentsImgs{imagesInFolder] ==B2.jpg
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/BoatsInstagram              index =2         =====studentsImgs{imagesInFolder] ==B3.jpg
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/CarriagesInstagram              index =0         =====studentsImgs{imagesInFolder] ==C1.jpg
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/CarriagesInstagram              index =1         =====studentsImgs{imagesInFolder] ==C2.jpg
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/CarriagesInstagram              index =2         =====studentsImgs{imagesInFolder] ==C3.jpg
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/CarriagesInstagram              index =3         =====studentsImgs{imagesInFolder] ==C4.jpg
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/RocketsInstgram              index =0         =====studentsImgs{imagesInFolder] ==image0.png
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/RocketsInstgram              index =1         =====studentsImgs{imagesInFolder] ==image1.png
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/RocketsInstgram              index =2         =====studentsImgs{imagesInFolder] ==image2.png
    Folder Name ===  C:\Users\C\Downloads\FloodFillandMove-20171123T035320Z-001\FloodFillandMove/data/4KStogram/RocketsInstgram              index =3         =====studentsImgs{imagesInFolder] ==image3.png
    
    java.lang.NullPointerException
        at processing.opengl.Texture.copyBufferFromSource(Texture.java:827)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.read(Unknown Source)
        at FloodFillandMove.movieEvent(FloodFillandMove.java:83)
        at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.fireMovieEvent(Unknown Source)
        at processing.video.Movie.invokeEvent(Unknown Source)
        at processing.video.Movie$1.bufferFrame(Unknown Source)
        at org.gstreamer.elements.BufferDataAppSink$AppSinkNewBufferListener.newBuffer(BufferDataAppSink.java:163)
        at org.gstreamer.elements.AppSink$2.callback(AppSink.java:184)
        at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.invokeCallback(CallbackReference.java:485)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.callback(CallbackReference.java:515)
    java.lang.NullPointerException
        at processing.opengl.Texture.copyBufferFromSource(Texture.java:827)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.read(Unknown Source)
        at FloodFillandMove.movieEvent(FloodFillandMove.java:83)
        at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.fireMovieEvent(Unknown Source)
        at processing.video.Movie.invokeEvent(Unknown Source)
        at processing.video.Movie$1.bufferFrame(Unknown Source)
        at org.gstreamer.elements.BufferDataAppSink$AppSinkNewBufferListener.newBuffer(BufferDataAppSink.java:163)
        at org.gstreamer.elements.AppSink$2.callback(AppSink.java:184)
        at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.invokeCallback(CallbackReference.java:485)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.callback(CallbackReference.java:515)
    java.lang.NullPointerException
        at processing.opengl.Texture.copyBufferFromSource(Texture.java:827)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.read(Unknown Source)
        at FloodFillandMove.movieEvent(FloodFillandMove.java:83)
        at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.fireMovieEvent(Unknown Source)
        at processing.video.Movie.invokeEvent(Unknown Source)
        at processing.video.Movie$1.bufferFrame(Unknown Source)
        at org.gstreamer.elements.BufferDataAppSink$AppSinkNewBufferListener.newBuffer(BufferDataAppSink.java:163)
        at org.gstreamer.elements.AppSink$2.callback(AppSink.java:184)
        at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.invokeCallback(CallbackReference.java:485)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.callback(CallbackReference.java:515)
    java.lang.NullPointerException
        at processing.opengl.Texture.copyBufferFromSource(Texture.java:827)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.read(Unknown Source)
        at FloodFillandMove.movieEvent(FloodFillandMove.java:83)
        at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.fireMovieEvent(Unknown Source)
        at processing.video.Movie.invokeEvent(Unknown Source)
        at processing.video.Movie$1.bufferFrame(Unknown Source)
        at org.gstreamer.elements.BufferDataAppSink$AppSinkNewBufferListener.newBuffer(BufferDataAppSink.java:163)
        at org.gstreamer.elements.AppSink$2.callback(AppSink.java:184)
        at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.invokeCallback(CallbackReference.java:485)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.callback(CallbackReference.java:515)
    java.lang.NullPointerException
        at processing.opengl.Texture.copyBufferFromSource(Texture.java:827)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.read(Unknown Source)
        at FloodFillandMove.movieEvent(FloodFillandMove.java:83)
        at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.fireMovieEvent(Unknown Source)
        at processing.video.Movie.invokeEvent(Unknown Source)
        at processing.video.Movie$1.bufferFrame(Unknown Source)
        at org.gstreamer.elements.BufferDataAppSink$AppSinkNewBufferListener.newBuffer(BufferDataAppSink.java:163)
        at org.gstreamer.elements.AppSink$2.callback(AppSink.java:184)
        at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.invokeCallback(CallbackReference.java:485)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.callback(CallbackReference.java:515)
    java.lang.NullPointerException
        at processing.opengl.Texture.copyBufferFromSource(Texture.java:827)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.read(Unknown Source)
        at FloodFillandMove.movieEvent(FloodFillandMove.java:83)
        at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.fireMovieEvent(Unknown Source)
        at processing.video.Movie.invokeEvent(Unknown Source)
        at processing.video.Movie$1.bufferFrame(Unknown Source)
        at org.gstreamer.elements.BufferDataAppSink$AppSinkNewBufferListener.newBuffer(BufferDataAppSink.java:163)
        at org.gstreamer.elements.AppSink$2.callback(AppSink.java:184)
        at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.invokeCallback(CallbackReference.java:485)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.callback(CallbackReference.java:515)
    java.lang.NullPointerException
        at processing.opengl.Texture.copyBufferFromSource(Texture.java:827)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.read(Unknown Source)
        at FloodFillandMove.movieEvent(FloodFillandMove.java:83)
        at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at processing.video.Movie.fireMovieEvent(Unknown Source)
        at processing.video.Movie.invokeEvent(Unknown Source)
        at processing.video.Movie$1.bufferFrame(Unknown Source)
        at org.gstreamer.elements.BufferDataAppSink$AppSinkNewBufferListener.newBuffer(BufferDataAppSink.java:163)
        at org.gstreamer.elements.AppSink$2.callback(AppSink.java:184)
        at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.invokeCallback(CallbackReference.java:485)
        at com.sun.jna.CallbackReference$DefaultCallbackProxy.callback(CallbackReference.java:515)
    
    reszing
    reszing
    size = 86      xPos=Ypos=748
    reszing
    reszing
    reszing
    reszing
    reszing
    reszing
    
  • Thank you a lot kfrazer. It worked. I use spout to bring the processing sketch into resolume arena (projection mapping software) to project on 3D surface as we don't find plain 2D surfaces all the time in areas we do these social activities.

    Yes , i have to fix the overlapping :).

Sign In or Register to comment.