Looking Through A Stained Glass Darkly.

A stained glass image from a photo using Worley noise.
original photo by Pixabay.
original photo by Pixabay.

Description of this Image Manipulation code.

It's a Image Manipulation type creative coding made with Processing. It draws a stained glass image from a photo using Worley noise.

I made another image manipulation work 'MeoWorley' with Worley noise. This time, I draw borderlines and enhance the effect that looks like stained glass.

I choose the detected edges of the original photo as drawing points. And add some random points.
  // PVectors of detected edge and some random location
  ArrayList<PVector> pvs = new ArrayList<PVector>();
  pvs.addAll(detectEdge(img));
  pvs.addAll(randomAdds(rndsMax, img.width, img.height));
I draw Worley noise twice with this code to glow the glass part.
    blendMode(ADD);
    drawWorley(nodes, 5.0, 3, img.width, img.height);
    drawWorley(nodes, paleSize, 1, img.width, img.height);
You can use your photo with this code.
if your photo file is './data/your_photo.jpg'.
PImage img = loadImage("your_photo.jpg");

An example code of Processing.

Please feel free to use this example code.
To see other works based on my code is my pleasure. And my honor.
This code does not display any images on the screen but generates image files in frames directory.



/**
 * Looking Through A Stained Glass Darkly.
 * 
 * It draws a stained glass image from a photo.
 * reference. https://www.youtube.com/watch?v=4066MndcyCk&feature=youtu.be
 *
 * Processing 3.5.3
 * @author @deconbatch
 * @version 0.1
 * created 0.1 2020.06.13
 */

import java.util.Arrays;
import java.util.Collections;

void setup() {
  size(980, 980);
  colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
  rectMode(CENTER);
  smooth();
}

void draw() {

  int   frmMax     = 3;
  int   rndsMax    = 300;
  int   caseWidth  = 36;
  int   baseCanvas = max(width, height) - caseWidth * 2;
  float moveStep   = random(0.0005, 0.001);

  PImage img = loadImage("your_photo.jpg");
  float rateSize = baseCanvas * 1.0 / max(img.width, img.height);
  img.resize(floor(img.width * rateSize), floor(img.height * rateSize));
  translate((width - img.width) / 2, (height - img.height) / 2);

  // save original image
  background(0.0, 0.0, 90.0, 100.0);
  image(img, 0, 0);
  casing(img.width, img.height, caseWidth);
  saveFrame("frames/" + String.format("%04d", frmMax + 1) + ".png");

  // PVectors of detected edge and some random location
  ArrayList<PVector> pvs = new ArrayList<PVector>();
  pvs.addAll(detectEdge(img));
  pvs.addAll(randomAdds(rndsMax, img.width, img.height));
  
  for (int frmCnt = 0; frmCnt < frmMax; ++frmCnt) {

    float baseHue   = random(240.0, 380.0); // green background is no good.
    float nodeGap   = random(50.0, 100.0);
    float paleSize  = random(3.0, 8.0);
    float moveLimit = nodeGap * 0.5;

    // select nodes from PVectors, move these nodes to center
    Collections.shuffle(pvs);
    ArrayList<Node> nodes = plotImageNodes(img, pvs, nodeGap);
    for (int i = 0; i < 100; i++) {
      centralizeNodes(nodes, moveLimit, moveStep);
    }

    blendMode(BLEND);
    background(baseHue % 360.0, 90.0, 30.0, 100.0);

    blendMode(ADD);
    drawWorley(nodes, 5.0, 3, img.width, img.height);
    drawWorley(nodes, paleSize, 1, img.width, img.height);

    blendMode(BLEND);
    drawBorder(nodes, 4.0, 1, img.width, img.height, baseHue);
    casing(img.width, img.height, caseWidth);

    saveFrame("frames/" + String.format("%04d", frmCnt + 1) + ".png");

  }

  exit();

}

/**
 * randomAdds : make random PVectors
 * @param  _rndsMax : PVectors number.
 * @param  _w, _h   : x, y scope of random location.
 * @return : random PVectors.
 */
public ArrayList<PVector> randomAdds(float _rndsMax, float _w, float _h) {

  ArrayList<PVector> rnds = new ArrayList<PVector>();
  for (int rndsCnt = 0; rndsCnt < _rndsMax; rndsCnt++) {
    rnds.add(new PVector(random(_w), random(_h)));
  }
  return rnds;

}

/**
 * centralizeNodes : move nodes to the center.
 * @param _nodes : Nodes to move.
 * @param _limit : min distance that detect collision. 
 * @param _step  : moving step
 */
public void centralizeNodes(ArrayList<Node> _nodes, float _limit, float _step) {

  float range = max(width, height) * _step;

  for (Node r : _nodes) {
    boolean collide = false;
    float centerD = dist(r.x, r.y, width * 0.5, height * 0.5);
    float centerR = atan2(width * 0.5 - r.y, height * 0.5 - r.x);
    for (Node p : _nodes) {
      if (r != p) {
        float nR = atan2(p.x - r.y, p.y - r.x);
        if (abs(nR - centerR) < HALF_PI) {
          float nD = dist(r.x, r.y, p.x, p.y);
          if (nD < _limit) {
            collide = true;
            break;
          }
        }
      }
    }
    if (!collide) {
      r.x += cos(centerR) * range * centerD / width;
      r.y += sin(centerR) * range * centerD / height;
    }
  }
}

/**
 * plotImageNodes : locate Nodes with some distance each other.
 * @param _img  : original image to get color.
 * @param _adds : location point candidates.
 * @param _gap  : minimum distance between each nodes.
 * @return      : nodes.
 */
public ArrayList<Node> plotImageNodes(PImage _img, ArrayList<PVector> _adds, float _gap) {

  float gap = _gap;
  ArrayList<Node> nodes = new ArrayList<Node>();
  _img.loadPixels();

  for (int i = 0; i < _adds.size(); i++) {
    int fX = floor(_adds.get(i).x);
    int fY = floor(_adds.get(i).y);
    
    // add new node
    boolean inner = false;
    for (Node f : nodes) {
      if (dist(fX, fY, f.x, f.y) < gap) {
        inner = true;
        break;
      }
    }
    if (!inner) {
      int pixIndex = floor(fY * _img.width + fX);
      nodes.add(new Node(
                         fX,
                         fY,
                         hue(_img.pixels[pixIndex]),
                         saturation(_img.pixels[pixIndex]) * 1.2,
                         brightness(_img.pixels[pixIndex])
                         ));
    }
  }

  return nodes;

}

/**
 * drawBorder : draw lines between nodes that have the same distance.
 * @param _nodes   : holds Circle Packing results.
 * @param _borderW : border width.
 * @param _detail  : plot interval, bigger value makes light color.
 * @param _w, _h   : width and height of drawing area.
 * @param _baseHue : border color(hue value).
 */
public void drawBorder(ArrayList<Node> _nodes, float _borderW, int _detail, float _w, float _h, float _baseHue) {

  int nodeNum = _nodes.size();
  if (nodeNum < 2) return;

  strokeWeight(_borderW);
  stroke(_baseHue % 360.0, 90.0, 30.0, 10.0);
  fill(_baseHue % 360.0, 90.0, 30.0, 100.0);
  for (int iX = 0; iX < _w; iX += _detail) {
    for (int iY = 0; iY < _h; iY += _detail) {
      float[] dists = new float[nodeNum];
      for (int i = 0; i < nodeNum; i++) {
        dists[i] = dist(iX, iY, _nodes.get(i).x, _nodes.get(i).y);
      }

      // draw if the distance of the top 2 nearest nodes are nearly the same
      Arrays.sort(dists);
      if (abs(dists[0] - dists[1]) < 1.0) {
        ellipse(iX, iY, _borderW, _borderW);
      }
    }
  }
}

/**
 * drawWorley : draw Worley's noise.
 * @param _nodes    : Nodes to draw.
 * @param _cellSize : cell size, bigger value makes little size cell.
 * @param _detail   : plot interval, bigger value makes light color.
 * @param _w, _h   : width and height of drawing area.
 */
public void drawWorley(ArrayList<Node> _nodes, float _cellSize, int _detail, float _w, float _h) {

  float range = max(_w, _h);
  
  noStroke();
  for (int iX = 0; iX < _w; iX += _detail) {
    for (int iY = 0; iY < _h; iY += _detail) {
      int   minIndx = 0;
      float minDist = range;
      for (int i = 0; i < _nodes.size(); i++) {
        float distance = dist(iX, iY, _nodes.get(i).x, _nodes.get(i).y);
        if (minDist > distance) {
          minIndx = i;
          minDist = distance;
        }
      }

      float nAlp = constrain((range - minDist * _cellSize) / range, 0.0, 1.0) * 100.0;
      Node n = _nodes.get(minIndx);
      fill(n.hueVal, n.satVal, n.briVal, nAlp);
      rect(iX, iY, _detail, _detail);
    }
  }
}

/**
 * detectEdge : detect edges of photo image.
 * @param  _img : detect edges of this image.
 * @return ArrayList<PVector> : edges locations.
 */
public ArrayList<PVector> detectEdge(PImage _img) {

  ArrayList<PVector> edges = new ArrayList<PVector>();

  _img.loadPixels();
  for (int idxW = 1; idxW < _img.width - 1; idxW++) {  
    for (int idxH = 1; idxH < _img.height - 1; idxH++) {

      int pixIndex = idxH * _img.width + idxW;

      // saturation difference
      float satCenter = saturation(_img.pixels[pixIndex]);
      float satNorth  = saturation(_img.pixels[pixIndex - _img.width]);
      float satWest   = saturation(_img.pixels[pixIndex - 1]);
      float satEast   = saturation(_img.pixels[pixIndex + 1]);
      float satSouth  = saturation(_img.pixels[pixIndex + _img.width]);
      float lapSat = pow(
                         - satCenter * 4.0
                         + satNorth
                         + satWest
                         + satSouth
                         + satEast
                         , 2);

      // brightness difference
      float briCenter = brightness(_img.pixels[pixIndex]);
      float briNorth  = brightness(_img.pixels[pixIndex - _img.width]);
      float briWest   = brightness(_img.pixels[pixIndex - 1]);
      float briEast   = brightness(_img.pixels[pixIndex + 1]);
      float briSouth  = brightness(_img.pixels[pixIndex + _img.width]);
      float lapBri = pow(
                         - briCenter * 4.0
                         + briNorth
                         + briWest
                         + briSouth
                         + briEast
                         , 2);

      // hue difference
      float hueCenter = hue(_img.pixels[pixIndex]);
      float hueNorth  = hue(_img.pixels[pixIndex - _img.width]);
      float hueWest   = hue(_img.pixels[pixIndex - 1]);
      float hueEast   = hue(_img.pixels[pixIndex + 1]);
      float hueSouth  = hue(_img.pixels[pixIndex + _img.width]);
      float lapHue = pow(
                         - hueCenter * 4.0
                         + hueNorth
                         + hueWest
                         + hueSouth
                         + hueEast
                         , 2);

      // bright and saturation difference
      if (
          brightness(_img.pixels[pixIndex]) > 30.0
          && lapSat > 20.0
          ) edges.add(new PVector(idxW, idxH));

      // bright and some saturation and hue difference
      if (
          brightness(_img.pixels[pixIndex]) > 30.0
          && saturation(_img.pixels[pixIndex]) > 10.0
          && lapHue > 100.0
          ) edges.add(new PVector(idxW, idxH));

      // just brightness difference
      if (lapBri > 100.0) edges.add(new PVector(idxW, idxH));

    }
  }

  return edges;
}

/**
 * casing : draw fancy casing
 * @param _w, _h     : width and height of drawing area.
 * @param _caseWidth : border width of the case.
 */
public void casing(float _w, float _h, float _caseWidth) {

  pushMatrix();
  translate(_w * 0.5, _h * 0.5);
  fill(0.0, 0.0, 0.0, 0.0);
  strokeWeight(_caseWidth * 0.75);
  stroke(0.0, 0.0, 100.0, 100.0);
  rect(0.0, 0.0, _w + _caseWidth * 1.25, _h + _caseWidth * 1.25);
  noStroke();
  noFill();
  popMatrix();
  
}

/**
 * Node : draw and hold location and color.
 */
public class Node {

  public  float x, y;   // coordinate of node
  private float hueVal; // hue value of node
  private float satVal; // saturation value of node
  private float briVal; // brightness value of node

  Node(float _x, float _y, float _c, float _s, float _b) {
    x = _x;
    y = _y;
    hueVal = _c;
    satVal = _s;
    briVal = _b;
  }

}


/*
Copyright (C) 2020- deconbatch

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/





Yet another example.

A stained glass image from a photo using Worley noise.
original photo by Pixabay.

No comments :

Post a Comment