File indexing completed on 2025-01-19 03:59:54
0001 # NOTE: requires pillow, pypotrace>=0.2, numpy, scipy to be installed 0002 from PIL import Image 0003 import potrace 0004 import numpy 0005 import enum 0006 from scipy.cluster.vq import kmeans 0007 from .. import objects 0008 from ..nvector import NVector 0009 from .pixel import _vectorizing_func 0010 0011 0012 class QuanzationMode(enum.Enum): 0013 Nearest = 1 0014 Exact = 2 0015 0016 0017 class RasterImage: 0018 def __init__(self, data): 0019 self.data = data 0020 0021 @classmethod 0022 def from_pil(cls, image): 0023 return cls(numpy.array(image)) 0024 0025 #@classmethod 0026 #def open(cls, filename): 0027 #return cls.from_pil(Image.open(filename)) 0028 0029 def k_means(self, n_colors): 0030 """! 0031 Returns a list of centroids 0032 """ 0033 colors = [] 0034 for row in range(self.data.shape[0]): 0035 for column in range(self.data.shape[1]): 0036 if self.get_alpha(row, column) == 255: 0037 colors.append(self.data[row][column]) 0038 0039 colors = numpy.array(colors, numpy.float) 0040 return kmeans(colors, n_colors+1)[0] 0041 0042 def get_alpha(self, row, column): 0043 if self.data.shape[2] >= 4: 0044 return self.data[row][column][3] 0045 return 255 0046 0047 def quantize(self, codebook, quantization_mode=QuanzationMode.Nearest): 0048 """! 0049 Returns a list of tuple [color, data] where for each color in codebook 0050 data is a bit mask for the image 0051 0052 You can get codebook from k_means 0053 """ 0054 if codebook is None or len(codebook) == 0: 0055 return [(numpy.array([0., 0., 0., 255.]), self.mono())] 0056 0057 mono_data = [] 0058 for c in codebook: 0059 mono_data.append((c, numpy.zeros(self.data.shape[:2]))) 0060 0061 for row in range(self.data.shape[0]): 0062 for column in range(self.data.shape[1]): 0063 if self.get_alpha(row, column) == 255: 0064 if quantization_mode == QuanzationMode.Nearest: 0065 min_norm = 511 # (norm of [255, 255, 255, 255]) + 1 0066 best = None 0067 for color, bitmap in mono_data: 0068 norm = numpy.linalg.norm(self.data[row][column] - color) 0069 if norm < min_norm: 0070 min_norm = norm 0071 best = bitmap 0072 if norm == 0: 0073 break 0074 best[row][column] = 1 0075 else: 0076 for color, bitmap in mono_data: 0077 if numpy.array_equal(color, self.data[row][column]): 0078 bitmap[row][column] = 1 0079 break 0080 0081 return mono_data 0082 0083 def mono(self): 0084 """! 0085 Returns a bit mask of opaque pixels 0086 """ 0087 mono_data = numpy.zeros(self.data.shape[:2]) 0088 for row in range(self.data.shape[0]): 0089 for column in range(self.data.shape[1]): 0090 mono_data[row][column] = int(self.data[row][column][3] == 255) 0091 return mono_data 0092 0093 0094 class Vectorizer: 0095 def __init__(self): 0096 self.palette = None 0097 self.layers = {} 0098 0099 def _create_layer(self, animation, layer_name): 0100 layer = animation.add_layer(objects.ShapeLayer()) 0101 if layer_name: 0102 self.layers[layer_name] = layer 0103 layer.name = layer_name 0104 return layer 0105 0106 def prepare_layer(self, animation, layer_name=None): 0107 layer = self._create_layer(animation, layer_name) 0108 layer._max_verts = {} 0109 if self.palette is None: 0110 group = layer.add_shape(objects.Group()) 0111 group.name = "bitmap" 0112 layer._max_verts[group.name] = 0 0113 group.add_shape(objects.Path()) 0114 group.add_shape(objects.Fill(NVector(0, 0, 0))) 0115 else: 0116 for color in self.palette: 0117 group = layer.add_shape(objects.Group()) 0118 group.name = "color_%s" % "".join("%02x" % int(c) for c in color) 0119 layer._max_verts[group.name] = 0 0120 fcol = color/255 0121 fill = group.add_shape(objects.Fill(NVector(*fcol))) 0122 if len(fcol) > 3 and fcol[3] < 1: 0123 fill.opacity.value = fcol[3] * 100 0124 return layer 0125 0126 def raster_to_layer(self, animation, raster, layer_name=None, mode=QuanzationMode.Nearest): 0127 layer = self.prepare_layer(animation, layer_name) 0128 mono_data = raster.quantize(self.palette, mode) 0129 for (color, bitmap), group in zip(mono_data, layer.shapes): 0130 self.raster_to_shapes(group, bitmap) 0131 return layer 0132 0133 def raster_to_shapes(self, group, mono_data): 0134 shapes = [] 0135 for bezier in self.raster_to_bezier(mono_data): 0136 shape = group.insert_shape(0, objects.Path()) 0137 shapes.append(shape) 0138 shape.shape.value = bezier 0139 return shapes 0140 0141 def raster_to_bezier(self, mono_data): 0142 bmp = potrace.Bitmap(mono_data) 0143 path = bmp.trace() 0144 shapes = [] 0145 for curve in path: 0146 bezier = objects.Bezier() 0147 shapes.append(bezier) 0148 bezier.add_point(NVector(*curve.start_point)) 0149 for segment in curve: 0150 if segment.is_corner: 0151 bezier.add_point(NVector(*segment.c)) 0152 bezier.add_point(NVector(*segment.end_point)) 0153 else: 0154 sp = NVector(*bezier.vertices[-1]) 0155 ep = NVector(*segment.end_point) 0156 c1 = NVector(*segment.c1) - sp 0157 c2 = NVector(*segment.c2) - ep 0158 bezier.out_tangents[-1] = c1 0159 bezier.add_point(ep, c2) 0160 return shapes 0161 0162 def _frame_keyframe(self, layer, group, time, shapes, beziers): 0163 if shapes: 0164 # TODO handle multiple shapes 0165 nverts = len(beziers[0].vertices) 0166 if nverts > layer._max_verts[group.name]: 0167 layer._max_verts[group.name] = nverts 0168 for shape, bezier in zip(shapes, beziers): 0169 shape.shape.add_keyframe(time, bezier) 0170 0171 def raster_to_frame(self, animation, raster, layer_name, time, mode=QuanzationMode.Nearest): 0172 mono_data = raster.quantize(self.palette, mode) 0173 0174 if layer_name not in self.layers: 0175 layer = self.prepare_layer(animation, layer_name) 0176 for (color, bitmap), group in zip(mono_data, layer.shapes): 0177 shapes = self.raster_to_shapes(group, bitmap) 0178 beziers = [s.shape.value for s in shapes] 0179 self._frame_keyframe(layer, group, time, shapes, beziers) 0180 else: 0181 layer = self.layers[layer_name] 0182 0183 for (color, bitmap), group in zip(mono_data, layer.shapes): 0184 shapes = [s for s in group.shapes if isinstance(s, objects.Path)] 0185 beziers = self.raster_to_bezier(bitmap) 0186 self._frame_keyframe(layer, group, time, shapes, beziers) 0187 0188 def adjust_missing_vertices(self, layer_name): 0189 layer = self.layers[layer_name] 0190 for group in layer.shapes: 0191 # TODO handle multiple shapes 0192 shape = group.shapes[0] 0193 nverts = layer._max_verts[group.name] 0194 if shape.shape.animated: 0195 for kf in shape.shape.keyframes: 0196 bezier = kf.start 0197 count = nverts - len(bezier.vertices) 0198 bezier.vertices += [bezier.vertices[-1]] * count 0199 bezier.in_tangents += [NVector(0, 0)] * count 0200 bezier.out_tangents += [NVector(0, 0)] * count 0201 0202 def duplicate_start_frame(self, layer_name, time): 0203 layer = self.layers[layer_name] 0204 for group in layer.shapes: 0205 shape = group.shapes[0] 0206 bezier = shape.shape.keyframes[0].start 0207 group.shapes[0].shape.add_keyframe(time, bezier) 0208 0209 0210 def color2numpy(vcolor): 0211 l = (vcolor * 255).components 0212 if len(l) == 3: 0213 l.append(255) 0214 return numpy.array(l, numpy.uint8) 0215 0216 0217 def raster_to_animation(filenames, n_colors=1, frame_delay=1, 0218 looping=True, framerate=60, palette=[], 0219 mode=QuanzationMode.Nearest): 0220 0221 vc = Vectorizer() 0222 0223 def callback(animation, raster, frame): 0224 raster = RasterImage.from_pil(raster) 0225 if vc.palette is None: 0226 if palette: 0227 vc.palette = [color2numpy(c) for c in palette] 0228 elif n_colors > 1: 0229 vc.palette = raster.k_means(n_colors) 0230 #vc.raster_to_frame(animation, raster, "anim", frame * frame_delay, mode) 0231 layer = vc.raster_to_layer(animation, raster, "frame_%s" % frame, mode) 0232 layer.in_point = frame * frame_delay 0233 layer.out_point = (frame + 1) * frame_delay 0234 0235 animation = _vectorizing_func(filenames, frame_delay, framerate, callback) 0236 0237 #vc.adjust_missing_vertices("anim") 0238 #if looping and animation._nframes > 1: 0239 #animation.out_point += frame_delay 0240 #vc.duplicate_start_frame("anim", animation.out_point) 0241 #elif animation._nframes == 1: 0242 #for g in animation.find("anim").shapes: 0243 #for shape in g.find_all(objects.Path): 0244 #shape.shape.clear_animation(shape.shape.get_value(0)) 0245 0246 #animation.find("anim").out_point = animation.out_point 0247 0248 return animation