xpra icon
Bug tracker and wiki

Ticket #1232: detect-scrolling-v16.patch

File detect-scrolling-v16.patch, 24.1 KB (added by Antoine Martin, 3 years ago)

updated patch

  • tests/unit/server/motion_test.py

     
     1#!/usr/bin/env python
     2# This file is part of Xpra.
     3# Copyright (C) 2011-2014 Antoine Martin <antoine@devloop.org.uk>
     4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
     5# later version. See the file COPYING for details.
     6
     7import unittest
     8import time
     9from zlib import crc32
     10
     11try:
     12        from xpra.server.window import motion
     13except ImportError:
     14        motion = None
     15
     16
     17class TestMotion(unittest.TestCase):
     18
     19        def test_match_distance(self):
     20                def t(a1, a2, distance, matches):
     21                        lines = motion.match_distance(a1, a2, distance)
     22                        assert len(lines)==matches, "expected %i matches for distance=%i but got %i for a1=%s, a2=%s, result=%s" % (matches, distance, len(lines), a1, a2, lines)
     23                for N in (1, 10, 100):
     24                        a = range(N)
     25                        t(a, a, 0, N)           #identity: all match
     26
     27                        a = [0]*N
     28                        t(a, a, 0, N)
     29                        for M in range(N):
     30                                t(a, a, M, N-M)
     31
     32                #from a2 to a1: shift by -2, get 2 hits
     33                t([0, 1, 2, 3], [2, 3, 4, 5], -2, 2)
     34                N = 100
     35                a1 = range(N)
     36                for M in (1, 2, 10, 100):
     37                        a2 = range(M, M+N)
     38                        t(a1, a2, -M, N-abs(M))
     39                        a2 = range(-M, N-M)
     40                        t(a1, a2, M, N-abs(M))
     41                for M in (-10, -20):
     42                        a2 = range(M, N+M)
     43                        t(a1, a2, -M, N-abs(M))
     44
     45        def test_consecutive_lines(self):
     46                def f(v):
     47                        try:
     48                                motion.consecutive_lines(v)
     49                        except:
     50                                pass
     51                        else:
     52                                raise Exception("consecutive_lines should have failed for value %s" % v)
     53                f(None)
     54                f("")
     55                f([])
     56                f(1)
     57                def t(v, e):
     58                        r = motion.consecutive_lines(v)
     59                        assert r==e, "expected %s but got %s for input=%s" % (e, r, v)
     60                t([5, 10], [(5, 1), (10, 1)])
     61                t([5], [(5, 1)])
     62                t([1,2,3], [(1,3)])
     63                t(range(100), [(0, 100),])
     64                t([1,2,3,100,200,201], [(1,3), (100, 1), (200, 2)])
     65
     66        def test_calculate_distances(self):
     67                array1 = [crc32(str(x)) for x in (1234, "abc", 99999)]
     68                array2 = array1
     69                d = motion.calculate_distances(array1, array2, 1)
     70                assert len(d)==1 and d[0]==3
     71
     72                array1 = range(0, 3)
     73                array2 = range(1, 4)
     74                d = motion.calculate_distances(array1, array2, 1)
     75                assert len(d)==1, "expected 1 match but got: %s" % d
     76                assert d.get(1)==2, "expected distance of 1 with 2 hits but got: %s" % d
     77                def cdf(v1, v2):
     78                        try:
     79                                motion.calculate_distances(v1, v2, 1)
     80                        except:
     81                                return
     82                        raise Exception("calculate_distances should have failed for values: %s" % (v1, v2))
     83                cdf(None, None)
     84                cdf([], None)
     85                cdf(None, [])
     86                cdf([1, 2], [1])
     87                assert len(motion.calculate_distances([], [], 1))==0
     88
     89                #performance:
     90                N = 4096
     91                start = time.time()
     92                array1 = range(N)
     93                array2 = [N*2-x*2 for x in range(N)]
     94                d = motion.calculate_distances(array1, array2, 1)
     95                end = time.time()
     96                print("distances for %ix%i took %.1fms" % (N, N, (end-start)*1000))
     97
     98
     99def main():
     100        if motion:
     101                unittest.main()
     102
     103
     104if __name__ == '__main__':
     105        main()
  • xpra/log.py

    Property changes on: tests/unit/server/motion_test.py
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
    175175                ("encoding"     , "Server side encoding selection and compression"),
    176176                ("scaling"      , "Picture scaling"),
    177177                ("delta"        , "Delta pre-compression"),
     178                ("scroll"       , "Scrolling detection and compression"),
    178179                ("xor"          , "XOR delta pre-compression"),
    179180                ("subregion"    , "Video subregion processing"),
    180181                ("regiondetect" , "Video region detection"),
  • xpra/server/window/window_source.py

     
    13911391        self.pixel_format = image.get_pixel_format()
    13921392
    13931393        now = time.time()
    1394         log("process_damage_regions: wid=%i, adding pixel data to encode queue (%ix%i - %s), elapsed time: %.1f ms, request time: %.1f ms",
    1395                 self.wid, w, h, coding, 1000*(now-damage_time), 1000*(now-rgb_request_time))
    13961394        item = (w, h, damage_time, now, image, coding, sequence, options, flush)
    1397         av_sync = options.get("av-sync", False)
    1398         if not av_sync:
     1395        if self.must_freeze(coding, options):
     1396            newstride = image.get_width()*4
     1397            if not image.restride(newstride):
     1398                avsynclog("Warning: failed to freeze image pixels for:")
     1399                avsynclog(" %s", image)
     1400        av_delay = self.get_frame_encode_delay(options)
     1401        log("process_damage_regions: wid=%i, adding pixel data to encode queue (%ix%i - %s), elapsed time: %.1f ms, request time: %.1f ms, frame delay=%ims",
     1402                self.wid, w, h, coding, 1000*(now-damage_time), 1000*(now-rgb_request_time), av_delay)
     1403        if av_delay<0:
    13991404            self.call_in_encode_thread(True, self.make_data_packet_cb, *item)
    14001405        else:
    1401             #schedule encode via queue, after freezing the pixels:
    1402             if not image.freeze():
    1403                 avsynclog("Warning: failed to freeze image pixels for:")
    1404                 avsynclog(" %s", image)
    1405                 self.call_in_encode_thread(True, self.make_data_packet_cb, *item)
    1406                 return
    14071406            self.encode_queue.append(item)
    1408             l = len(self.encode_queue)
    1409             if l>=self.encode_queue_max_size:
    1410                 av_delay = 0        #we must free some space!
    1411             else:
    1412                 av_delay = self.av_sync_delay*int(av_sync)
    1413             avsynclog("scheduling encode queue iteration in %ims, encode queue size=%i (max=%i)", av_delay, l, self.encode_queue_max_size)
    14141407            self.timeout_add(av_delay, self.call_in_encode_thread, True, self.encode_from_queue)
    14151408
     1409    def must_freeze(self, coding, options):
     1410        return options.get("av-sync", False)
     1411
     1412    def get_frame_encode_delay(self, options):
     1413        if options.get("av-sync", False):
     1414            return -1
     1415        l = len(self.encode_queue)
     1416        if l>=self.encode_queue_max_size:
     1417            #we must free some space!
     1418            return 0
     1419        return self.av_sync_delay
     1420
    14161421    def encode_from_queue(self):
    14171422        #note: we use a queue here to ensure we preserve the order
    14181423        #(so we encode frames in the same order they were grabbed)
     
    18821887        if self.supports_flush and flush is not None:
    18831888            client_options["flush"] = flush
    18841889        end = time.time()
    1885         compresslog.info("compress: %5.1fms for %4ix%-4i pixels for wid=%-5i using %5s with ratio %5.1f%% (%5iKB to %5iKB), client_options=%s",
     1890        compresslog("compress: %5.1fms for %4ix%-4i pixels for wid=%-5i using %5s with ratio %5.1f%% (%5iKB to %5iKB), client_options=%s",
    18861891                 (end-start)*1000.0, outw, outh, self.wid, coding, 100.0*csize/psize, psize/1024, csize/1024, client_options)
    18871892        self.statistics.encoding_stats.append((end, coding, w*h, bpp, len(data), end-start))
    18881893        return self.make_draw_packet(x, y, outw, outh, coding, data, outstride, client_options)
  • xpra/server/window/window_video_source.py

     
    88import time
    99import operator
    1010
    11 from xpra.net.compression import Compressed
     11from xpra.net.compression import Compressed, LargeStructure
    1212from xpra.codecs.codec_constants import get_subsampling_divs, \
    1313                                        TransientCodecException, RGB_FORMATS, PIXEL_SUBSAMPLING, LOSSY_PIXEL_FORMATS
    1414from xpra.server.window.window_source import WindowSource, STRICT_MODE, AUTO_REFRESH_SPEED, AUTO_REFRESH_QUALITY
     
    1515from xpra.server.window.region import merge_all            #@UnresolvedImport
    1616from xpra.server.window.video_subregion import VideoSubregion
    1717from xpra.codecs.loader import PREFERED_ENCODING_ORDER, EDGE_ENCODING_ORDER
    18 from xpra.util import parse_scaling_value, engs
     18from xpra.util import parse_scaling_value, engs, repr_ellipsized
    1919from xpra.log import Logger
    2020
    2121log = Logger("encoding")
     
    2525sublog = Logger("subregion")
    2626videolog = Logger("video")
    2727avsynclog = Logger("av-sync")
     28scrolllog = Logger("scroll")
    2829
    2930
    3031def envint(name, d):
     
    4647VIDEO_SUBREGION = os.environ.get("XPRA_VIDEO_SUBREGION", "1")=="1"
    4748B_FRAMES = os.environ.get("XPRA_B_FRAMES", "1")=="1"
    4849VIDEO_SKIP_EDGE = os.environ.get("XPRA_VIDEO_SKIP_EDGE", "0")=="1"
     50SCROLL_ENCODING = os.environ.get("XPRA_SCROLL_ENCODING", "0")=="1"
    4951
    5052
    5153class WindowVideoSource(WindowSource):
     
    5658    def __init__(self, *args):
    5759        #this will call init_vars():
    5860        WindowSource.__init__(self, *args)
     61        self.scroll_encoding = SCROLL_ENCODING
     62        self.supports_scrolling = self.scroll_encoding and self.encoding_options.boolget("scrolling")
    5963        self.supports_video_scaling = self.encoding_options.boolget("video_scaling", False)
    6064        self.supports_video_b_frames = self.encoding_options.strlistget("video_b_frames", [])
    6165        self.supports_video_subregion = VIDEO_SUBREGION
     
    109113        self.non_video_encodings = []
    110114        self.edge_encoding = None
    111115        self.b_frame_flush_timer = None
     116        self.scroll_data = None
    112117
    113118    def set_auto_refresh_delay(self, d):
    114119        WindowSource.set_auto_refresh_delay(self, d)
     
    177182                                                 "non-video"    : self.non_video_encodings,
    178183                                                 "edge"         : self.edge_encoding or "",
    179184                                                 })
    180         einfo = {"pipeline_param" : self.get_pipeline_info()}
     185        einfo = {
     186                 "pipeline_param" : self.get_pipeline_info(),
     187                 "scrolling"      : self.supports_scrolling,
     188                 }
    181189        if self._last_pipeline_check>0:
    182190            einfo["pipeline_last_check"] = int(1000*(time.time()-self._last_pipeline_check))
    183191        lps = self.last_pipeline_scores
     
    288296    def do_set_client_properties(self, properties):
    289297        #client may restrict csc modes for specific windows
    290298        self.parse_csc_modes(properties.dictget("encoding.full_csc_modes", default_value=None))
     299        self.supports_scrolling = self.scroll_encoding and properties.boolget("encoding.scrolling", self.supports_scrolling)
    291300        self.supports_video_scaling = properties.boolget("encoding.video_scaling", self.supports_video_scaling)
    292301        self.supports_video_subregion = properties.boolget("encoding.video_subregion", self.supports_video_subregion)
    293302        self.scaling_control = max(0, min(100, properties.intget("scaling.control", self.scaling_control)))
     
    441450
    442451    def cancel_damage(self):
    443452        self.video_subregion.cancel_refresh_timer()
     453        self.scroll_data = None
    444454        WindowSource.cancel_damage(self)
    445455        #we must clean the video encoder to ensure
    446456        #we will resend a key frame because we may be missing a frame
     
    455465        else:
    456466            #keep the region, but cancel the refresh:
    457467            self.video_subregion.cancel_refresh_timer()
     468        self.scroll_data = None
    458469        #refresh the whole window in one go:
    459470        damage_options["novideo"] = True
    460471        WindowSource.full_quality_refresh(self, damage_options)
     
    659670                WindowSource.process_damage_region(self, damage_time, x+w-dw, y, dw, h, self.edge_encoding, options, flush=1+int(dh>0))
    660671            if dh>0:
    661672                WindowSource.process_damage_region(self, damage_time, x, y+h-dh, x+w, dh, self.edge_encoding, options, flush=1)
    662         WindowSource.process_damage_region(self, damage_time, x, y, w-dw, h-dh, coding, options, flush=flush)
     673        #use the unmasked dimensions to prevent us restriding for nothing:
     674        WindowSource.process_damage_region(self, damage_time, x, y, w, h, coding, options, flush=flush)
    663675
    664676
     677    def must_freeze(self, coding, options):
     678        return WindowSource.must_freeze(self, coding, options) or coding in self.video_encodings
     679
    665680    def must_encode_full_frame(self, encoding):
    666681        return self.full_frames_only or (encoding in self.video_encodings) or not self.non_video_encodings
    667682
     
    12951310        return  True
    12961311
    12971312
     1313    def encode_scrolling(self, last_image, image, distances, old_csums, csums, options):
     1314        def reb(v):
     1315            return repr_ellipsized(str(v))
     1316        tstart = time.time()
     1317        SAVE = False
     1318        from xpra.server.window.motion import match_distance, consecutive_lines           #@UnresolvedImport
     1319        scrolllog("encode_scrolling(%s, %s, %s, [], [], %s)", last_image, image, distances, options)
     1320        if SAVE:
     1321            now = time.time()
     1322            dirname = "scroll-%i" % int(1000*now)
     1323            os.mkdir(dirname)
     1324            def save_data(filename, data):
     1325                path = os.path.join(dirname, filename)
     1326                with open(path, "wb") as f:
     1327                    f.write(data)
     1328            def save_pic(basename, img):
     1329                coding, data = self.video_fallback(img, options)[:2]
     1330                filename = "%s.%s" % (basename, coding)
     1331                save_data(filename, data.data)
     1332                return filename
     1333            save_pic("old", last_image)
     1334            save_pic("new", image)
     1335
     1336        x, y, w, h = image.get_geometry()[:4]
     1337        yscroll_values = []
     1338        max_scroll_regions = 50
     1339        #process distances with the highest score first:
     1340        for hits in reversed(sorted(distances.values())):
     1341            for scroll in (d for d,v in distances.items() if v==hits):
     1342                assert scroll<h
     1343                yscroll_values.append(scroll)
     1344            if len(yscroll_values)>=max_scroll_regions:
     1345                break
     1346        assert yscroll_values
     1347        #always add zero=no-change so we can drop those updates!
     1348        if 0 not in yscroll_values and 0 in distances:
     1349            #(but do this last so we don't end up cutting too many rectangles)
     1350            yscroll_values.append(0)
     1351        scrolllog(" will send scroll packets for yscroll=%s", repr_ellipsized(str(dict((d,distances[d]) for d in yscroll_values))))
     1352        #keep track of the lines we have handled already:
     1353        #(the same line may be available from multiple scroll directions)
     1354        handled = set()
     1355        scrolls = []
     1356        max_scrolls = 1000
     1357        for s in yscroll_values:
     1358            #find all the lines that scroll by this much:
     1359            slines = match_distance(old_csums, csums, s)
     1360            assert slines, "no lines matching distance %i" % s
     1361            #log.warn("match_distance(%s, %s, %s)=%s", reb(old_csums), reb(csums), s, reb(slines))
     1362            #remove any lines we have already handled:
     1363            lines = [v+s for v in slines if ((v+s) not in handled and v not in handled)]
     1364            if not lines:
     1365                continue
     1366            #and them to the handled set so we don't try to paint them again:
     1367            handled = handled.union(set(lines))
     1368            if s==0:
     1369                scrolllog(" %i lines have not changed: %s", len(lines), reb(lines))
     1370            else:
     1371                #things have actually moved
     1372                #aggregate consecutive lines into rectangles:
     1373                cl = consecutive_lines(lines)
     1374                scrolllog(" scroll groups for distance=%i : %s=%s", s, reb(str(lines)), cl)
     1375                for start,count in cl:
     1376                    #new rectangle
     1377                    scrolls.append((x, y+start-s, w, count, 0, s))
     1378                    if len(scrolls)>max_scrolls:
     1379                        break
     1380                if len(scrolls)>max_scrolls:
     1381                    break
     1382        non_scroll = []
     1383        remaining = set(range(h))-handled
     1384        if remaining:
     1385            damaged_lines = sorted(list(remaining))
     1386            non_scroll = consecutive_lines(damaged_lines)
     1387            scrolllog(" non scroll: %i packets: %s=%s", len(non_scroll), reb(damaged_lines), non_scroll)
     1388        nscount = len(non_scroll)
     1389        flush = nscount
     1390        #send as scroll paints packets:
     1391        if scrolls:
     1392            client_options = options.copy()
     1393            if flush>0:
     1394                client_options["flush"] = flush
     1395            packet = self.make_draw_packet(x, y, w, h, "scroll", LargeStructure("scroll data", scrolls), 0, client_options)
     1396            if SAVE:
     1397                save_data("scrolls.txt", "\n".join(repr(v) for v in scrolls))
     1398            self.queue_damage_packet(packet)
     1399        #send the rest as rectangles:
     1400        if non_scroll:
     1401            replay = []
     1402            for start, count in non_scroll:
     1403                sub = image.get_sub_image(0, start, w, count)
     1404                #log.info("%s.get_sub_image%s=%s", image, (0, start, w, count), sub)
     1405                flush -= 1
     1406                ret = self.video_fallback(sub, options)
     1407                if not ret:
     1408                    #cancelled?
     1409                    return None
     1410                coding, data, client_options, outw, outh, outstride, _ = ret
     1411                assert data
     1412                client_options["flush"] = flush
     1413                packet = self.make_draw_packet(sub.get_x(), sub.get_y(), outw, outh, coding, data, outstride, client_options)
     1414                self.queue_damage_packet(packet)
     1415                if SAVE:
     1416                    filename = save_pic("%s" % (nscount-flush), sub)
     1417                    replay.append("%s %s" % (filename, (sub.get_x(), sub.get_y(), outw, outh)))
     1418            if SAVE:
     1419                save_data("replay.txt", "\n".join(replay))
     1420        assert flush==0
     1421        tend = time.time()
     1422        scrolllog("scroll encoding took %ims", (tend-tstart)*1000)
     1423        return None
     1424
    12981425    def video_fallback(self, image, options):
    12991426        #find one that is not video:
    13001427        fallback_encodings = [x for x in PREFERED_ENCODING_ORDER if (x in self.non_video_encodings and x in self._encoders and x!="mmap")]
     
    13541481        if not ve:
    13551482            return video_fallback()
    13561483
     1484        #check for scrolling:
     1485        if self.supports_scrolling:
     1486            try:
     1487                from xpra.server.window.motion import calculate_distances, CRC_Image      #@UnresolvedImport
     1488                start = time.time()
     1489                lsd = self.scroll_data
     1490                pixels = image.get_pixels()
     1491                stride = image.get_rowstride()
     1492                width = image.get_width()
     1493                height = image.get_height()
     1494                csums = CRC_Image(pixels, width, height, stride)
     1495                self.scroll_data = (width, height, csums, image.get_sub_image(0, 0, width, height))
     1496                if lsd:
     1497                    lw, lh, lcsums, last_image = lsd
     1498                    if lw==width and lh==height:
     1499                        #same size, try to find scrolling value
     1500                        assert len(csums)==len(lcsums)
     1501                        distances = calculate_distances(csums, lcsums, 2, 500)
     1502                        if len(distances)>0:
     1503                            best = max(distances.values())
     1504                            scroll = distances.keys()[distances.values().index(best)]
     1505                            end = time.time()
     1506                            scrolllog("best scroll guess took %ims, matches %i%% of %i lines: %s", (end-start)*1000, 100*best/height, height, scroll)
     1507                            #at least 40% of the picture was found as scroll areas:
     1508                            if best>=40:
     1509                                return self.encode_scrolling(last_image, image, distances, lcsums, csums, options)
     1510            except Exception:
     1511                scrolllog.error("Error during scrolling detection!", exc_info=True)
     1512
    13571513        #dw and dh are the edges we don't handle here
    13581514        width = w & self.width_mask
    13591515        height = h & self.height_mask
     
    13811537        if delayed is not None:
    13821538            last_frame = client_options.get("frame")
    13831539            flush_delay = max(100, min(500, int(self.batch_config.delay*10)))
    1384             videolog("schedule video_encoder_flush for encoder %s, last frame=%i, client_options=%s, flush delay=%i", ve, last_frame, client_options, flush_delay)
     1540            videolog.info("schedule video_encoder_flush for encoder %s, last frame=%i, client_options=%s, flush delay=%i", ve, last_frame, client_options, flush_delay)
    13851541            self.b_frame_flush_timer = self.timeout_add(flush_delay, self.flush_video_encoder, ve, csc, last_frame, x, y)
    13861542            if data is None:
    13871543                return None
     
    14131569            #x264 has problems if we try to re-use a context after flushing the first IDR frame
    14141570            self._video_encoder = None
    14151571            ve.clean()
    1416             self.idle_add(self.full_quality_refresh)
     1572            self.idle_add(self.refresh)
    14171573            return
    14181574        v = ve.flush(frame)
    14191575        if not v:
  • xpra/x11/bindings/ximage.pyx

     
    298298            return None
    299299        return memory_as_pybuffer(pix_ptr, self.get_size(), False)
    300300
     301    def get_sub_image(self, unsigned int x, unsigned int y, unsigned int w, unsigned int h):
     302        assert w>0 and h>0, "invalid sub-image size: %ix%i" % (w, h)
     303        if x+w>self.width:
     304            raise Exception("invalid sub-image width: %i+%i greater than image width %i" % (x, w, self.width))
     305        if y+h>self.height:
     306            raise Exception("invalid sub-image height: %i+%i greater than image height %i" % (y, h, self.height))
     307        cdef void *src = self.get_pixels_ptr()
     308        if src==NULL:
     309            raise Exception("source image does not have pixels!")
     310        cdef void *dst
     311        if posix_memalign(<void **> &dst, 64, h*w*4+64):
     312            raise Exception("posix_memalign failed!")
     313        #cache into local var:
     314        cdef int stride = self.rowstride
     315        cdef unsigned int i
     316        for i in range(h):
     317            memcpy(dst+i*w*4, src + x*4 + (y+i)*stride, w*4)
     318        #memset(dst + h*w*4, 0, 64)
     319        return XImageWrapper(self.x+x, self.y+y, w, h, <unsigned long> dst, self.pixel_format, self.depth, w*4, 0, True)
     320
    301321    cdef void *get_pixels_ptr(self):
    302322        if self.pixels!=NULL:
    303323            return self.pixels
     
    391411
    392412    def restride(self, const unsigned int rowstride):
    393413        #NOTE: this must be called from the UI thread!
     414        start = time.time()
    394415        cdef unsigned int newsize = rowstride*self.height                #desirable size we could have
    395416        cdef unsigned int size = self.rowstride*self.height
    396417        #is it worth re-striding to save space:
     
    408429        #Note: we don't zero the buffer,
    409430        #so if the newstride is bigger than oldstride, you get garbage..
    410431        cdef unsigned int cpy_size = MIN(rowstride, oldstride)
    411         for ry in range(self.height):
    412             memcpy(to, img_buf, rowstride)
    413             to += rowstride
    414             img_buf += oldstride
    415         log("restride(%s) %s pixels re-stride saving %i%% from %s (%s bytes) to %s (%s bytes)",
    416             rowstride, self.pixel_format, 100-100*newsize/size, self.rowstride, size, rowstride, newsize)
     432        if oldstride==rowstride:
     433            memcpy(to, img_buf, size)
     434        else:
     435            for ry in range(self.height):
     436                memcpy(to, img_buf, rowstride)
     437                to += rowstride
     438                img_buf += oldstride
    417439        #we can now free the pixels buffer if present
    418440        #(but not the ximage - this is not running in the UI thread!)
    419441        self.free_pixels()
     
    423445        #without any X11 image to free, this is now thread safe:
    424446        if self.image==NULL:
    425447            self.thread_safe = 1
     448        #log("restride(%s) %s pixels re-stride saving %i%% from %s (%s bytes) to %s (%s bytes) took %.1fms",
     449        #    rowstride, self.pixel_format, 100-100*newsize/size, oldstride, size, rowstride, newsize, (time.time()-start)*1000)
    426450        return True
    427451
    428452