hydrus/hydrus/core/HydrusImageHandling.py

1243 lines
32 KiB
Python

import hashlib
import io
import os
import typing
import numpy
import numpy.core.multiarray # important this comes before cv!
import struct
import warnings
try:
# more hidden imports for pyinstaller
import numpy.random.common # pylint: disable=E0401
import numpy.random.bounded_integers # pylint: disable=E0401
import numpy.random.entropy # pylint: disable=E0401
except:
pass # old version of numpy, screw it
from PIL import _imaging
from PIL import ImageFile as PILImageFile
from PIL import Image as PILImage
from PIL import ImageCms as PILImageCms
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
from hydrus.core import HydrusTemp
PIL_SRGB_PROFILE = PILImageCms.createProfile( 'sRGB' )
def EnableLoadTruncatedImages():
if hasattr( PILImageFile, 'LOAD_TRUNCATED_IMAGES' ):
# this can now cause load hangs due to the trunc load code adding infinite fake EOFs to the file stream, wew lad
# hence debug only
PILImageFile.LOAD_TRUNCATED_IMAGES = True
return True
else:
return False
if not hasattr( PILImage, 'DecompressionBombError' ):
# super old versions don't have this, so let's just make a stub, wew
class DBEStub( Exception ):
pass
PILImage.DecompressionBombError = DBEStub
if not hasattr( PILImage, 'DecompressionBombWarning' ):
# super old versions don't have this, so let's just make a stub, wew
class DBWStub( Exception ):
pass
PILImage.DecompressionBombWarning = DBWStub
warnings.simplefilter( 'ignore', PILImage.DecompressionBombWarning )
warnings.simplefilter( 'ignore', PILImage.DecompressionBombError )
OLD_PIL_MAX_IMAGE_PIXELS = PILImage.MAX_IMAGE_PIXELS
PILImage.MAX_IMAGE_PIXELS = None # this turns off decomp check entirely, wew
PIL_ONLY_MIMETYPES = { HC.IMAGE_GIF, HC.IMAGE_ICON }
try:
import cv2
if cv2.__version__.startswith( '2' ):
CV_IMREAD_FLAGS_PNG = cv2.CV_LOAD_IMAGE_UNCHANGED
CV_IMREAD_FLAGS_JPEG = CV_IMREAD_FLAGS_PNG
CV_IMREAD_FLAGS_WEIRD = CV_IMREAD_FLAGS_PNG
CV_JPEG_THUMBNAIL_ENCODE_PARAMS = []
CV_PNG_THUMBNAIL_ENCODE_PARAMS = []
else:
# allows alpha channel
CV_IMREAD_FLAGS_PNG = cv2.IMREAD_UNCHANGED
# this preserves colour info but does EXIF reorientation and flipping
CV_IMREAD_FLAGS_JPEG = cv2.IMREAD_ANYDEPTH | cv2.IMREAD_ANYCOLOR
# this seems to allow weirdass tiffs to load as non greyscale, although the LAB conversion 'whitepoint' or whatever can be wrong
CV_IMREAD_FLAGS_WEIRD = CV_IMREAD_FLAGS_PNG
CV_JPEG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_JPEG_QUALITY, 92 ]
CV_PNG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_PNG_COMPRESSION, 9 ]
OPENCV_OK = True
except:
OPENCV_OK = False
def MakeClipRectFit( image_resolution, clip_rect ):
( im_width, im_height ) = image_resolution
( x, y, clip_width, clip_height ) = clip_rect
x = max( 0, x )
y = max( 0, y )
clip_width = min( clip_width, im_width )
clip_height = min( clip_height, im_height )
if x + clip_width > im_width:
x = im_width - clip_width
if y + clip_height > im_height:
y = im_height - clip_height
return ( x, y, clip_width, clip_height )
def ClipNumPyImage( numpy_image: numpy.array, clip_rect ):
if len( numpy_image.shape ) == 3:
( im_height, im_width, depth ) = numpy_image.shape
else:
( im_height, im_width ) = numpy_image.shape
( x, y, clip_width, clip_height ) = MakeClipRectFit( ( im_width, im_height ), clip_rect )
return numpy_image[ y : y + clip_height, x : x + clip_width ]
def ClipPILImage( pil_image: PILImage.Image, clip_rect ):
( x, y, clip_width, clip_height ) = MakeClipRectFit( pil_image.size, clip_rect )
return pil_image.crop( box = ( x, y, x + clip_width, y + clip_height ) )
def ConvertToPNGIfBMP( path ) -> None:
with open( path, 'rb' ) as f:
header = f.read( 2 )
if header == b'BM':
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath()
try:
with open( path, 'rb' ) as f_source:
with open( temp_path, 'wb' ) as f_dest:
HydrusPaths.CopyFileLikeToFileLike( f_source, f_dest )
pil_image = GeneratePILImage( temp_path )
pil_image.save( path, 'PNG' )
finally:
HydrusTemp.CleanUpTempPath( os_file_handle, temp_path )
def DequantizeNumPyImage( numpy_image: numpy.array ) -> numpy.array:
# OpenCV loads images in BGR, and we want to normalise to RGB in general
if numpy_image.dtype == 'uint16':
numpy_image = numpy.array( numpy_image // 256, dtype = 'uint8' )
shape = numpy_image.shape
if len( shape ) == 2:
# monochrome image
convert = cv2.COLOR_GRAY2RGB
else:
( im_y, im_x, depth ) = shape
if depth == 4:
convert = cv2.COLOR_BGRA2RGBA
else:
convert = cv2.COLOR_BGR2RGB
numpy_image = cv2.cvtColor( numpy_image, convert )
return numpy_image
def DequantizePILImage( pil_image: PILImage.Image ) -> PILImage.Image:
if HasICCProfile( pil_image ):
try:
pil_image = NormaliseICCProfilePILImageToSRGB( pil_image )
except Exception as e:
HydrusData.ShowException( e )
HydrusData.ShowText( 'Failed to normalise image ICC profile.' )
pil_image = NormalisePILImageToRGB( pil_image )
return pil_image
def GenerateNumPyImage( path, mime, force_pil = False ) -> numpy.array:
if HG.media_load_report_mode:
HydrusData.ShowText( 'Loading media: ' + path )
if not OPENCV_OK:
force_pil = True
if not force_pil:
try:
pil_image = RawOpenPILImage( path )
try:
pil_image.verify()
except:
raise HydrusExceptions.UnsupportedFileException()
# I and F are some sort of 32-bit monochrome or whatever, doesn't seem to work in PIL well, with or without ICC
if pil_image.mode not in ( 'I', 'F' ):
if pil_image.mode == 'LAB':
force_pil = True
if HasICCProfile( pil_image ):
if HG.media_load_report_mode:
HydrusData.ShowText( 'Image has ICC, so switching to PIL' )
force_pil = True
except HydrusExceptions.UnsupportedFileException:
# pil had trouble, let's cross our fingers cv can do it
pass
if mime in PIL_ONLY_MIMETYPES or force_pil:
if HG.media_load_report_mode:
HydrusData.ShowText( 'Loading with PIL' )
pil_image = GeneratePILImage( path )
numpy_image = GenerateNumPyImageFromPILImage( pil_image )
else:
if HG.media_load_report_mode:
HydrusData.ShowText( 'Loading with OpenCV' )
if mime in ( HC.IMAGE_JPEG, HC.IMAGE_TIFF ):
flags = CV_IMREAD_FLAGS_JPEG
elif mime == HC.IMAGE_PNG:
flags = CV_IMREAD_FLAGS_PNG
else:
flags = CV_IMREAD_FLAGS_WEIRD
numpy_image = cv2.imread( path, flags = flags )
if numpy_image is None: # doesn't support some random stuff
if HG.media_load_report_mode:
HydrusData.ShowText( 'OpenCV Failed, loading with PIL' )
pil_image = GeneratePILImage( path )
numpy_image = GenerateNumPyImageFromPILImage( pil_image )
else:
numpy_image = DequantizeNumPyImage( numpy_image )
numpy_image = StripOutAnyOpaqueAlphaChannel( numpy_image )
return numpy_image
def GenerateNumPyImageFromPILImage( pil_image: PILImage.Image ) -> numpy.array:
( w, h ) = pil_image.size
try:
s = pil_image.tobytes()
except OSError as e: # e.g. OSError: unrecognized data stream contents when reading image file
raise HydrusExceptions.UnsupportedFileException( str( e ) )
depth = len( s ) // ( w * h )
return numpy.fromstring( s, dtype = 'uint8' ).reshape( ( h, w, depth ) )
def GeneratePILImage( path, dequantize = True ) -> PILImage.Image:
pil_image = RawOpenPILImage( path )
if pil_image is None:
raise Exception( 'The file at {} could not be rendered!'.format( path ) )
pil_image = RotateEXIFPILImage( pil_image )
if dequantize:
# note this destroys animated gifs atm, it collapses down to one frame
pil_image = DequantizePILImage( pil_image )
return pil_image
def GeneratePILImageFromNumPyImage( numpy_image: numpy.array ) -> PILImage.Image:
# I'll leave this here as a neat artifact, but I really shouldn't ever be making a PIL from a cv2 image. the only PIL benefits are the .info dict, which this won't generate
if len( numpy_image.shape ) == 2:
( h, w ) = numpy_image.shape
format = 'L'
else:
( h, w, depth ) = numpy_image.shape
if depth == 1:
format = 'L'
elif depth == 2:
format = 'LA'
elif depth == 3:
format = 'RGB'
elif depth == 4:
format = 'RGBA'
pil_image = PILImage.frombytes( format, ( w, h ), numpy_image.data.tobytes() )
return pil_image
def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, clip_rect = None ) -> bytes:
if OPENCV_OK:
numpy_image = GenerateNumPyImage( path, mime )
if clip_rect is not None:
numpy_image = ClipNumPyImage( numpy_image, clip_rect )
thumbnail_numpy_image = ResizeNumPyImage( numpy_image, target_resolution )
try:
thumbnail_bytes = GenerateThumbnailBytesNumPy( thumbnail_numpy_image )
return thumbnail_bytes
except HydrusExceptions.CantRenderWithCVException:
pass # fallback to PIL
pil_image = GeneratePILImage( path )
if clip_rect is not None:
pil_image = ClipPILImage( pil_image, clip_rect )
thumbnail_pil_image = pil_image.resize( target_resolution, PILImage.ANTIALIAS )
thumbnail_bytes = GenerateThumbnailBytesPIL( thumbnail_pil_image )
return thumbnail_bytes
def GenerateThumbnailBytesNumPy( numpy_image ) -> bytes:
( im_height, im_width, depth ) = numpy_image.shape
numpy_image = StripOutAnyOpaqueAlphaChannel( numpy_image )
if depth == 4:
convert = cv2.COLOR_RGBA2BGRA
else:
convert = cv2.COLOR_RGB2BGR
numpy_image = cv2.cvtColor( numpy_image, convert )
( im_height, im_width, depth ) = numpy_image.shape
if depth == 4:
ext = '.png'
params = CV_PNG_THUMBNAIL_ENCODE_PARAMS
else:
ext = '.jpg'
params = CV_JPEG_THUMBNAIL_ENCODE_PARAMS
( result_success, result_byte_array ) = cv2.imencode( ext, numpy_image, params )
if result_success:
thumbnail_bytes = result_byte_array.tostring()
return thumbnail_bytes
else:
raise HydrusExceptions.CantRenderWithCVException( 'Thumb failed to encode!' )
def GenerateThumbnailBytesPIL( pil_image: PILImage.Image ) -> bytes:
f = io.BytesIO()
if PILImageHasAlpha( pil_image ):
pil_image.save( f, 'PNG' )
else:
pil_image.save( f, 'JPEG', quality = 92 )
f.seek( 0 )
thumbnail_bytes = f.read()
f.close()
return thumbnail_bytes
def GetEXIFDict( pil_image: PILImage.Image ) -> typing.Optional[ dict ]:
if pil_image.format in ( 'JPEG', 'TIFF' ) and hasattr( pil_image, '_getexif' ):
try:
exif_dict = pil_image._getexif()
if len( exif_dict ) > 0:
return exif_dict
except:
pass
return None
def GetGIFFrameDurations( path ):
pil_image = RawOpenPILImage( path )
times_to_play_gif = GetTimesToPlayGIFFromPIL( pil_image )
frame_durations = []
i = 0
while True:
try:
pil_image.seek( i )
except:
break
if 'duration' not in pil_image.info:
duration = 83 # (83ms -- 1000 / 12) Set a 12 fps default when duration is missing or too funky to extract. most stuff looks ok at this.
else:
duration = pil_image.info[ 'duration' ]
# In the gif frame header, 10 is stored as 1ms. This 1 is commonly as utterly wrong as 0.
if duration in ( 0, 10 ):
duration = 83
frame_durations.append( duration )
i += 1
return ( frame_durations, times_to_play_gif )
def GetICCProfileBytes( pil_image: PILImage.Image ) -> bytes:
if HasICCProfile( pil_image ):
return pil_image.info[ 'icc_profile' ]
raise HydrusExceptions.DataMissing( 'This image has no ICC profile!' )
def GetImagePixelHash( path, mime ) -> bytes:
numpy_image = GenerateNumPyImage( path, mime )
return hashlib.sha256( numpy_image.data.tobytes() ).digest()
def GetImageProperties( path, mime ):
if OPENCV_OK and mime not in PIL_ONLY_MIMETYPES: # webp here too maybe eventually, or offload it all to ffmpeg
numpy_image = GenerateNumPyImage( path, mime )
( width, height ) = GetResolutionNumPy( numpy_image )
duration = None
num_frames = None
else:
( ( width, height ), num_frames ) = GetResolutionAndNumFramesPIL( path, mime )
if num_frames > 1:
( durations, times_to_play_gif ) = GetGIFFrameDurations( path )
duration = sum( durations )
else:
duration = None
num_frames = None
width = max( width, 1 )
height = max( height, 1 )
return ( ( width, height ), duration, num_frames )
# bigger number is worse quality
# this is very rough and misses some finesse
def GetJPEGQuantizationQualityEstimate( path ):
try:
pil_image = RawOpenPILImage( path )
except HydrusExceptions.UnsupportedFileException:
return ( 'unknown', None )
if hasattr( pil_image, 'quantization' ):
table_arrays = list( pil_image.quantization.values() )
if len( table_arrays ) == 0:
return ( 'unknown', None )
quality = sum( ( sum( table_array ) for table_array in table_arrays ) )
quality /= len( table_arrays )
if quality >= 3400:
label = 'very low'
elif quality >= 2000:
label = 'low'
elif quality >= 1400:
label = 'medium low'
elif quality >= 1000:
label = 'medium'
elif quality >= 700:
label = 'medium high'
elif quality >= 400:
label = 'high'
elif quality >= 200:
label = 'very high'
else:
label = 'extremely high'
return ( label, quality )
return ( 'unknown', None )
def GetEmbeddedFileText( pil_image: PILImage.Image ) -> typing.Optional[ str ]:
def render_dict( d, prefix ):
texts = []
keys = sorted( d.keys() )
for key in keys:
if key in ( 'exif', 'icc_profile' ):
continue
value = d[ key ]
if isinstance( value, bytes ):
continue
if isinstance( value, dict ):
value_string = render_dict( value, prefix = ' ' + prefix )
if value_string is None:
continue
else:
value_string = ' {}{}'.format( prefix, value )
row_text = '{}{}:'.format( prefix, key )
row_text += os.linesep
row_text += value_string
texts.append( row_text )
if len( texts ) > 0:
return os.linesep.join( texts )
else:
return None
if hasattr( pil_image, 'info' ):
try:
return render_dict( pil_image.info, '' )
except:
pass
return None
def GetPSDResolution( path ):
with open( path, 'rb' ) as f:
f.seek( 14 )
height_bytes = f.read( 4 )
width_bytes = f.read( 4 )
height = struct.unpack( '>L', height_bytes )[0]
width = struct.unpack( '>L', width_bytes )[0]
return ( width, height )
def GetResolutionNumPy( numpy_image ):
( image_height, image_width, depth ) = numpy_image.shape
return ( image_width, image_height )
def GetResolutionAndNumFramesPIL( path, mime ):
pil_image = GeneratePILImage( path, dequantize = False )
( x, y ) = pil_image.size
if mime == HC.IMAGE_GIF: # some jpegs came up with 2 frames and 'duration' because of some embedded thumbnail in the metadata
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
else:
num_frames = 1
return ( ( x, y ), num_frames )
THUMBNAIL_SCALE_DOWN_ONLY = 0
THUMBNAIL_SCALE_TO_FIT = 1
THUMBNAIL_SCALE_TO_FILL = 2
thumbnail_scale_str_lookup = {
THUMBNAIL_SCALE_DOWN_ONLY : 'scale down only',
THUMBNAIL_SCALE_TO_FIT : 'scale to fit',
THUMBNAIL_SCALE_TO_FILL : 'scale to fill'
}
def GetThumbnailResolutionAndClipRegion( image_resolution, bounding_dimensions, thumbnail_scale_type: int ):
clip_rect = None
( im_width, im_height ) = image_resolution
( bounding_width, bounding_height ) = bounding_dimensions
if thumbnail_scale_type == THUMBNAIL_SCALE_DOWN_ONLY:
if bounding_width >= im_width and bounding_height >= im_height:
return ( clip_rect, ( im_width, im_height ) )
width_ratio = im_width / bounding_width
height_ratio = im_height / bounding_height
thumbnail_width = bounding_width
thumbnail_height = bounding_height
if thumbnail_scale_type in ( THUMBNAIL_SCALE_DOWN_ONLY, THUMBNAIL_SCALE_TO_FIT ):
if width_ratio > height_ratio:
thumbnail_height = im_height / width_ratio
elif height_ratio > width_ratio:
thumbnail_width = im_width / height_ratio
elif thumbnail_scale_type == THUMBNAIL_SCALE_TO_FILL:
if width_ratio == height_ratio:
# we have something that fits bounding region perfectly, no clip region required
pass
else:
clip_x = 0
clip_y = 0
clip_width = im_width
clip_height = im_height
if width_ratio > height_ratio:
clip_width = max( int( im_width * height_ratio / width_ratio ), 1 )
clip_x = ( im_width - clip_width ) // 2
elif height_ratio > width_ratio:
clip_height = max( int( im_height * width_ratio / height_ratio ), 1 )
clip_y = ( im_height - clip_height ) // 2
clip_rect = ( clip_x, clip_y, clip_width, clip_height )
thumbnail_width = max( int( thumbnail_width ), 1 )
thumbnail_height = max( int( thumbnail_height ), 1 )
return ( clip_rect, ( thumbnail_width, thumbnail_height ) )
def GetTimesToPlayGIF( path ) -> int:
try:
pil_image = RawOpenPILImage( path )
except HydrusExceptions.UnsupportedFileException:
return 1
return GetTimesToPlayGIFFromPIL( pil_image )
def GetTimesToPlayGIFFromPIL( pil_image: PILImage.Image ) -> int:
if 'loop' in pil_image.info:
times_to_play_gif = pil_image.info[ 'loop' ]
else:
times_to_play_gif = 1
return times_to_play_gif
def HasEXIF( path: str ) -> bool:
try:
pil_image = RawOpenPILImage( path )
except:
return False
result = GetEXIFDict( pil_image )
return result is not None
def HasHumanReadableEmbeddedMetadata( path: str ) -> bool:
try:
pil_image = RawOpenPILImage( path )
except:
return False
result = GetEmbeddedFileText( pil_image )
return result is not None
def HasICCProfile( pil_image: PILImage.Image ) -> bool:
if 'icc_profile' in pil_image.info:
icc_profile = pil_image.info[ 'icc_profile' ]
if isinstance( icc_profile, bytes ) and len( icc_profile ) > 0:
return True
return False
def IsDecompressionBomb( path ) -> bool:
# there are two errors here, the 'Warning' and the 'Error', which atm is just a test vs a test x 2 for number of pixels
# 256MB bmp by default, ( 1024 ** 3 ) // 4 // 3
# we'll set it at 512MB, and now catching error should be about 1GB
PILImage.MAX_IMAGE_PIXELS = ( 512 * ( 1024 ** 2 ) ) // 3
warnings.simplefilter( 'error', PILImage.DecompressionBombError )
try:
RawOpenPILImage( path )
except ( PILImage.DecompressionBombError ):
return True
except:
# pil was unable to load it, which does not mean it was a decomp bomb
return False
finally:
PILImage.MAX_IMAGE_PIXELS = None
warnings.simplefilter( 'ignore', PILImage.DecompressionBombError )
return False
def NormaliseICCProfilePILImageToSRGB( pil_image: PILImage.Image ) -> PILImage.Image:
try:
icc_profile_bytes = GetICCProfileBytes( pil_image )
except HydrusExceptions.DataMissing:
return pil_image
try:
f = io.BytesIO( icc_profile_bytes )
src_profile = PILImageCms.ImageCmsProfile( f )
if pil_image.mode in ( 'L', 'LA' ):
# had a bunch of LA pngs that turned pure white on RGBA ICC conversion
# but seem to work fine if keep colourspace the same for now
# it is a mystery, I guess a PIL bug, but presumably L and LA are technically sRGB so it is still ok to this
outputMode = pil_image.mode
else:
if PILImageHasAlpha( pil_image ):
outputMode = 'RGBA'
else:
outputMode = 'RGB'
pil_image = PILImageCms.profileToProfile( pil_image, src_profile, PIL_SRGB_PROFILE, outputMode = outputMode )
except ( PILImageCms.PyCMSError, OSError ):
# 'cannot build transform' and presumably some other fun errors
# way more advanced than we can deal with, so we'll just no-op
# OSError is due to a "OSError: cannot open profile from string" a user got
# no idea, but that seems to be an ImageCms issue doing byte handling and ending up with an odd OSError?
# or maybe somehow my PIL reader or bytesIO sending string for some reason?
# in any case, nuke it for now
pass
pil_image = NormalisePILImageToRGB( pil_image )
return pil_image
def NormalisePILImageToRGB( pil_image: PILImage.Image ) -> PILImage.Image:
if PILImageHasAlpha( pil_image ):
desired_mode = 'RGBA'
else:
desired_mode = 'RGB'
if pil_image.mode != desired_mode:
if pil_image.mode == 'LAB':
pil_image = PILImageCms.profileToProfile( pil_image, PILImageCms.createProfile( 'LAB' ), PIL_SRGB_PROFILE, outputMode = 'RGB' )
else:
pil_image = pil_image.convert( desired_mode )
return pil_image
def NumPyImageHasOpaqueAlphaChannel( numpy_image: numpy.array ) -> bool:
shape = numpy_image.shape
if len( shape ) == 2:
return False
if shape[2] == 4:
# RGBA image
# if the alpha channel is all opaque, there is no use storing that info in our pixel hash
# opaque means 255
alpha_channel = numpy_image[:,:,3].copy()
if ( alpha_channel == numpy.full( ( shape[0], shape[1] ), 255, dtype = 'uint8' ) ).all():
return True
return False
def PILImageHasAlpha( pil_image: PILImage.Image ) -> bool:
return pil_image.mode in ( 'LA', 'RGBA' ) or ( pil_image.mode == 'P' and 'transparency' in pil_image.info )
def RawOpenPILImage( path ) -> PILImage.Image:
try:
pil_image = PILImage.open( path )
except Exception as e:
raise HydrusExceptions.DamagedOrUnusualFileException( 'Could not load the image--it was likely malformed!' )
return pil_image
def ResizeNumPyImage( numpy_image: numpy.array, target_resolution ) -> numpy.array:
( target_width, target_height ) = target_resolution
( image_width, image_height ) = GetResolutionNumPy( numpy_image )
if target_width == image_width and target_height == target_width:
return numpy_image
elif target_width > image_height or target_height > image_width:
interpolation = cv2.INTER_LANCZOS4
else:
interpolation = cv2.INTER_AREA
return cv2.resize( numpy_image, ( target_width, target_height ), interpolation = interpolation )
def RotateEXIFPILImage( pil_image: PILImage.Image )-> PILImage.Image:
exif_dict = GetEXIFDict( pil_image )
if exif_dict is not None:
EXIF_ORIENTATION = 274
if EXIF_ORIENTATION in exif_dict:
orientation = exif_dict[ EXIF_ORIENTATION ]
if orientation == 1:
pass # normal
elif orientation == 2:
# mirrored horizontal
pil_image = pil_image.transpose( PILImage.FLIP_LEFT_RIGHT )
elif orientation == 3:
# 180
pil_image = pil_image.transpose( PILImage.ROTATE_180 )
elif orientation == 4:
# mirrored vertical
pil_image = pil_image.transpose( PILImage.FLIP_TOP_BOTTOM )
elif orientation == 5:
# seems like these 90 degree rotations are wrong, but fliping them works for my posh example images, so I guess the PIL constants are odd
# mirrored horizontal, then 90 CCW
pil_image = pil_image.transpose( PILImage.FLIP_LEFT_RIGHT ).transpose( PILImage.ROTATE_90 )
elif orientation == 6:
# 90 CW
pil_image = pil_image.transpose( PILImage.ROTATE_270 )
elif orientation == 7:
# mirrored horizontal, then 90 CCW
pil_image = pil_image.transpose( PILImage.FLIP_LEFT_RIGHT ).transpose( PILImage.ROTATE_270 )
elif orientation == 8:
# 90 CCW
pil_image = pil_image.transpose( PILImage.ROTATE_90 )
return pil_image
def StripOutAnyOpaqueAlphaChannel( numpy_image: numpy.array ) -> numpy.array:
if NumPyImageHasOpaqueAlphaChannel( numpy_image ):
numpy_image = numpy_image[:,:,:3].copy()
# old way, which doesn't actually remove the channel lmao lmao lmao
'''
convert = cv2.COLOR_RGBA2RGB
numpy_image = cv2.cvtColor( numpy_image, convert )
'''
return numpy_image