|
| 1 | +#!/usr/bin/python |
| 2 | +# Copyright (c) 2012, Sublime HQ Pty Ltd |
| 3 | +# All rights reserved. |
| 4 | + |
| 5 | +# Redistribution and use in source and binary forms, with or without |
| 6 | +# modification, are permitted provided that the following conditions are met: |
| 7 | +# * Redistributions of source code must retain the above copyright |
| 8 | +# notice, this list of conditions and the following disclaimer. |
| 9 | +# * Redistributions in binary form must reproduce the above copyright |
| 10 | +# notice, this list of conditions and the following disclaimer in the |
| 11 | +# documentation and/or other materials provided with the distribution. |
| 12 | +# * Neither the name of the <organization> nor the |
| 13 | +# names of its contributors may be used to endorse or promote products |
| 14 | +# derived from this software without specific prior written permission. |
| 15 | + |
| 16 | +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
| 17 | +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 18 | +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 19 | +# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY |
| 20 | +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| 21 | +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| 22 | +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| 23 | +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 24 | +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| 25 | +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 26 | + |
| 27 | +import scipy.ndimage.measurements as me |
| 28 | +import json |
| 29 | +import scipy.misc as misc |
| 30 | +import re |
| 31 | +import sys |
| 32 | +import os |
| 33 | +import cv2 |
| 34 | +from numpy import * |
| 35 | +from time import time |
| 36 | + |
| 37 | +# How long to wait before the animation restarts |
| 38 | +END_FRAME_PAUSE = 4000 |
| 39 | + |
| 40 | +# How many pixels can be wasted in the name of combining neighbouring changed |
| 41 | +# regions. |
| 42 | +SIMPLIFICATION_TOLERANCE = 512 |
| 43 | + |
| 44 | +MAX_PACKED_HEIGHT = 10000 |
| 45 | + |
| 46 | +def slice_size(a, b): |
| 47 | + return (a.stop - a.start) * (b.stop - b.start) |
| 48 | + |
| 49 | +def combine_slices(a, b, c, d): |
| 50 | + return (slice(min(a.start, c.start), max(a.stop, c.stop)), |
| 51 | + slice(min(b.start, d.start), max(b.stop, d.stop))) |
| 52 | + |
| 53 | +def slices_intersect(a, b, c, d): |
| 54 | + if (a.start >= c.stop): return False |
| 55 | + if (c.start >= a.stop): return False |
| 56 | + if (b.start >= d.stop): return False |
| 57 | + if (d.start >= b.stop): return False |
| 58 | + return True |
| 59 | + |
| 60 | +# Combine a large set of rectangles into a smaller set of rectangles, |
| 61 | +# minimising the number of additional pixels included in the smaller set of |
| 62 | +# rectangles |
| 63 | +def simplify(boxes, tol = 0): |
| 64 | + out = [] |
| 65 | + for a,b in boxes: |
| 66 | + sz1 = slice_size(a, b) |
| 67 | + did_combine = False |
| 68 | + for i in xrange(len(out)): |
| 69 | + c,d = out[i] |
| 70 | + cu, cv = combine_slices(a, b, c, d) |
| 71 | + sz2 = slice_size(c, d) |
| 72 | + if slices_intersect(a, b, c, d) or (slice_size(cu, cv) <= sz1 + sz2 + tol): |
| 73 | + out[i] = (cu, cv) |
| 74 | + did_combine = True |
| 75 | + break |
| 76 | + if not did_combine: |
| 77 | + out.append((a,b)) |
| 78 | + |
| 79 | + if tol != 0: |
| 80 | + return simplify(out, 0) |
| 81 | + else: |
| 82 | + return out |
| 83 | + |
| 84 | +def slice_tuple_size(s): |
| 85 | + a, b = s |
| 86 | + return (a.stop - a.start) * (b.stop - b.start) |
| 87 | + |
| 88 | +# Allocates space in the packed image. This does it in a slow, brute force |
| 89 | +# manner. |
| 90 | +class Allocator2D: |
| 91 | + def __init__(self, rows, cols): |
| 92 | + self.bitmap = zeros((rows, cols), dtype=uint8) |
| 93 | + self.available_space = zeros(rows, dtype=uint32) |
| 94 | + self.available_space[:] = cols |
| 95 | + self.num_used_rows = 0 |
| 96 | + |
| 97 | + def allocate(self, w, h): |
| 98 | + bh, bw = shape(self.bitmap) |
| 99 | + |
| 100 | + for row in xrange(bh - h + 1): |
| 101 | + if self.available_space[row] < w: |
| 102 | + continue |
| 103 | + |
| 104 | + for col in xrange(bw - w + 1): |
| 105 | + if self.bitmap[row, col] == 0: |
| 106 | + if not self.bitmap[row:row+h,col:col+w].any(): |
| 107 | + self.bitmap[row:row+h,col:col+w] = 1 |
| 108 | + self.available_space[row:row+h] -= w |
| 109 | + self.num_used_rows = max(self.num_used_rows, row + h) |
| 110 | + return row, col |
| 111 | + raise RuntimeError() |
| 112 | + |
| 113 | +def find_matching_rect(bitmap, num_used_rows, packed, src, sx, sy, w, h): |
| 114 | + template = src[sy:sy+h, sx:sx+w] |
| 115 | + bh, bw = shape(bitmap) |
| 116 | + image = packed[0:num_used_rows, 0:bw] |
| 117 | + |
| 118 | + if num_used_rows < h: |
| 119 | + return None |
| 120 | + |
| 121 | + result = cv2.matchTemplate(image,template,cv2.TM_CCOEFF_NORMED) |
| 122 | + |
| 123 | + row,col = unravel_index(result.argmax(),result.shape) |
| 124 | + if ((packed[row:row+h,col:col+w] == src[sy:sy+h,sx:sx+w]).all() |
| 125 | + and (packed[row:row+1,col:col+w,0] == src[sy:sy+1,sx:sx+w,0]).all()): |
| 126 | + return row,col |
| 127 | + else: |
| 128 | + return None |
| 129 | + |
| 130 | +def generate_animation(anim_name): |
| 131 | + frames = [] |
| 132 | + rex = re.compile("screen_([0-9]+).png") |
| 133 | + for f in os.listdir(anim_name): |
| 134 | + m = re.search(rex, f) |
| 135 | + if m: |
| 136 | + frames.append((int(m.group(1)), anim_name + "/" + f)) |
| 137 | + frames.sort() |
| 138 | + |
| 139 | + images = [misc.imread(f) for t, f in frames] |
| 140 | + |
| 141 | + zero = images[0] - images[0] |
| 142 | + pairs = zip([zero] + images[:-1], images) |
| 143 | + diffs = [sign((b - a).max(2)) for a, b in pairs] |
| 144 | + |
| 145 | + # Find different objects for each frame |
| 146 | + img_areas = [me.find_objects(me.label(d)[0]) for d in diffs] |
| 147 | + |
| 148 | + # Simplify areas |
| 149 | + img_areas = [simplify(x, SIMPLIFICATION_TOLERANCE) for x in img_areas] |
| 150 | + |
| 151 | + ih, iw, _ = shape(images[0]) |
| 152 | + |
| 153 | + # Generate a packed image |
| 154 | + allocator = Allocator2D(MAX_PACKED_HEIGHT, iw) |
| 155 | + packed = zeros((MAX_PACKED_HEIGHT, iw, 3), dtype=uint8) |
| 156 | + |
| 157 | + # Sort the rects to be packed by largest size first, to improve the packing |
| 158 | + rects_by_size = [] |
| 159 | + for i in xrange(len(images)): |
| 160 | + src_rects = img_areas[i] |
| 161 | + |
| 162 | + for j in xrange(len(src_rects)): |
| 163 | + rects_by_size.append((slice_tuple_size(src_rects[j]), i, j)) |
| 164 | + |
| 165 | + rects_by_size.sort(reverse = True) |
| 166 | + |
| 167 | + allocs = [[None] * len(src_rects) for src_rects in img_areas] |
| 168 | + |
| 169 | + print anim_name,"packing, num rects:",len(rects_by_size),"num frames:",len(images) |
| 170 | + |
| 171 | + t0 = time() |
| 172 | + |
| 173 | + for size,i,j in rects_by_size: |
| 174 | + src = images[i] |
| 175 | + src_rects = img_areas[i] |
| 176 | + |
| 177 | + a, b = src_rects[j] |
| 178 | + sx, sy = b.start, a.start |
| 179 | + w, h = b.stop - b.start, a.stop - a.start |
| 180 | + |
| 181 | + # See if the image data already exists in the packed image. This takes |
| 182 | + # a long time, but results in worthwhile space savings (20% in one |
| 183 | + # test) |
| 184 | + existing = find_matching_rect(allocator.bitmap, allocator.num_used_rows, packed, src, sx, sy, w, h) |
| 185 | + if existing: |
| 186 | + dy, dx = existing |
| 187 | + allocs[i][j] = (dy, dx) |
| 188 | + else: |
| 189 | + dy, dx = allocator.allocate(w, h) |
| 190 | + allocs[i][j] = (dy, dx) |
| 191 | + |
| 192 | + packed[dy:dy+h, dx:dx+w] = src[sy:sy+h, sx:sx+w] |
| 193 | + |
| 194 | + print anim_name,"packing finished, took:",time() - t0 |
| 195 | + |
| 196 | + packed = packed[0:allocator.num_used_rows] |
| 197 | + |
| 198 | + misc.imsave(anim_name + "_packed_tmp.png", packed) |
| 199 | + os.system("pngcrush -q " + anim_name + "_packed_tmp.png " + anim_name + "_packed.png") |
| 200 | + os.system("rm " + anim_name + "_packed_tmp.png") |
| 201 | + |
| 202 | + # Generate JSON to represent the data |
| 203 | + times = [t for t, f in frames] |
| 204 | + delays = (array(times[1:] + [times[-1] + END_FRAME_PAUSE]) - array(times)).tolist() |
| 205 | + |
| 206 | + timeline = [] |
| 207 | + for i in xrange(len(images)): |
| 208 | + src_rects = img_areas[i] |
| 209 | + dst_rects = allocs[i] |
| 210 | + |
| 211 | + blitlist = [] |
| 212 | + |
| 213 | + for j in xrange(len(src_rects)): |
| 214 | + a, b = src_rects[j] |
| 215 | + sx, sy = b.start, a.start |
| 216 | + w, h = b.stop - b.start, a.stop - a.start |
| 217 | + dy, dx = dst_rects[j] |
| 218 | + |
| 219 | + blitlist.append([dx, dy, w, h, sx, sy]) |
| 220 | + |
| 221 | + timeline.append({'delay': delays[i], 'blit': blitlist}) |
| 222 | + |
| 223 | + f = open(anim_name + '_anim.js', 'wb') |
| 224 | + f.write(anim_name + "_timeline = ") |
| 225 | + json.dump(timeline, f) |
| 226 | + f.close() |
| 227 | + |
| 228 | + |
| 229 | +if __name__ == '__main__': |
| 230 | + generate_animation(sys.argv[1]) |
0 commit comments