-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Thomas Lindemeier
committed
Apr 7, 2019
1 parent
188e067
commit 7e524a2
Showing
5 changed files
with
505 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
/** | ||
* Simple implementation of te painterly rendering | ||
* algorithm introduced by Hertzmann. | ||
* | ||
* This implementation only renders one layer of brushs. | ||
* Uses the output as input for a new bruh width. | ||
* | ||
* http://www.mrl.nyu.edu/publications/painterly98/hertzmann-siggraph98.pdf | ||
* University of Constance- | ||
* Department for Computergraphics | ||
* | ||
* @author Thomas Lindemeier | ||
* @date 25.10.2012 | ||
* | ||
*/ | ||
import java.util.Collections; | ||
import java.util.Vector; | ||
import java.util.Iterator; | ||
|
||
PImage sourceImage; // this is the image which should be rendered | ||
|
||
PImage brushTexture; | ||
|
||
PImage blurred; // this is the image used to grow strokes | ||
PGraphics canvas; // offscreen buffer, used to render strokes | ||
PImage colorDistance; // the difference between canvas and source | ||
PImage render; // image to render in the window | ||
|
||
int[] brushRadii = new int[] {32, 32, 32, 16, 16, 8, 8, 8, 5, 5, 5}; // used brushes | ||
int brushIndex = -1; | ||
int brushRadius = -1; // the radius of the brush | ||
final int areaError = 30; // the error threshold for the grid cells | ||
final float FC = 0.5; // weight factor of the current direction vector used to integrate stroke | ||
final int maxLength = 32; // max number of points in a stroke | ||
final int minLength = 6; | ||
float renderAlpha = 160.0f; | ||
|
||
ArrayList<ErrorRegion> grid; | ||
|
||
//////////////////////////////////////////////////////////// | ||
// compute euclidean color distance | ||
|
||
float colorDistance(color c, color s) { | ||
return sqrt(sq(red(c)-red(s)) + sq(green(c)-green(s)) + sq(blue(c)-blue(s))); | ||
} | ||
|
||
void computeImageColorDistance(final PImage canvas, final PImage source) { | ||
canvas.loadPixels(); | ||
source.loadPixels(); | ||
for (int i = 0; i < source.pixels.length; ++i) | ||
colorDistance.pixels[i] = color(colorDistance(canvas.pixels[i], | ||
source.pixels[i])); | ||
} | ||
|
||
void computeRegionColorDistance( final PImage canvas, | ||
final PImage source, final ErrorRegion er) { | ||
|
||
for (int x = er.x0; x < source.width && x < er.x0+er.w; ++x) | ||
for (int y = er.y0; y < source.height && y < er.y0+er.h; ++y) | ||
colorDistance.pixels[x+y*source.width] = | ||
color(colorDistance(canvas.pixels[x+y*source.width], | ||
source.pixels[x+y*source.width])); | ||
} | ||
|
||
|
||
//////////////////////////////////////////////////////////// | ||
/** | ||
* compute the direction with sobel operator | ||
*/ | ||
|
||
PVector sobel(final PVector vec, final PImage source) | ||
{ | ||
PVector direction = new PVector(); | ||
|
||
// directions can only be computed inside the image | ||
int x = constrain((int)vec.x,1,source.width-2); | ||
int y = constrain((int)vec.y,1,source.height-2); | ||
|
||
float tx = brightness(source.pixels[x+1 +(y-1)*source.width]) | ||
+2*brightness(source.pixels[x+1 +(y)*source.width] ) | ||
+brightness(source.pixels[x+1 +(y+1)*source.width]) | ||
-brightness(source.pixels[x-1 +(y-1)*source.width] ) | ||
-2*brightness(source.pixels[x-1 +(y)*source.width] ) | ||
-brightness(source.pixels[x-1 +(y+1)*source.width]); | ||
float ty = brightness(source.pixels[x-1 +(y+1)*source.width]) | ||
+2*brightness(source.pixels[x +(y+1)*source.width]) | ||
+brightness(source.pixels[x+1 +(y+1)*source.width]) | ||
-brightness(source.pixels[x-1 +(y-1)*source.width] ) | ||
-2*brightness(source.pixels[x +(y-1)*source.width] ) | ||
-brightness(source.pixels[x+1 +(y-1)*source.width]); | ||
|
||
// rotate vector about 90 degree | ||
direction.x = -ty; | ||
direction.y = tx; | ||
direction.z = sqrt(sq(direction.x) + sq(direction.y)); | ||
direction.normalize(); | ||
|
||
return direction; | ||
} | ||
|
||
//////////////////////////////////////////////////////////// | ||
/** | ||
* interpolates direction with runge kutta 4th order | ||
*/ | ||
|
||
PVector traceStroke(final int x, final int y, | ||
final PImage source, PVector pv) | ||
{ | ||
// compute direction perpendicular to gradient | ||
PVector v = sobel(new PVector(x, y), source); | ||
v.normalize(); | ||
if (v.z == 0) return new PVector(0, 0, 0); | ||
|
||
if (pv == null) return v; | ||
else pv.normalize(); | ||
|
||
// Hertzmann Painterly Rendering: if scalar | ||
// product is less zero, reverse vector | ||
if (pv.x * v.x + pv.y * v.y < 0) { | ||
v.x *= -1; | ||
v.y *= -1; | ||
} | ||
// Hertzmann Painterly Rendering: filter | ||
// stroke direction using previous vector | ||
float dx = FC * v.x + (1 - FC) * (pv.x); | ||
float dy = FC * v.y + (1 - FC) * (pv.y); | ||
v.x = dx / sqrt(dx*dx + dy*dy); | ||
v.y = dy / sqrt(dx*dx + dy*dy); | ||
pv.x = v.x; | ||
pv.y = v.y; | ||
v.normalize(); | ||
|
||
return v; | ||
} | ||
|
||
//////////////////////////////////////////////////////////// | ||
/** | ||
* render a stroke from a given seed point | ||
*/ | ||
|
||
Stroke computeStroke(final PVector seed, PGraphics canvas, final PImage source) | ||
{ | ||
Stroke str = new Stroke(); | ||
str.brushColor = source.get((int)seed.x, (int)seed.y); | ||
str.brushRadius = brushRadius; | ||
str.add(new PVector(seed.x, seed.y)); | ||
|
||
PVector p = new PVector(seed.x, seed.y); | ||
// previous vector, used to interpolate direction | ||
PVector lastDir = sobel(seed, source); | ||
|
||
// integrate stroke | ||
for (int i = 0; i < maxLength; ++i) | ||
{ | ||
// if longer than min length and the canvas has already a good color on it than break; | ||
if ((i > minLength) | ||
&& (colorDistance(source.get((int)p.x, (int)p.y), canvas.get((int)p.x, (int)p.y)) | ||
<colorDistance(source.get((int)p.x, (int)p.y), str.brushColor))) | ||
return str; | ||
|
||
// find actual stroke direction by integration | ||
PVector direction = traceStroke((int)p.x, (int)p.y, source, lastDir); | ||
// if gradient is vanishing then stop | ||
if (direction.z == 0) | ||
{ | ||
direction.x = 1.f; | ||
direction.y = 0.f; | ||
} | ||
lastDir = direction; | ||
|
||
// step size is brush radius | ||
p.x += direction.x * brushRadius; | ||
p.y += direction.y * brushRadius; | ||
str.add(new PVector(p.x, p.y)); | ||
} | ||
return str; | ||
} | ||
|
||
//////////////////////////////////////////////////////////// | ||
// compute the seeding grid | ||
// @param r brush radius -> cell size | ||
// @param w the width of the input image | ||
// @param h the height of the input image | ||
//////////////////////////////////////////////////////////// | ||
|
||
ArrayList<ErrorRegion> computeGrid(int r, int w, int h) { | ||
ArrayList<ErrorRegion> regions = new ArrayList<ErrorRegion>((w/r) * (h/r)); | ||
|
||
for (int x = 0; x < w; x+=r) { | ||
for (int y = 0; y < h; y+=r) { | ||
regions.add(new ErrorRegion(x, y, r, r, 0)); | ||
} | ||
} | ||
|
||
return regions; | ||
} | ||
|
||
//////////////////////////////////////////////////////////// | ||
// | ||
// reduces the brush size and draws the next layer | ||
// | ||
//////////////////////////////////////////////////////////// | ||
|
||
void nextLayer() | ||
{ | ||
// next layer | ||
brushIndex++; | ||
if (brushIndex >= brushRadii.length) return; | ||
brushRadius = brushRadii[brushIndex]; | ||
println("changing brush to width: " + brushRadius*2); | ||
|
||
// blur according to brush size | ||
blurred = sourceImage.get(); | ||
blurred.filter(BLUR, brushRadius); | ||
|
||
// compute error grids | ||
grid = computeGrid(brushRadius, blurred.width, blurred.height); | ||
|
||
// compute the color distance between canvas and source image | ||
computeImageColorDistance(canvas, blurred); | ||
|
||
//compute the average error of each grid cell | ||
for (ErrorRegion er : grid) | ||
er.computeAverageDistance(colorDistance); | ||
} | ||
|
||
//////////////////////////////////////////////////////////// | ||
// | ||
// main routines | ||
// | ||
//////////////////////////////////////////////////////////// | ||
|
||
void draw() | ||
{ | ||
// brush iteration | ||
canvas.loadPixels(); | ||
nextLayer(); | ||
|
||
if ((brushRadius <= 0) || (brushIndex >= brushRadii.length)) | ||
{ // layers are finished | ||
image(render, 0, 0); | ||
return; | ||
} | ||
|
||
// paint next stroke in current error cell | ||
canvas.beginDraw(); | ||
for (ErrorRegion er : grid) { | ||
|
||
// compute the color distance between canvas and | ||
// source image in the error cell | ||
computeRegionColorDistance(canvas, blurred, er); | ||
er.computeAverageDistance(colorDistance); | ||
|
||
// if not enough average error in grid | ||
if (er.ae < areaError) continue; | ||
|
||
// generate stroke | ||
PVector seedp = er.computeSeedPoint(colorDistance); | ||
Stroke str = computeStroke(seedp, canvas, blurred); | ||
|
||
//draw stroke | ||
str.render(canvas); | ||
//str.renderTextured(canvas); | ||
} | ||
canvas.endDraw(); | ||
image(render, 0, 0); | ||
} | ||
|
||
//////////////////////////////////////////////////////////// | ||
|
||
void setup() | ||
{ | ||
// Photo by Mat Reding on Unsplash | ||
sourceImage = loadImage("data/mat-reding-1400097-unsplash.jpg"); | ||
sourceImage.resize(864, 1296); | ||
|
||
brushTexture = loadImage("data/brush.png"); | ||
|
||
size(864, 1296, P2D); // size must always have fixed parameters... | ||
|
||
canvas = createGraphics(sourceImage.width, sourceImage.height, P2D); | ||
canvas.beginDraw(); | ||
canvas.background(255); | ||
canvas.noFill(); | ||
canvas.strokeCap(ROUND); // make round caps | ||
canvas.strokeJoin(ROUND); // let the strokes join round | ||
canvas.endDraw(); | ||
|
||
colorDistance = createImage(sourceImage.width, sourceImage.height, RGB); | ||
|
||
render = canvas; | ||
background(255); | ||
} | ||
|
||
//////////////////////////////////////////////////////////// | ||
|
||
void keyPressed() | ||
{ | ||
if (key == '1') render = sourceImage; | ||
if (key == '2') render = canvas; | ||
if (key == '3') render = colorDistance; | ||
if (key == '4') render = blurred; | ||
if (key == 's') canvas.save("result.jpg"); | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
|
||
|
||
class ErrorRegion | ||
{ | ||
int x0, y0, w, h; | ||
float ae; | ||
|
||
ErrorRegion(int px, int py, int pw, int ph, float pae) { | ||
x0 = px; | ||
y0 = py; | ||
w = pw; | ||
h = ph; | ||
ae = pae; | ||
} | ||
|
||
PVector computeSeedPoint(final PImage distance) { | ||
PVector seed = new PVector(); | ||
float errorMax = 0; | ||
float error; | ||
int index; | ||
|
||
for (int x = x0; x < x0+w; x++) | ||
for (int y = y0; y < y0+h; y++) { | ||
index = x + y * distance.width; | ||
if (index < distance.pixels.length) { | ||
error = red(distance.pixels[index]); | ||
if (error > errorMax){ | ||
errorMax = error; | ||
seed.x = x; | ||
seed.y = y; | ||
} | ||
} | ||
} | ||
return seed; | ||
} | ||
|
||
void computeAverageDistance(final PImage distance) { | ||
ae = 0; | ||
int index; | ||
int totalPixels = 0; | ||
for (int x=x0; x<x0+w; x++) | ||
for (int y=y0; y<y0+h; y++) { | ||
index = x + y * distance.width; | ||
if (index < distance.pixels.length) { | ||
ae += red(distance.pixels[index]); | ||
totalPixels++; | ||
} | ||
} | ||
ae /= totalPixels; | ||
} | ||
} |
Oops, something went wrong.