Files
2026-03-01 21:36:54 +08:00

249 lines
9.1 KiB
Python

# NOTE: requires pillow, pypotrace>=0.2, numpy, scipy to be installed
from PIL import Image
import potrace
import numpy
import enum
from scipy.cluster.vq import kmeans
from .. import objects
from ..nvector import NVector
from .pixel import _vectorizing_func
class QuanzationMode(enum.Enum):
Nearest = 1
Exact = 2
class RasterImage:
def __init__(self, data):
self.data = data
@classmethod
def from_pil(cls, image):
return cls(numpy.array(image))
#@classmethod
#def open(cls, filename):
#return cls.from_pil(Image.open(filename))
def k_means(self, n_colors):
"""!
Returns a list of centroids
"""
colors = []
for row in range(self.data.shape[0]):
for column in range(self.data.shape[1]):
if self.get_alpha(row, column) == 255:
colors.append(self.data[row][column])
colors = numpy.array(colors, numpy.float)
return kmeans(colors, n_colors+1)[0]
def get_alpha(self, row, column):
if self.data.shape[2] >= 4:
return self.data[row][column][3]
return 255
def quantize(self, codebook, quantization_mode=QuanzationMode.Nearest):
"""!
Returns a list of tuple [color, data] where for each color in codebook
data is a bit mask for the image
You can get codebook from k_means
"""
if codebook is None or len(codebook) == 0:
return [(numpy.array([0., 0., 0., 255.]), self.mono())]
mono_data = []
for c in codebook:
mono_data.append((c, numpy.zeros(self.data.shape[:2])))
for row in range(self.data.shape[0]):
for column in range(self.data.shape[1]):
if self.get_alpha(row, column) == 255:
if quantization_mode == QuanzationMode.Nearest:
min_norm = 511 # (norm of [255, 255, 255, 255]) + 1
best = None
for color, bitmap in mono_data:
norm = numpy.linalg.norm(self.data[row][column] - color)
if norm < min_norm:
min_norm = norm
best = bitmap
if norm == 0:
break
best[row][column] = 1
else:
for color, bitmap in mono_data:
if numpy.array_equal(color, self.data[row][column]):
bitmap[row][column] = 1
break
return mono_data
def mono(self):
"""!
Returns a bit mask of opaque pixels
"""
mono_data = numpy.zeros(self.data.shape[:2])
for row in range(self.data.shape[0]):
for column in range(self.data.shape[1]):
mono_data[row][column] = int(self.data[row][column][3] == 255)
return mono_data
class Vectorizer:
def __init__(self):
self.palette = None
self.layers = {}
def _create_layer(self, animation, layer_name):
layer = animation.add_layer(objects.ShapeLayer())
if layer_name:
self.layers[layer_name] = layer
layer.name = layer_name
return layer
def prepare_layer(self, animation, layer_name=None):
layer = self._create_layer(animation, layer_name)
layer._max_verts = {}
if self.palette is None:
group = layer.add_shape(objects.Group())
group.name = "bitmap"
layer._max_verts[group.name] = 0
group.add_shape(objects.Path())
group.add_shape(objects.Fill(NVector(0, 0, 0)))
else:
for color in self.palette:
group = layer.add_shape(objects.Group())
group.name = "color_%s" % "".join("%02x" % int(c) for c in color)
layer._max_verts[group.name] = 0
fcol = color/255
fill = group.add_shape(objects.Fill(NVector(*fcol)))
if len(fcol) > 3 and fcol[3] < 1:
fill.opacity.value = fcol[3] * 100
return layer
def raster_to_layer(self, animation, raster, layer_name=None, mode=QuanzationMode.Nearest):
layer = self.prepare_layer(animation, layer_name)
mono_data = raster.quantize(self.palette, mode)
for (color, bitmap), group in zip(mono_data, layer.shapes):
self.raster_to_shapes(group, bitmap)
return layer
def raster_to_shapes(self, group, mono_data):
shapes = []
for bezier in self.raster_to_bezier(mono_data):
shape = group.insert_shape(0, objects.Path())
shapes.append(shape)
shape.shape.value = bezier
return shapes
def raster_to_bezier(self, mono_data):
bmp = potrace.Bitmap(mono_data)
path = bmp.trace()
shapes = []
for curve in path:
bezier = objects.Bezier()
shapes.append(bezier)
bezier.add_point(NVector(*curve.start_point))
for segment in curve:
if segment.is_corner:
bezier.add_point(NVector(*segment.c))
bezier.add_point(NVector(*segment.end_point))
else:
sp = NVector(*bezier.vertices[-1])
ep = NVector(*segment.end_point)
c1 = NVector(*segment.c1) - sp
c2 = NVector(*segment.c2) - ep
bezier.out_tangents[-1] = c1
bezier.add_point(ep, c2)
return shapes
def _frame_keyframe(self, layer, group, time, shapes, beziers):
if shapes:
# TODO handle multiple shapes
nverts = len(beziers[0].vertices)
if nverts > layer._max_verts[group.name]:
layer._max_verts[group.name] = nverts
for shape, bezier in zip(shapes, beziers):
shape.shape.add_keyframe(time, bezier)
def raster_to_frame(self, animation, raster, layer_name, time, mode=QuanzationMode.Nearest):
mono_data = raster.quantize(self.palette, mode)
if layer_name not in self.layers:
layer = self.prepare_layer(animation, layer_name)
for (color, bitmap), group in zip(mono_data, layer.shapes):
shapes = self.raster_to_shapes(group, bitmap)
beziers = [s.shape.value for s in shapes]
self._frame_keyframe(layer, group, time, shapes, beziers)
else:
layer = self.layers[layer_name]
for (color, bitmap), group in zip(mono_data, layer.shapes):
shapes = [s for s in group.shapes if isinstance(s, objects.Path)]
beziers = self.raster_to_bezier(bitmap)
self._frame_keyframe(layer, group, time, shapes, beziers)
def adjust_missing_vertices(self, layer_name):
layer = self.layers[layer_name]
for group in layer.shapes:
# TODO handle multiple shapes
shape = group.shapes[0]
nverts = layer._max_verts[group.name]
if shape.shape.animated:
for kf in shape.shape.keyframes:
bezier = kf.start
count = nverts - len(bezier.vertices)
bezier.vertices += [bezier.vertices[-1]] * count
bezier.in_tangents += [NVector(0, 0)] * count
bezier.out_tangents += [NVector(0, 0)] * count
def duplicate_start_frame(self, layer_name, time):
layer = self.layers[layer_name]
for group in layer.shapes:
shape = group.shapes[0]
bezier = shape.shape.keyframes[0].start
group.shapes[0].shape.add_keyframe(time, bezier)
def color2numpy(vcolor):
l = (vcolor * 255).components
if len(l) == 3:
l.append(255)
return numpy.array(l, numpy.uint8)
def raster_to_animation(filenames, n_colors=1, frame_delay=1,
looping=True, framerate=60, palette=[],
mode=QuanzationMode.Nearest):
vc = Vectorizer()
def callback(animation, raster, frame):
raster = RasterImage.from_pil(raster)
if vc.palette is None:
if palette:
vc.palette = [color2numpy(c) for c in palette]
elif n_colors > 1:
vc.palette = raster.k_means(n_colors)
#vc.raster_to_frame(animation, raster, "anim", frame * frame_delay, mode)
layer = vc.raster_to_layer(animation, raster, "frame_%s" % frame, mode)
layer.in_point = frame * frame_delay
layer.out_point = (frame + 1) * frame_delay
animation = _vectorizing_func(filenames, frame_delay, framerate, callback)
#vc.adjust_missing_vertices("anim")
#if looping and animation._nframes > 1:
#animation.out_point += frame_delay
#vc.duplicate_start_frame("anim", animation.out_point)
#elif animation._nframes == 1:
#for g in animation.find("anim").shapes:
#for shape in g.find_all(objects.Path):
#shape.shape.clear_animation(shape.shape.get_value(0))
#animation.find("anim").out_point = animation.out_point
return animation