其实,Photoshop本质是一种数学软件,它把图片作为一个矩阵来处理,只不过隐藏了内部的数学原理。所以PS能做的,R语言在一定程度上也可以做。首先介绍一个最为简单的软件包,名字叫做“jpeg”,里面仅包含两个函数:一个是读取jpg,一个是写入jpg,而使用方法更是简单无比。magick在这里负责显示图片。

加载R包,读取原图片

1
2
3
4
5
library(jpeg)
library(magick)
orgpic = readJPEG("~/mydata/Desert.jpg")
Desert = image_read("~/mydata/Desert.jpg")
print(Desert)  # 查看原图
1
2
##   format width height colorspace matte filesize density
## 1   JPEG  1024    768       sRGB FALSE   154671   72x72

敲一下dim(orgpic),你会发现结果是:768 1024 3, 也就是说,这是一个array,代表了1024*768的一幅图片,每个像素点都由一个三维向量(R,G,B)来描述。我们可以这样访问一个像素点:orgpic[300,400,]结果并不是一般人在PS类软件常见的0到255之间的整数,而是0到1之间的浮点数。

下面来做一个负片效果

1
2
3
4
5
6
7
neg_pic = 1-orgpic
writeJPEG(neg_pic, quality = 0.95, 
          target = "~/mydata/neg_Desert.jpg")

# 效果如下
neg_Desert = image_read("~/mydata/neg_Desert.jpg")
print(neg_Desert)
1
2
##   format width height colorspace matte filesize density
## 1   JPEG  1024    768       sRGB FALSE   155612   72x72

是不是略酷?虽然没有什么实际的用途…… 接下来可以做出很多类似操作。比如,原图像的平方是什么?

做原图像的平方

1
2
3
4
5
6
writeJPEG(orgpic^2, quality = 0.95,
          target = "~/mydata/square_Desert.jpg")

# 效果如下
square_Desert = image_read("~/mydata/square_Desert.jpg")
print(square_Desert)
1
2
##   format width height colorspace matte filesize density
## 1   JPEG  1024    768       sRGB FALSE   153168   72x72

看起来似乎比原来的图像更深沉了。

原图像的平方根什么效果?

1
2
3
4
5
6
writeJPEG(sqrt(orgpic), quality = 0.95, 
          target = "~/mydata/sqrt_Desert.jpg")

# 效果如下
sqrt_Desert = image_read("~/mydata/sqrt_Desert.jpg")
print(sqrt_Desert)
1
2
##   format width height colorspace matte filesize density
## 1   JPEG  1024    768       sRGB FALSE   174798   72x72

你猜对了,图片会更加明亮清淡一些。难道这就是所谓日系小清新的奥秘? 事实上,常见的PS操作,本质都是一些数学运算。比如调整曲线就是建立一个映射,最简单的降噪方法就是加权平均。调整对比,亮度,饱和等等这些原理也都不复杂。

下面我们可以自己定义一个分段二次函数看看效果

自定义分段二次函数作用于原图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
seg_curve = function(orgpic,low,high){ 
  high=min(high, 2) 
  low=min(low, 2) 
  high=max(high, -2) 
  low=max(low, -2) 
  trans<-function(x){ 
    (x < 0.5) * (low * x^2 + (1 - 0.5 * low) * x ) +
    (x >= 0.5) * (-high * x^2 + (1 + 1.5 * high) * x - 0.5 * high) 
  } 
  newpic=trans(orgpic) 
  return(newpic) 
  } 

这个分段二次函数,以0.5为中心点。当x<0.5时,函数定义为low*x^2+(1-0.5*low)*x;当x>=0.5时,函数定义为-high*x^2+(1+1.5*high)*x-0.5*high,这里low与high都是我们需要指定的参数。这个函数有如下性质:f(0)=0,f(0.5)=0.5,f(1)=1;在[0,1]区间内单调增,是连续可导函数。

这里我们可以看到,其实这个分段二次函数就是一种“曲线”。

单调增的性质表明,如果原来这个像素点是R>G>B,加工后还是,不会改变图像原有的基本观感。而两个端点的限制则主要是为了保证取值在我们允许的范围内。f(0.5)=0.5这个限制比较特殊,它的意思就是说,尽可能的不大幅改变中间调附近的颜色。这跟前面的平方函数就不同。平方函数直接就把0.5变成0.25了,图像整体会变暗,要是更高次方,可能就接近于0,黑乎乎一片了。而此处的限制,基本上保证图像在“能看”的范围内。

其中的两个参数,low和high分别控制高光与阴影,取值范围都在[-2,2]内,即使你指定了范围之外的值也会自动恢复到边界。当这两个数值设置的比较大时,亮部会更亮,暗部会更暗,直观看就是对比度增强。反之,如果取得是负数,图片会变得比较“苍白”。如果是0的话,那么就退化为恒等的一次函数。

1
2
3
4
5
6
writeJPEG(seg_curve(orgpic,2,2), quality = 0.95, 
          target = "~/mydata/seg2.jpg")

# 效果如下
seg2 = image_read("~/mydata/seg2.jpg")
print(seg2)
1
2
##   format width height colorspace matte filesize density
## 1   JPEG  1024    768       sRGB FALSE   190355   72x72

下图看起来似乎是蓝天更鲜艳了,也许是对比/饱和度加强?

下面来做另一个例子,反转负冲

这里需要分别控制R,G,B三个通道。下面这个函数的基本原理就是,对G,B通道,反相(例如1-orgpic[,,3])后正片叠底(temp[,,3]*temp[,,3])再与原片混合(b*temp[,,3]+(1-b)*orgpic[,,3]);对于R通道,不反相而直接颜色加深(2-1/temp[,,1])。 注:不难证明,2-1/temp[,,1] <= temp[,,1],利用的是均值不等式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
changehue = function(orgpic, r, g, b) { 
  newpic = orgpic 
  temp = orgpic 
  temp[,,3] = b*(1-orgpic[,,3])+(1-b)*orgpic[,,3] 
  temp[,,3] = temp[,,3]*temp[,,3] 
  newpic[,,3] = b*temp[,,3]+(1-b)*orgpic[,,3] 
  temp[,,2] = g*(1-orgpic[,,2])+(1-g)*orgpic[,,2] 
  temp[,,2] = temp[,,2]*temp[,,2] 
  newpic[,,2] = g*temp[,,2]+(1-g)*orgpic[,,2] 
  temp[,,1] = 2-1/temp[,,1] 
  temp[,,1][temp[,,1]<0] = 0 
  newpic[,,1] = r*temp[,,1]+(1-r)*newpic[,,1] 
  return(newpic) } 

writeJPEG(changehue(orgpic,0.7,0.2,0.5), quality = 0.95, 
          target = "~/mydata/changehue.jpg")

# 效果如下
changehue = image_read("~/mydata/changehue.jpg")
print(changehue)
1
2
##   format width height colorspace matte filesize density
## 1   JPEG  1024    768       sRGB FALSE   151641   72x72

效果是不是很神奇?

RGB当然大家都知道。但是,我们一般谈论颜色,很少会说“某某分量占多少”,更多的时候我们的表达方式可能是:比较深,比较浅,等等。那么这就与HSL或HSV色彩模式相关。这里H是Hue,色相。S是Saturation,饱和度。L是明度,V是色调。基础知识可以参考网络资源。RGB色彩模式与HSL,HSV色彩模式之间互相转换的关系此处从略。

再来一个很灵活的转换为黑白的函数

1
2
3
4
5
6
7
8
RGBBW = function(orgpic, r, g, b) { 
  newpic = orgpic 
  newmatrix = (r * orgpic[,,1] + g * orgpic[,,2] + b * orgpic[,,3]) / (r + g + b) 
  newpic[,,1] = newmatrix 
  newpic[,,2] = newmatrix 
  newpic[,,3] = newmatrix 
  return(newpic) 
  } 

这个函数最终返回一个加权平均值,权重可以自定。比如RGBBW(orgpic,1,0,0)就相当于给照片加了个红色滤镜。我们来随便做个例子:

1
2
3
4
5
6
writeJPEG(RGBBW(orgpic,2,1,2), quality = 0.95, 
          target = "~/mydata/bw.jpg")

# 效果如下
bw = image_read("~/mydata/bw.jpg")
print(bw)
1
2
##   format width height colorspace matte filesize density
## 1   JPEG  1024    768       sRGB FALSE   142308   72x72

在人像照片处理中,假设人物穿的是蓝色的衣服,那么,如果直接加红色滤镜,衣服就会看起来像是黑的。能否让人脸的红色表现得更白,同时衣服也不要太黑呢? 用这个就可以:RGBBW(orgpic,2,1,2),这样的话绿色通道会得到抑制,而红色与蓝色都会变强,相当于一个近似的绿色截止滤镜,可以看到草地就变黑了。

众所周知,宾得相机的色彩风格很有特点。虽然我们没有办法知道很细节的东西,不过,我们可以做一个简单粗暴的假设:宾得机身直出jpg不同风格的算法只是对RGB三个通道做分别的计算,那么实际是很容易做逆向工程的。比如说,首先用自然模式拍一张照片,然后利用宾得的机身处理功能,分别生成鲜明,人像,风景,风雅,明亮等风格的照片。接下来,我们把这几张图片读入R当中,以自然模式的数据作为x,以其他风格的数据作为y,做一个模型拟合。因为操作步骤较多,因此这里不再赘述。