查看原文
其他

绘画与编程 - Processing 实现自动配色

2016-11-30 Wenzy InsLab

之前分享的文章,图形更多是使用代码直接生成的,与绘画相关的谈得比较少。这次将介绍一个实例,可以打通绘画与编程。基于已有图片的色彩,对插画自动配色。

下面先看效果

(较早前创作的线稿)

(扫描到电脑,做简单调色)

(开始使用程序配色)

演示视频:

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入门资源索引
系列文章与小组


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存