绘画与编程 - Processing 实现自动配色
之前分享的文章,图形更多是使用代码直接生成的,与绘画相关的谈得比较少。这次将介绍一个实例,可以打通绘画与编程。基于已有图片的色彩,对插画自动配色。
下面先看效果
(较早前创作的线稿)
(扫描到电脑,做简单调色)
(开始使用程序配色)
演示视频:
https://v.qq.com/txp/iframe/player.html?width=500&height=375&auto=0&vid=w03507dww2s
输出图片:
实现原理
作为范例,这里并没有用到复杂的配色算法。仅仅是在选定的图片上随机拾色。下面会提供程序的 Processing 源码以及相应的操作说明。即使你不懂编程,只要有一定动手能力,都可以上手体验。
使用指南
准备工作:
1.下载 Processing,需使用 3.0 或以上版本
2.制作一个 PSD 源文件。含线稿,以及对应色块的填充图层
( PSD 文件下载:
这段程序并没有那么智能,它无法直接对线稿自动涂色。部分工作还是需要我们在 Photoshop 上手动完成。比如对图片进行分层处理,将线稿和填色图层用独立的图层去保存。
这些步骤,同时也是 CG 绘画中常见的作画流程,是无法省略的。
如果你使用的是经过扫描的手绘线稿。那么图片背景自然是白色的,而非透明。这时如果选择在线稿上方新建图层进行涂色,很容易就会覆盖和遮挡掉线条图层。若选择在下方,由于线稿图层不透明,就会把涂色的图层都遮挡掉。
要解决这个问题,可以将线稿图层设为正片叠底模式。
正片叠底的效果有点像水彩,颜色越叠越深。线稿图层上越是偏向白色的部分,颜色就越少,所以与下面图层叠加时,不会产生影响,也就产生了透出效果。而深色部分,就会叠加保留。
线稿图层设成正片叠底后,再将新建的图层置于下方,就能直接涂色。由于程序需要的只是图层的形状轮廓,所以涂色时可随意选一种颜色进行填充。另外,要注意一点,画面上同种颜色的色块可共用一个图层。比如处于画面前方的两株植物,便同属一个图层
(图层 brushWood)
当把所有物体都分层并着色完毕,就可以开始下一个步骤
PS:除了线稿,场景中的阴影,暗部,也可以考虑使用正片叠底。源文件中的 shadow 图层便是采用这个模式
3.将图层模式为“正常”的所有图层,进行“白化处理”
所有使用了正片叠底的图层无需进行白化处理。这里所谓的白化,就是去掉图层本身的色彩倾向。这里通过快捷键 ctrl + u,把明度调到最大即可。
PS: 这里必须调白,而不仅是将色彩灰度化。这个操作与程序的着色机制有关,只有把基础色调成白色,后期才可能模拟所有色彩。
PS2: 熟悉 Photoshop 脚本的朋友,也可以在导出图片后才进行批处理。这就无需手动逐个操作
4.批量导出图层
完成后,可以通过一个命令对图层进行批量导出。选择菜单栏上的“文件”-“脚本”-“将图层导出文件”
按如上方式设置,就能导出大小一致,且包含透明通道的 png 图片。例子中的图片宽度都为 417,高度都为 700。
PS:在导出前,请清除一些不必要的隐藏图层
5.将以下源码复制到 Processing 程序中,并保存到特定路径
Processing 源代码:
import java.util.Date;
ArrayList<PImage> mulPics = new ArrayList<PImage>();
ArrayList<PImage> normalPics = new ArrayList<PImage>();
color myColors[];
int colorNum, mode;
PImage pickPic;
void setup() {
size(417, 700);
mode = 1; // 0:randomPick 1;randomFromPic
myColors = new color[0];
File[] files = listFiles(sketchPath() + "/loadPic");
for (int i = 0; i < files.length; i++) {
File f = files[i];
String strArray[] = splitTokens(f.getName(), ".");
String suffix = strArray[strArray.length-1];
if (suffix.equals("png")||suffix.equals("jpg")||suffix.equals("jpeg")) {
pickPic = loadImage("loadPic/" + f.getName());
}
}
files = listFiles(sketchPath() + "/multiply");
for (int i = 0; i < files.length; i++) {
File f = files[i];
String strArray[] = splitTokens(f.getName(), ".");
String suffix = strArray[strArray.length-1];
if (suffix.equals("png")) {
PImage temp = loadImage("multiply/" + f.getName());
mulPics.add(temp);
}
}
files = listFiles(sketchPath() + "/normal");
for (int i = 0; i < files.length; i++) {
File f = files[i];
String strArray[] = splitTokens(f.getName(), ".");
String suffix = strArray[strArray.length-1];
if (suffix.equals("png")) {
PImage temp = loadImage("normal/" + f.getName());
normalPics.add(temp);
color tempColor = pickPic.get(int(random(0, pickPic.width)),
int(random(0, pickPic.height)));
myColors = append(myColors, tempColor);
}
}
colorNum = normalPics.size();
}
void draw() {
background(255);
imageMode(CENTER);
blendMode(BLEND);
for (int i = normalPics.size() - 1; i >= 0; i--) {
tint(myColors[i]);
PImage temp = normalPics.get(i);
image(temp, width/2, height/2);
}
blendMode(MULTIPLY);
for (int i = mulPics.size() - 1; i >= 0; i--) {
tint(255);
PImage temp = mulPics.get(i);
image(temp, width/2, height/2);
}
}
void keyPressed() {
// 保存图片
if (key == 's') {
saveFrame(millis() + ".png");
}
}
void mousePressed() {
resetColor();
}
void mouseDragged() {
resetColor();
}
void resetColor() {
if (mode == 0) {
myColors = new color[0];
for (int i = 0; i < colorNum; i++) {
color tempColor = color(random(255), random(255), random(255));
myColors = append(myColors, tempColor);
}
}
if (mode == 1) {
myColors = new color[0];
for (int i = 0; i < colorNum; i++) {
color tempColor = pickPic.get(int(random(0, pickPic.width)),
int(random(0, pickPic.height)));
myColors = append(myColors, tempColor);
}
}
}
File[] listFiles(String dir) {
File file = new File(dir);
if (file.isDirectory()) {
File[] files = file.listFiles();
return files;
} else {
return null;
}
}
保存到特定位置后,会产生一个 pde 文件。这个就是 Processing 的源程序。我们需要将之前在 PS 中导出的 png 素材,复制到这个源程序的目录下。复制时,请不要更改导出图片的文件名。同时,如图新建三个文件夹。其中 normal 文件夹里,存放需要正常模式显示的图层(也就是之前经过白化处理的图层)。multiply 文件夹里,存放需要正片叠底模式显示的图层。loadPic 文件夹里,可以任意放一张 jpg,jpeg 或 png 后缀的图片文件。之后程序着色,就会基于这个图片。
7.准备工作告一段落,最后只要将程序中 size(417,700) 的两个参数,分别修改成输出图片的宽、高即可。
现在就能一劳永逸,让程序帮你完成上色工作
PS:尽量别使用过于高清的图片,载入前尽量降低图片的分辨率以适应屏幕大小
8.大功告成~只要点击或拖动鼠标,就能切换配色
当看到满意的效果,可以按下 s 键进行保存。若是是觉得以上步骤过于繁琐,你也可以在文末直接下载整个源文件。只要安装了 Processing,就能直接打开运行。把相应的图片素材拖动到文件夹里,点击程序就能自动配色。
(其他实验)
代码详解
通过随机的方式去取色。得出的结果自然不是每张都令人满意,就像作画一样,需要反复调试。这个实例,重点其实不在于提高效率本身,而是呈现一种量化的思路,让你可以尽情去探索各种色调组合。
接下来是技术分析,将从最简单的代码开始展开
实例01
PImage outLine,pic1,pic2,pic3,pic4;
int seedNum;
void setup(){
size(400,400);
outLine = loadImage("0.png");
pic1 = loadImage("1.png");
pic2 = loadImage("2.png");
pic3 = loadImage("3.png");
pic4 = loadImage("4.png");
seedNum = 0;
}
void draw(){
randomSeed(seedNum);
background(random(255),random(255),random(255));
blendMode(BLEND);
imageMode(CENTER);
tint(random(255),random(255),random(255));
image(pic4,width/2,height/2);
tint(random(255),random(255),random(255));
image(pic3,width/2,height/2);
tint(random(255),random(255),random(255));
image(pic2,width/2,height/2);
tint(random(255),random(255),random(255));
image(pic1,width/2,height/2);
blendMode(MULTIPLY);
tint(255);
image(outLine,width/2,height/2);
}
void keyPressed(){
if(key == '1'){
seedNum++;
}
if(key == '2'){
seedNum--;
}
if(key == 's'){
saveFrame(millis() + ".png");
}
}
运行效果
代码浅析:
这段代码中的所有图层颜色都是随机生成的
为了简化实例,这里只使用了 5 个图片素材。outLine 代表线稿,pic1、pic2、pic3、pic4 是已经经过白化处理的图层。它们代表了几类调子,固有色,反光,亮部,高光。
为了简明扼要,只用了 PImage 类型储存图片
在 Processing 里,绘图函数的执行顺序非常重要。这个顺序类似于 PS 中的图层。先绘制的先显示,后绘制的后显示。所以越是靠后的绘图函数,就会显示在最顶层。pic4 由于对应的是盔甲的固有色,所以最先绘制。pic1 代表高光,所以更后一点。而线稿图层必须是要置于最上层,所以最后绘制。
blendMode 函数作用是设置图层模式,参数 BLEND 相当于 PS 中的正常模式。而 MULTIPLY 则相当于正片叠底。
tint 函数决定图片在红绿蓝通道上的放出比例,它是一个对图片进行着色的函数。之所以进行白化处理,是为了让图片可以尽可能涵盖所有的色彩。
这里没有创建 color 数组去单独储存每个图层的颜色,而是取巧使用了 randomSeed。按数字键 1,2 可以切换色彩。
实例02
ArrayList<PImage> pics = new ArrayList<PImage>();
int seedNum;
void setup() {
size(400, 400);
for (int i = 0; i <=4; i++) {
PImage temp;
temp = loadImage(str(i)+".png");
pics.add(temp);
}
seedNum = 0;
}
void draw() {
randomSeed(seedNum);
background(random(255), random(255), random(255));
imageMode(CENTER);
blendMode(BLEND);
for (int i = 4; i >= 0; i--) {
if (i != 4) {
blendMode(ADD);
}
tint(random(255), random(255), random(255), 190);
if (i == 0) {
blendMode(MULTIPLY);
tint(255);
}
PImage temp = pics.get(i);
image(temp, width/2, height/2);
}
}
void keyPressed() {
if (key == '1') {
seedNum++;
}
if (key == '2') {
seedNum--;
}
if (key == '3') {
saveFrame(millis() + ".png");
}
}
运行效果:
代码浅析:
前面的例子你会发现,由于所有颜色都是随机的,所以某些调子的搭配可能有点突兀,不符合人的视觉经验。例如本来是反光或高光的部分,反而比固有色还暗。为了解决这个问题,你可以单独地控制每个图层的随机范围。也有另一种办法,就像这段代码一样,采用加色模式。
加色模式和正片叠底的模式是相反的,它会越叠越亮。所以后绘制的图层,只要不是黑色,采用加色模式并叠加到原有图层上,都会比原来的更亮。
另外,加色模式更接近真实的光色混合模式,所以色彩过渡会更自然和谐
当遇到大量的图层需要处理时,建议使用 ArrayList 或者数组。否则载入每个图片都手动输入路径,会非常繁琐。现在只要结合简单的 for 循环,就可以替代大量重复代码
END
通过上面的两个基础实例,再看开头的例子也就不难理解了。仅仅是多了一些语句,来更便捷地读取图片文件而已。将来随着你掌握的代码知识越加丰富,你可以用更好的思路去改进它。譬如创建自己的专用色板,去细化各种上色规则,拉开层次关系。
一直认为绘画本身是个不断调试过程。为了学习这个技能,我们进行大量练习,就是为了掌握一套图形语言,来更好更快地表现自己的心中所想。
但必须懂绘画才能进行图形创作吗?我觉得不然。有些时候,我们可以让程序来帮我们“执笔”,完成这些调试工作。上面的例子就是有关色的调试,让程序随机组合。此时人只要充当评审,在里面筛出符合自己审美的方案即可。
当然,这个方法也有局限,由于图片素材是位图,所以物体的形状就无法改变,也很难对它进行精确调控。除非你懂编程,这就能冲破限制,用更参数化的方式去描述这些造型,从而让树木生长,花叶飞舞。
我相信通过形色的组合,就能囊括所有情况,用代码模拟万物。
下载地址
示例合集(
若失效可点击阅读原文到 github 主页下载
相关链接:
Creative Coding入门资源索引
系列文章与小组