我有一个颜色列表和一个closest_color(pixel, colors)函数。该函数将给定像素的RGB值与我的颜色列表进行比较,并输出列表中最接近的颜色。
我需要将此功能应用于整个图像。当我尝试像素逐个使用它(通过使用2个嵌套的for循环)时,速度很慢。有没有更好的方法可以使用numpy实现这个功能?
我有一个颜色列表和一个closest_color(pixel, colors)函数。该函数将给定像素的RGB值与我的颜色列表进行比较,并输出列表中最接近的颜色。
我需要将此功能应用于整个图像。当我尝试像素逐个使用它(通过使用2个嵌套的for循环)时,速度很慢。有没有更好的方法可以使用numpy实现这个功能?
这可以使用FLANN(附带于OpenCV)实现。代码也不多。在我的旧电脑上,查找需要两秒钟。
此方法的一个优点是它可以处理“大型”调色板而不需要大量内存。但这与FLANN无关。 FLANN独特之处可能在于其所需的很少(用户端)代码。
缺点:这仍然需要几秒钟。
FLANN使用索引结构并且可以处理任意向量,并且它使用float32类型。由于FLANN中的索引结构,它的性能是次线性的(可能是O(log(n))
或sth.),即比“线性扫描”(O(n)
)更好。但是,只有当调色板变得非常巨大时,FLANN的复杂性和通用性的成本才能通过更好的查找复杂性摊销。使用特定于此问题的代码的“线性扫描”,我在另一个回答中使用numba
。
完整的笔记本:https://gist.github.com/crackwitz/bbb1aff9b7c6c744665715a5337192c0
# set up FLANN
# somewhat arbitrary parameters because under-documented
norm = cv.NORM_L2
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
fm = cv.FlannBasedMatcher(index_params, search_params)
# make up a palette and give it to FLANN
levels = (0, 64, 128, 192, 255)
palette = np.uint8([
[b,g,r]
for b in levels
for g in levels
for r in levels
])
print("palette size:", len(palette))
fm.add(np.float32([palette])) # extra dimension is "pictures", unused
fm.train()
# find nearest neighbor matches for all pixels
queries = im.reshape((-1, 3)).astype(np.float32)
matches = fm.match(queries)
# get match indices and distances
assert len(palette) <= 256
indices = np.uint8([m.trainIdx for m in matches]).reshape(height, width)
dist = np.float32([m.distance for m in matches]).reshape(height, width)
# indices to palette colors
output = palette[indices]
# imshow(output)
选项1:单张图片评估(缓慢)
Pros
- any palette any time (flexible)
Cons
- slow
- memory for large number of colors in palette
- not good for batch processing
2. 选项:批处理(超级快速)
Pros
- super fast (50ms per image), independent of palette size
- low memory, independent of image size or pallete size
- ideal for batch processing if palette doesnt change
- simple code
Cons
- requires creation of color cube (once, up to 3 minutes)
- color cube can contain only one palette
Requirements
- color cube requires 1.5mb of space on disk in form of compressed np matrix
选项1:
获取图像,创建与图像大小相同的调色板对象,计算距离,通过np.argmin索引检索新图像。
import numpy as np
from PIL import Image
import requests
# get some image
im = Image.open(requests.get("https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Big_Nature_%28155420955%29.jpeg/800px-Big_Nature_%28155420955%29.jpeg", stream=True).raw)
newsize = (1000, 1000)
im = im.resize(newsize)
# im.show()
im = np.asarray(im)
new_shape = (im.shape[0],im.shape[1],1,3)
# Ignore above
# Now we have image of shape (1000,1000,1,3). 1 is there so its easy to subtract from color container
image = im.reshape(im.shape[0],im.shape[1],1,3)
# test colors
colors = [[0,0,0],[255,255,255],[0,0,255]]
# Create color container
## It has same dimensions as image (1000,1000,number of colors,3)
colors_container = np.ones(shape=[image.shape[0],image.shape[1],len(colors),3])
for i,color in enumerate(colors):
colors_container[:,:,i,:] = color
def closest(image,color_container):
shape = image.shape[:2]
total_shape = shape[0]*shape[1]
# calculate distances
### shape = (x,y,number of colors)
distances = np.sqrt(np.sum((color_container-image)**2,axis=3))
# get position of the smalles distance
## this means we look for color_container position ????-> (x,y,????,3)
### before min_index has shape (x,y), now shape = (x*y)
#### reshaped_container shape = (x*y,number of colors,3)
min_index = np.argmin(distances,axis=2).reshape(-1)
# Natural index. Bind pixel position with color_position
natural_index = np.arange(total_shape)
# This is due to easy index access
## shape is (1000*1000,number of colors, 3)
reshaped_container = colors_container.reshape(-1,len(colors),3)
# Pass pixel position with corresponding position of smallest color
color_view = reshaped_container[natural_index,min_index].reshape(shape[0],shape[1],3)
return color_view
# NOTE: Dont pass uint8 due to overflow during subtract
result_image = closest(image,colors_container)
Image.fromarray(result_image.astype(np.uint8)).show()
选项 2:
根据您的调色板构建一个256x256x256x3大小的彩色立方体。换句话说,对于每个现有的颜色,分配相应的最接近的调色板颜色。保存彩色立方体(第一次/仅一次)。加载彩色立方体。获取图像并将图像中的每种颜色用作彩色立方体中的索引。
import numpy as np
from PIL import Image
import requests
import time
# get some image
im = Image.open(requests.get("https://helpx.adobe.com/content/dam/help/en/photoshop/using/convert-color-image-black-white/jcr_content/main-pars/before_and_after/image-before/Landscape-Color.jpg", stream=True).raw)
newsize = (1000, 1000)
im = im.resize(newsize)
im = np.asarray(im)
### Initialization: Do just once
# Step 1: Define palette
palette = np.array([[255,255,255],[125,0,0],[0,0,125],[0,0,0]])
# Step 2: Create/Load precalculated color cube
try:
# for all colors (256*256*256) assign color from palette
precalculated = np.load('view.npz')['color_cube']
except:
precalculated = np.zeros(shape=[256,256,256,3])
for i in range(256):
print('processing',100*i/256)
for j in range(256):
for k in range(256):
index = np.argmin(np.sqrt(np.sum(((palette)-np.array([i,j,k]))**2,axis=1)))
precalculated[i,j,k] = palette[index]
np.savez_compressed('view', color_cube = precalculated)
# Processing part
#### Step 1: Take precalculated color cube for defined palette and
def get_view(color_cube,image):
shape = image.shape[0:2]
indices = image.reshape(-1,3)
# pass image colors and retrieve corresponding palette color
new_image = color_cube[indices[:,0],indices[:,1],indices[:,2]]
return new_image.reshape(shape[0],shape[1],3).astype(np.uint8)
start = time.time()
result = get_view(precalculated,im)
print('Image processing: ',time.time()-start)
Image.fromarray(result).show()
以下是使用numba
的两个变体,它是一个用于Python代码的JIT编译器。
from numba import njit, prange
第一个变量使用了更多的numpy原语(np.argmin
),因此会占用"更多"内存。也许这点内存会有影响,或者numba调用numpy例程时,无法对其进行优化。
@njit(parallel=True)
def lookup1(palette, im):
palette = palette.astype(np.int32)
(rows,cols) = im.shape[:2]
result = np.zeros((rows, cols), dtype=np.uint8)
for i in prange(rows):
for j in range(cols):
sqdists = ((im[i,j] - palette) ** 2).sum(axis=1)
index = np.argmin(sqdists)
result[i,j] = index
return result
我在使用125种颜色的调色板对lena.jpg
进行处理时,每次运行大约需要180-190毫秒。
第二个版本使用更多手写代码来替换大部分numpy原语,这使得它更快。
@njit(parallel=True)
def lookup2(palette, im):
(rows,cols) = im.shape[:2]
result = np.zeros((rows, cols), dtype=np.uint8)
for i in prange(rows): # parallelize over this
for j in range(cols):
pb,pg,pr = im[i,j] # take pixel apart
bestindex = -1
bestdist = 2**20
for index in range(len(palette)):
cb,cg,cr = palette[i] # take palette color apart
dist = (pb-cb)**2 + (pg-cg)**2 + (pr-cr)**2
if dist < bestdist:
bestdist = dist
bestindex = index
result[i,j] = bestindex
return result
每次运行30毫秒!
我认为这已经接近理论最大值了,误差不超过一个数量级。我是根据所需的数学运算得出的。
每个调色板条目:A = 10个操作
3次减法,3次平方,3次加法,1次比较
每个像素:B = 1375个操作
len(palette) * (A+1),一个索引增量
每行:C = 704512个操作
ncols * (B+1),一个索引增量
每个图像:D = 360710656个操作
nrows * (C+1),一个索引增量
因此,在我的古老四核心超线程上,30毫秒内可以达到12000 MIPS(我不会说flop/s,因为没有浮点数)。这意味着每个周期接近一个指令。我确定代码缺乏一些SIMD向量化...可以调查LLVM对这些循环的看法,但我现在不想麻烦。
使用cython
编写的一些代码可能能够打败这个结果,因为在那里您甚至可以更加细致地确定变量的类型。
笔记本:https://gist.github.com/crackwitz/208a1ed8ff470ad70ae41e2061111f02