import cStringIO import numpy.core.multiarray # important this comes before cv! import cv import HydrusConstants as HC import os from PIL import Image as PILImage import shutil import struct import threading import time import traceback import wx #LINEAR_SCALE_PALETTE = [ 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 43, 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, 61, 61, 61, 62, 62, 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 68, 68, 68, 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, 73, 73, 74, 74, 74, 75, 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 80, 80, 80, 81, 81, 81, 82, 82, 82, 83, 83, 83, 84, 84, 84, 85, 85, 85, 86, 86, 86, 87, 87, 87, 88, 88, 88, 89, 89, 89, 90, 90, 90, 91, 91, 91, 92, 92, 92, 93, 93, 93, 94, 94, 94, 95, 95, 95, 96, 96, 96, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, 100, 100, 101, 101, 101, 102, 102, 102, 103, 103, 103, 104, 104, 104, 105, 105, 105, 106, 106, 106, 107, 107, 107, 108, 108, 108, 109, 109, 109, 110, 110, 110, 111, 111, 111, 112, 112, 112, 113, 113, 113, 114, 114, 114, 115, 115, 115, 116, 116, 116, 117, 117, 117, 118, 118, 118, 119, 119, 119, 120, 120, 120, 121, 121, 121, 122, 122, 122, 123, 123, 123, 124, 124, 124, 125, 125, 125, 126, 126, 126, 127, 127, 127, 128, 128, 128, 129, 129, 129, 130, 130, 130, 131, 131, 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, 135, 135, 135, 136, 136, 136, 137, 137, 137, 138, 138, 138, 139, 139, 139, 140, 140, 140, 141, 141, 141, 142, 142, 142, 143, 143, 143, 144, 144, 144, 145, 145, 145, 146, 146, 146, 147, 147, 147, 148, 148, 148, 149, 149, 149, 150, 150, 150, 151, 151, 151, 152, 152, 152, 153, 153, 153, 154, 154, 154, 155, 155, 155, 156, 156, 156, 157, 157, 157, 158, 158, 158, 159, 159, 159, 160, 160, 160, 161, 161, 161, 162, 162, 162, 163, 163, 163, 164, 164, 164, 165, 165, 165, 166, 166, 166, 167, 167, 167, 168, 168, 168, 169, 169, 169, 170, 170, 170, 171, 171, 171, 172, 172, 172, 173, 173, 173, 174, 174, 174, 175, 175, 175, 176, 176, 176, 177, 177, 177, 178, 178, 178, 179, 179, 179, 180, 180, 180, 181, 181, 181, 182, 182, 182, 183, 183, 183, 184, 184, 184, 185, 185, 185, 186, 186, 186, 187, 187, 187, 188, 188, 188, 189, 189, 189, 190, 190, 190, 191, 191, 191, 192, 192, 192, 193, 193, 193, 194, 194, 194, 195, 195, 195, 196, 196, 196, 197, 197, 197, 198, 198, 198, 199, 199, 199, 200, 200, 200, 201, 201, 201, 202, 202, 202, 203, 203, 203, 204, 204, 204, 205, 205, 205, 206, 206, 206, 207, 207, 207, 208, 208, 208, 209, 209, 209, 210, 210, 210, 211, 211, 211, 212, 212, 212, 213, 213, 213, 214, 214, 214, 215, 215, 215, 216, 216, 216, 217, 217, 217, 218, 218, 218, 219, 219, 219, 220, 220, 220, 221, 221, 221, 222, 222, 222, 223, 223, 223, 224, 224, 224, 225, 225, 225, 226, 226, 226, 227, 227, 227, 228, 228, 228, 229, 229, 229, 230, 230, 230, 231, 231, 231, 232, 232, 232, 233, 233, 233, 234, 234, 234, 235, 235, 235, 236, 236, 236, 237, 237, 237, 238, 238, 238, 239, 239, 239, 240, 240, 240, 241, 241, 241, 242, 242, 242, 243, 243, 243, 244, 244, 244, 245, 245, 245, 246, 246, 246, 247, 247, 247, 248, 248, 248, 249, 249, 249, 250, 250, 250, 251, 251, 251, 252, 252, 252, 253, 253, 253, 254, 254, 254, 255, 255, 255 ] def ConvertToPngIfBmp( path ): with open( path, 'rb' ) as f: header = f.read( 2 ) if header == 'BM': temp_path = HC.GetTempPath() shutil.move( path, temp_path ) pil_image = GeneratePILImage( temp_path ) pil_image = pil_image.convert( 'P' ) pil_image.save( path, 'PNG' ) os.remove( temp_path ) def EfficientlyResizeImage( pil_image, ( x, y ) ): ( im_x, im_y ) = pil_image.size if x >= im_x and y >= im_y: return pil_image if pil_image.mode == 'RGB': # low quality resize screws up alpha channel! if im_x > 2 * x and im_y > 2 * y: pil_image.thumbnail( ( 2 * x, 2 * y ), PILImage.NEAREST ) return pil_image.resize( ( x, y ), PILImage.ANTIALIAS ) def EfficientlyThumbnailImage( pil_image, ( x, y ) ): ( im_x, im_y ) = pil_image.size if pil_image.mode == 'RGB': # low quality resize screws up alpha channel! if im_x > 2 * x or im_y > 2 * y: pil_image.thumbnail( ( 2 * x, 2 * y ), PILImage.NEAREST ) pil_image.thumbnail( ( x, y ), PILImage.ANTIALIAS ) def GenerateAnimatedFrame( pil_image, target_resolution, canvas ): if 'duration' not in pil_image.info: duration = 40 # 25 fps default when duration is missing or too funky to extract. most stuff looks ok at this. else: duration = pil_image.info[ 'duration' ] if duration == 0: duration = 40 current_frame = EfficientlyResizeImage( pil_image, target_resolution ) if pil_image.mode == 'P' and 'transparency' in pil_image.info: # I think gif problems are around here somewhere; the transparency info is not converted to RGBA properly, so it starts drawing colours when it should draw nothing current_frame = current_frame.convert( 'RGBA' ) if canvas is None: canvas = current_frame else: canvas.paste( current_frame, None, current_frame ) # yeah, use the rgba image as its own mask, wut. else: canvas = current_frame return ( canvas, duration ) def GenerateHydrusBitmap( path ): pil_image = GeneratePILImage( path ) return GenerateHydrusBitmapFromPILImage( pil_image ) def GenerateHydrusBitmapFromPILImage( pil_image ): if pil_image.mode == 'RGBA' or ( pil_image.mode == 'P' and pil_image.info.has_key( 'transparency' ) ): if pil_image.mode == 'P': pil_image = pil_image.convert( 'RGBA' ) return HydrusBitmap( pil_image.tostring(), wx.BitmapBufferFormat_RGBA, pil_image.size ) else: if pil_image.mode != 'RGB': pil_image = pil_image.convert( 'RGB' ) return HydrusBitmap( pil_image.tostring(), wx.BitmapBufferFormat_RGB, pil_image.size ) def GeneratePerceptualHash( path ): thumbnail = GeneratePILImage( path ) # convert to 32 x 32 greyscale if thumbnail.mode == 'RGBA': # this is some code i picked up somewhere # another great example of PIL failing; it turns all alpha to pure black on a RGBA->RGB thumbnail.load() canvas = PILImage.new( 'RGB', thumbnail.size, ( 255, 255, 255 ) ) canvas.paste( thumbnail, mask = thumbnail.split()[3] ) thumbnail = canvas thumbnail = thumbnail.convert( 'L' ) thumbnail = thumbnail.resize( ( 32, 32 ), PILImage.ANTIALIAS ) # convert to mat cv_thumbnail_8 = cv.CreateMatHeader( 32, 32, cv.CV_8UC1 ) cv.SetData( cv_thumbnail_8, thumbnail.tostring() ) cv_thumbnail_32 = cv.CreateMat( 32, 32, cv.CV_32FC1 ) cv.Convert( cv_thumbnail_8, cv_thumbnail_32 ) # compute dct dct = cv.CreateMat( 32, 32, cv.CV_32FC1 ) cv.DCT( cv_thumbnail_32, dct, cv.CV_DXT_FORWARD ) # take top left 8x8 of dct dct = cv.GetSubRect( dct, ( 0, 0, 8, 8 ) ) # get mean of dct, excluding [0,0] mask = cv.CreateMat( 8, 8, cv.CV_8U ) cv.Set( mask, 1 ) mask[0,0] = 0 channel_averages = cv.Avg( dct, mask ) average = channel_averages[0] # make a monochromatic, 64-bit hash of whether the entry is above or below the mean bytes = [] for i in range( 8 ): byte = 0 for j in range( 8 ): byte <<= 1 # shift byte one left value = dct[i,j] if value > average: byte |= 1 bytes.append( byte ) answer = str( bytearray( bytes ) ) # we good return answer def GeneratePILImage( path ): return PILImage.open( path ) def GenerateResolutionAndNumFrames( path ): pil_image = GeneratePILImage( path ) ( x, y ) = pil_image.size try: pil_image.seek( 1 ) pil_image.seek( 0 ) num_frames = 1 while True: try: pil_image.seek( pil_image.tell() + 1 ) num_frames += 1 except: break except: num_frames = 1 return ( ( x, y ), num_frames ) def GenerateThumbnail( path, dimensions = HC.UNSCALED_THUMBNAIL_DIMENSIONS ): pil_image = GeneratePILImage( path ) EfficientlyThumbnailImage( pil_image, dimensions ) f = cStringIO.StringIO() if pil_image.mode == 'P' and pil_image.info.has_key( 'transparency' ): pil_image.save( f, 'PNG', transparency = pil_image.info[ 'transparency' ] ) elif pil_image.mode == 'RGBA': pil_image.save( f, 'PNG' ) else: pil_image = pil_image.convert( 'RGB' ) pil_image.save( f, 'JPEG', quality=92 ) f.seek( 0 ) thumbnail = f.read() f.close() return thumbnail def GetHammingDistance( phash1, phash2 ): distance = 0 phash1 = bytearray( phash1 ) phash2 = bytearray( phash2 ) for i in range( len( phash1 ) ): xor = phash1[i] ^ phash2[i] while xor > 0: distance += 1 xor &= xor - 1 return distance def RenderImage( path, hash, target_resolution = None, synchronous = True ): try: ( original_resolution, num_frames ) = GenerateResolutionAndNumFrames( path ) if target_resolution is None: target_resolution = original_resolution image_container = RenderedImageContainer( hash, original_resolution, target_resolution, num_frames ) if image_container.IsAnimated(): renderer = AnimatedFrameRenderer( image_container, path, target_resolution ) else: renderer = StaticFrameRenderer( image_container, path, target_resolution ) if synchronous: renderer.Render() else: threading.Thread( target = renderer.RenderCallAfter ).start() return image_container except: raise Exception( 'Attempted to render the image, but it was either formatted slightly incorrectly or PIL could not handle it; look up PIL in the hydrus help for more info.' ) class FrameRenderer(): def __init__( self, image_container, path, target_resolution ): self._image_container = image_container self._pil_image = GeneratePILImage( path ) self._target_resolution = target_resolution class AnimatedFrameRenderer( FrameRenderer ): def GetFrames( self ): canvas = None global_palette = self._pil_image.palette dirty = self._pil_image.palette.dirty mode = self._pil_image.palette.mode rawmode = self._pil_image.palette.rawmode # believe it or not, doing this actually fixed a couple of gifs! self._pil_image.seek( 1 ) self._pil_image.seek( 0 ) while True: ( canvas, duration ) = GenerateAnimatedFrame( self._pil_image, self._target_resolution, canvas ) yield ( GenerateHydrusBitmapFromPILImage( canvas ), duration ) try: self._pil_image.seek( self._pil_image.tell() + 1 ) if self._pil_image.palette == global_palette: # for some reason, when we fall back to global palette (no local-frame palette), we reset bunch of important variables! self._pil_image.palette.dirty = dirty self._pil_image.palette.mode = mode self._pil_image.palette.rawmode = rawmode except: break def Render( self ): for ( frame, duration ) in self.GetFrames(): self._image_container.AddFrame( frame, duration ) def RenderCallAfter( self ): time.sleep( 0 ) # thread yield for ( frame, duration ) in self.GetFrames(): wx.CallAfter( self._image_container.AddFrame, frame, duration ) HC.pubsub.pub( 'finished_rendering', self._image_container.GetKey() ) class StaticFrameRenderer( FrameRenderer ): def GetFrame( self ): return GenerateHydrusBitmapFromPILImage( EfficientlyResizeImage( self._pil_image, self._target_resolution ) ) def Render( self ): self._image_container.AddFrame( self.GetFrame() ) def RenderCallAfter( self ): time.sleep( 0 ) # thread yield wx.CallAfter( self._image_container.AddFrame, self.GetFrame() ) HC.pubsub.pub( 'finished_rendering', self._image_container.GetKey() ) class HydrusBitmap(): def __init__( self, data, format, size ): self._data = data self._format = format self._size = size def CreateWxBmp( self ): ( width, height ) = self._size if self._format == wx.BitmapBufferFormat_RGB: return wx.BitmapFromBuffer( width, height, self._data ) else: return wx.BitmapFromBufferRGBA( width, height, self._data ) def GetEstimatedMemoryFootprint( self ): return len( self._data ) def GetSize( self ): return self._size class RenderedImageContainer(): def __init__( self, hash, original_resolution, my_resolution, num_frames ): self._hash = hash self._original_resolution = original_resolution self._my_resolution = my_resolution self._num_frames = num_frames ( original_width, original_height ) = original_resolution ( my_width, my_height ) = my_resolution width_zoom = my_width / float( original_width ) height_zoom = my_height / float( original_height ) self._zoom = min( ( width_zoom, height_zoom ) ) if self._zoom > 1.0: self._zoom = 1.0 self._frames = [] self._durations = [] self._finished_rendering = False def AddFrame( self, frame, duration = None ): self._frames.append( frame ) if duration is not None: self._durations.append( duration ) def GetDuration( self, index ): return self._durations[ index ] def GetEstimatedMemoryFootprint( self ): return sum( [ frame.GetEstimatedMemoryFootprint() for frame in self._frames ] ) def GetFrame( self, index = None ): if index is None: return self._frames[ 0 ] else: return self._frames[ index ] def GetHash( self ): return self._hash def GetKey( self ): return ( self._hash, self._my_resolution ) def GetNumFrames( self ): return self._num_frames def GetNumFramesRendered( self ): return len( self._frames ) def GetResolution( self ): return self._original_resolution def GetSize( self ): return self._my_resolution def GetTotalDuration( self ): return sum( self._durations ) def GetZoom( self ): return self._zoom def HasFrame( self, index = None ): if index is None: index = 0 return len( self._frames ) > index def IsAnimated( self ): return self._num_frames > 1 def IsFinishedRendering( self ): return len( self._frames ) == self._num_frames def IsScaled( self ): return self._zoom != 1.0