请问apple store里的这幅图如何用processing、Photoshop或者AI做出来?

图片来自processing大神@暗流涌动 的视频教程。如图,无数个大小不一的彩色小圆点聚集到图形轮廓周围,请问用PS、AI或processing怎么实现?如果要把彩色替换成同一色系深浅程度不同的颜色呢,比如全是各种深浅程度的蓝色圆点?
关注者
471
被浏览
74233

12 个回答

这个问题我也关注好久了。虽然 @Chaojie Mo 已经提出了思路,并分享了一个processing的实现方法,但这个实现没有复制原图中圆圈尺寸随距中心距离增大而减小的趋势,稍显可惜。

同时,他的方法中采用了一个圆圈class的阵列,在打靶过程中判断正在生成的圆是否与其他圆相交时,需要将所有圆都循环检测一遍。而每次刷新都会重新绘制所有的圆。我没有实际运行那段code,但稍稍有些担心在圆的数量较多的时候,会不会显著降低打靶速度。

于是在他的思路的基础上,我尝试用一种检测像素的方式来绘制图形。最终结果如下:


思路基本和Chaojie Mo的相同,但是在检测圆是否相交时是检测以目标点为中心,边长为半径(逐渐增长)的方框内的有色点和目标点的距离。这样避免了程序到后期因circle ArrayList增长而导致的性能下降。

我不是程序员我不知道我的改动在处理器这么快以及java框架下是否有意义。。。最初的想法只是想提供一个基于像素的程序,提供一个不同思路。如果上述有错误请多担待。

1 读取图像,初始化窗口,设置图像位置但不绘出,初始化半径最大最小值;
2 读取窗口像素,计算白色点数量,并将像素阵列转化为笛卡尔坐标系(x,y);
3 以randomGaussian生成极坐标的r,logoSize<r<windowDiagonalLength,random生成极坐标的theta,再转换成笛卡尔坐标(x,y)。当坐标不在窗口内或者不在logo内时重新生成坐标。
这样使得点最密集处为靠近logo边缘的
一个环,而不是在logo中心,避免logo边缘圆点不够密。
这种打靶方式相对于Chaojie Mo的方法的缺点是对中心是空的的图形不支持,且不可控,但是贵在省心。按下鼠标可以在两种打靶模式里切换。
4 计算坐标距窗口中心的距离,并据此初始化该点处的半径最大最小值。距中心越远,最小值越小,最大值越接近最小值;
5 从半径 r = minRadius 开始慢慢增大半径,检测rect(x-r, y-r, x+r, y+r) 范围内是否有有色点,且有色点与(x,y)距离是否小于r。
若有,则此时r为(x,y)处能得到的最大半径。以此画圆;
若没有检测到有色点,则以(x,y)处最大半径画圆;
6 在void draw()中,当白色点数量多于一定值时,重复步骤2-5;

以下的code只适用于实心图形。当图形为空心时,可以直接用randomGaussian生成x,y坐标不用极坐标转换。

鉴于样图logo边缘的圆点大小和密度显著高于其他部位,可以在初始化logo时,利用判断圆点相交的类似方法先检测logo的边缘并建立一个边缘像素区域首先填充大圆,再全局填充。
我就是懒没写这个功能,你打我呀~


PImage img;
int RadiusUpperLimit=60;
int RadiusLowerLimit=3;
int space = 0;
float Fade = 0.8;
float blankRate = 1;
float logoSize = 120;
boolean manual = true;
int manualBrushSize = 20;

void setup () {
  frameRate(6000);
  img = loadImage("apple.jpg");
  size (img.width, img.height);
  background (255);
  img.loadPixels();
  colorMode(HSB);
  noStroke();
}

void draw () {
  if (blankRate > 0.5) {
    shoot();
  } else {
    saveFrame("result#######.png");
    noLoop();
  }
}

void shoot () {
  int x = -1;
  int y = -1;
  loadPixels();
  //Finish condition calculation
  int blankN = 0;
  for (int i=0; i<pixels.length; i++) {
    if (pixels[i] == #FFFFFF) {
      blankN += 1;
    }
  }
  blankRate = blankN/float(width*height);
  println(blankRate);
  //Change linear pixel array index into screen coordinates and pick a random pixel; higher chance in the around the center
  color [][] pixelsMatrix = new color[width][height];
  for (int i = 0; i < width-1; i++) {
    for (int j = 0; j < height-1; j++) {
      pixelsMatrix[i][j]=pixels[i+j*width];
    }
  }
  println(manual);
  if (manual == false) {
    //Using polar coordinates to make a ring of dense dots
    if ((x<0) || (x>width-1) || (y<0) || (y>height-1)) {
      float r = abs(randomGaussian())*((width+height)/4-logoSize)+logoSize;
      float theta = random(0, TWO_PI);
      x = int(cos(theta)*r)+width/2;
      y = int(sin(theta)*r)+height/2;
    } else {
      if (brightness(img.pixels[x+y*width])>122) {
        float r = abs(randomGaussian())*((width+height)/4-logoSize)+logoSize;
        float theta = random(0, TWO_PI);
        x = int(cos(theta)*r)+width/2;
        y = int(sin(theta)*r)+height/2;
      }
    }
  } else {
    x = int((randomGaussian()*manualBrushSize)+mouseX);
    y = int((randomGaussian()*manualBrushSize)+mouseY);
  }
  //Change max radius and min radius according to the distance to the center of the window
  float dist = dist(x, y, width/2, height/2);
  float percentage = constrain(dist/((dist(width, height, 0, 0)/2)*Fade), 0, 1);
  int maxR = int(RadiusUpperLimit*(1-percentage) + RadiusLowerLimit*percentage);
  int minR = RadiusLowerLimit;
  //Detect the distance between filled pixels and the picked pixel within a square region and determine the radius
  int R=0;
search:
  for (int r=0; r<=maxR; r++) {
    //Growing circle, growing bounding box
    for (int i=x-r; i<=x+r; i++) {
      for (int j=y-r; j<=y+r; j++) {
        if ((i>=0) && (j>=0) && (i<width) && (j<height)) {
          if ((color(pixelsMatrix[i][j]) != #FFFFFF && dist(i, j, x, y) <= r+space) || brightness(img.pixels[i+j*width])<122) {
            R=r;
            break search;
          }
          if (r==maxR) {
            R=r;
            break search;
          }
        }
      }
    }
  }


  if (R>=minR) {
    fill(random(255), 255, 255);
    //fill(img.pixels[x+y*width]);
    ellipse(x, y, R, R);
  }
}

void keyPressed () {
  saveFrame("result#######.png");
}

void mousePressed() {
  manual = !manual;
}
虽然这个问题已经有一段时间了,但现有的答案中并没有给出用processing实现的具体方法,我介绍一下我用Processing实现这个图的方法吧,希望对同样在学习Processing的童鞋有帮助。
先上结果:
实现这个图的思路是这样的:
1、首先要有一张苹果的黑色logo图:upload.wikimedia.org/wi
2、在Processing的画布里确定苹果logo的位置(假想地确定,并不实际画出来)。
3、在画布上随机打靶,按高斯分布鼠标位置处概率最高,四周逐渐降低。(将随机打靶的高斯分布中心设为在鼠标所在的位置,通过操纵鼠标可很大地提高生成效率和可控制性。)
4、在打靶打中的位置判断是否接受该位置,有两种情况不予接受,即 1)该处在已有圆球之内; 2)该位置在苹果的logo之内(通过读取与打靶位置对应的第一步中苹果logo图中的亮度值来判断)。
5、若打靶位置被接受则在该点放置彩色小球,小球半径逐渐增大,遇到三种情况时停止增大: 1)大于预设的最大半径rmax(rmax也呈高斯分布); 2)与已有小球发生接触; 3)与苹果logo发生接触(同样是通过图像亮度值判断)。这里先确定位置再确定小球半径而不是一开始就确定的半径再确定小球位置的想法很关键。
6、通过不断打靶添加小球就得到上面这张图了。

上面的结果中每个小球颜色都是随机的,也就是在HSB颜色模式下,Hue在(0,255)之间平均分布,Saturation,Brightness则为定值255。要实现题主说的各种深浅程度的蓝色圆点那么只要把Hue定为蓝色(比如180),然后令saturation在(0,255)随机分布即可:
用Processing的好处就是一种处理方法实现后可以方便地更换很多对象,比如把苹果logo改成19,用几乎同样的程序就可以得到下面的结果:




附实现第一个图的程序:
PImage img;
ArrayList <Circle> circles = new ArrayList <Circle> ();
int shiftx, shifty;
void setup(){
  //noLoop();
  colorMode(HSB);
  background(255);
  smooth();
  noStroke();
  img = loadImage("Apple_logo_black.jpg");
  //注意从维基上下载的Apple logo是png格式的,要转成jpg的才能用,
  //否则出来的就是个矩形框了,具体原因我也不清楚。
  size((int) (img.width*2), (int) (img.height*1.2));
  shiftx = width/2-img.width/2;
  shifty = height/2-img.height/2;
  img.loadPixels();
  //image(img,shiftx,shifty);
}
void draw(){
  //img.loadPixels();
  background(255);
  //image(img,shiftx,shifty);
  if(circles.size()>0){
    for(Circle c : circles){
      c.display();
    }
  }
  float xn, yn;
  while(true){
    xn = (randomGaussian()*200)+mouseX;
    yn = (randomGaussian()*200)+mouseY;
    int xns,yns;
    xns = (int) (xn-shiftx);
    yns = (int) (yn-shifty);
    if (xns<0 || xns>=img.width || yns<0 || yns>=img.height) break;
    int loc = xns + yns*img.width;
    float b=brightness(img.pixels[loc]);
    if(b>50){
      break;
    }
  }
  boolean sign = true;
  for(Circle c:circles){
    if(dist(xn,yn,c.x,c.y)<c.r+2){
      sign = false;
      break;
    }
  }
  if(sign){     
    Circle cir = new Circle(xn,yn);
    cir.grow(randomGaussian()*5+10);
    circles.add(cir);
  }
}



class Circle{
  float x,y,r;
  color c;
  Circle(float xin, float yin){
    x = xin;
    y = yin;
    c = color(random(255),255,255);
  }
  void display(){
    noStroke();
    fill(c);
    ellipse(x,y,2*r,2*r);
  }
  void grow(float rmax){
    for(float ri=2; ri<rmax; ri+=1){
      r = ri;
      boolean sign1 = false;
      for(Circle c : circles){
        if(dist(x,y,c.x,c.y) <= c.r+r){
          sign1 = true;
          break;
        }
      }
      if(sign1){
        break;
      }
      boolean sign2 = false;
      for(int i=0; i<360; i++){
        float rad = radians(i);
        float xa = x+cos(rad)*r;
        float ya = y+sin(rad)*r;
        int xs = (int) (xa-shiftx);
        int ys = (int) (ya-shifty);
        if(xs<0 || xs>=img.width || ys<0 || ys>=img.height) break;
          int loc = (int) (xs+ys*img.width);
          float b = brightness(img.pixels[loc]);
          if(b<50){
            sign2 = true;
            break;
          }
        }
      if(sign2){
        break;
      }
    }
  }
}
void mousePressed(){
  save("apple2.png");
}
为什么?