-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSimplePainterlyRendering.pde
299 lines (244 loc) · 8.82 KB
/
SimplePainterlyRendering.pde
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
/**
* Simple implementation of te painterly rendering
* algorithm introduced by Hertzmann.
*
* http://www.mrl.nyu.edu/publications/painterly98/hertzmann-siggraph98.pdf
*
* @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, 3, 3}; // used brushes
int brushIndex = -1;
int brushRadius = -1; // the radius of the brush
final int areaError = 20; // 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("data/result.jpg");
}