#!/usr/bin/env python # file: $NEDC_NFC/class/python/nedc_image_tools/nedc_image_tools.py # # revision history: # # 20200604 (JP): refactored the code and created images classes # 20180617 (RA): separate class and driver to ISIP standard # 20180620 (RA): initial version # # This class contains methods to process images. #------------------------------------------------------------------------------ # import system modules # import os import sys # import NEDC modules # import nedc_debug_tools as ndt import nedc_file_tools as nft # import 3rd-party modules # import openslide import cv2 import numpy as np from PIL import Image as pil from PIL import ImageOps #------------------------------------------------------------------------------ # # global variables are listed here # #------------------------------------------------------------------------------ # set the filename using basename # __FILE__ = os.path.basename(__file__) # define a flag for the type of image # ERR = int(-1) PIL = int(0) SVS = int(1) DEF = ERR # define important image specific information # SVS_MAGIC = b'II*\x00' SVS_DEF_LEVEL = int(0) SVS_MODE_CONVERT = "RGB" DEF_MAX_DIM = int(1600) DEF_ITYPE = "JPEG" DEF_OEXT = "jpg" DEF_SLIDE_LEVEL = int(0) #------------------------------------------------------------------------------ # # classes are listed here # #------------------------------------------------------------------------------ # class: Pil # # This class facilitates manipulation of Python Image Library (PIL) files. # class Pil: #-------------------------------------------------------------------------- # # static data declarations # #-------------------------------------------------------------------------- # define static variables for debug and verbosity # dbgl_d = ndt.Dbgl() vrbl_d = ndt.Vrbl() #-------------------------------------------------------------------------- # # constructors # #-------------------------------------------------------------------------- # method: Pil::constructor # def __init__(self): # create class data # Pil.__CLASS_NAME__ = self.__class__.__name__ self.image_d = None #-------------------------------------------------------------------------- # # set/get methods # #-------------------------------------------------------------------------- #-------------------------------------------------------------------------- # # file-based methods # #-------------------------------------------------------------------------- # method: Pil::is_pil # # arguments: # fname: input filename # # return: an integer value indicating the type of file # def is_pil(self, fname): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: opening pil file" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__, fname)) # check if it is a PIL image # try: img = pil.open(fname) img.close() return PIL except: return ERR # else: it is not a pil file # return ERR # method: Pil::open # # arguments: # fname: input filename # # return: an integer value indicating the type of file # def open(self, fname): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: opening pil file" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__)) # open the file # self.image_d = pil.open(fname) if self.image_d == None: return ERR # exit gracefully # return PIL # method: Pil::close # # arguments: none # # return: a boolean value indicating status # def close(self): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: closing pil file" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__)) # close the file # self.image_d.close() # exit gracefully # return True # method: Pil::read # # arguments: none # # return: a boolean value indicating status # def read(self): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: reading pil image" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__)) if self.image_d == None: print("Error: %s (line: %s) %s::%s: image not read" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__)) return False # note with the PIL class, we don't need to actually load # the image since it is automatically loaded, and using the load # method closes the file. # # self.image_d.load() # exit gracefully # return True # method: Pil::write # # arguments: # fname: filename to be written (input) # image: a Pil image object # itype: the image type # # return: a boolean value indicating status # def write(self, fname, image, itype): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: writing pil image" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__)) if image == None: print("Error: %s (line: %s) %s::%s: pil empty image" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__)) return False # save the image # try: image.save(fname, itype) except: print("Error: %s (line: %s) %s::%s: pil image not written" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__)) return False # exit gracefully # return True # method: Pil::resize # # arguments: # max_dim: the maximum dimension of the new image # # return: a new PIL image object # # This method resizes a PIL image. # def resize(self, max_dim = DEF_MAX_DIM): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: resizing pil image" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__)) # rotate image if detect EXIF is flaged meaning the camera was rotated # image = ImageOps.exif_transpose(self.image_d) # gets the size of the image in the tuple (width, height) # size = image.size # check to see if the dimensions exceed the maximum dimension: # do nothing # if(size[0] < max_dim and size[1] < max_dim): return self.image_d # gets the resize scale # resize_scale = max_dim / np.max(size) # gets the new size of the image # new_size = np.int64(resize_scale * np.array(size)) # resize the image object # image_new = image.resize(tuple(new_size)) # return the image object # return image_new # # end of class # class: Svs # # This class facilitates manipulation of Svs images. # class Svs: #-------------------------------------------------------------------------- # # static data declarations # #-------------------------------------------------------------------------- # define a static variable to hold the value # dbgl_d = ndt.Dbgl() vrbl_d = ndt.Vrbl() #-------------------------------------------------------------------------- # # constructors # #-------------------------------------------------------------------------- # method: Svs::constructor # def __init__(self): # create class data # Svs.__CLASS_NAME__ = self.__class__.__name__ self.image_d = None self.level_d = SVS_DEF_LEVEL self.svs_d = None #-------------------------------------------------------------------------- # # set/get methods # #-------------------------------------------------------------------------- # method: Svs::set level # # arguments: # level: the level of the hierarchical image to be read # # return: an integer containing the new value # def set_level(self, level = SVS_DEF_LEVEL): self.level_d = level return level #-------------------------------------------------------------------------- # # file-based methods # #-------------------------------------------------------------------------- # method: Svs::is_svs # # arguments: # fname: input filename # # return: an integer value indicating the type of file # def is_svs(self, fname): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: opening pil file" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__, fname)) # check if it is an SVS image: # note that we simply look at the magic sequence at the beginning # of the file. this is much faster than opening the file in openslide. # try: fp = open(fname, nft.MODE_READ_BINARY) val = fp.read(4) fp.close() if val == SVS_MAGIC: return SVS except: return ERR # else: it is not an svs file # return ERR # method: Svs::open # # arguments: # fname: input filename # # return: an integer value indicating the type of file # def open(self, fname): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: opening svs file (%s)" % (__FILE__, ndt.__LINE__, Svs.__CLASS_NAME__, ndt.__NAME__, fname)) # open the file # self.svs_d = openslide.OpenSlide(fname) if self.svs_d == None: return ERR # exit gracefully # return SVS # method: Svs::close # # arguments: none # # return: a boolean value indicating status # def close(self): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: closing svs file" % (__FILE__, ndt.__LINE__, Svs.__CLASS_NAME__, ndt.__NAME__)) # close the file # self.image_d.close() # exit gracefully # return True # method: Svs::read # # arguments: none # # return: a boolean value indicating status # def read(self): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: reading svs image" % (__FILE__, ndt.__LINE__, Svs.__CLASS_NAME__, ndt.__NAME__)) # gets the dimensions at the given slide level # if self.level_d < len(self.svs_d.level_dimensions): dims = self.svs_d.level_dimensions[self.level_d] else: print("Error: %s (line: %s) %s::%s: %s (%d) exceeds %s (%d)" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__, "level", self.level_d, "dimension", len(self.svs_d.level_dimensions))) return False # read the desired layer # image = self.svs_d.read_region((0, 0), self.level_d, dims) # remove the alpha channel # self.image_d = image.convert(SVS_MODE_CONVERT) # exit gracefully # return True # method: Svs::write # # arguments: # fname: filename to be written (input) # image: a Pil image object # itype: the image type # # return: a boolean value indicating status # # Note that we always write images in PIL format. # def write(self, fname, image, itype): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: writing svs as a pil image" % (__FILE__, ndt.__LINE__, Svs.__CLASS_NAME__, ndt.__NAME__)) if image == None: print("Error: %s (line: %s) %s::%s: svs empty image" % (__FILE__, ndt.__LINE__, Svs.__CLASS_NAME__, ndt.__NAME__)) return False # save the image # try: image.save(fname, itype) except: print("Error: %s (line: %s) %s::%s: svs image not written" % (__FILE__, ndt.__LINE__, Svs.__CLASS_NAME__, ndt.__NAME__)) return False # exit gracefully # return True # method: Svs::resize # # arguments: # max_dim: the maximum dimension of the new image # # return: a new PIL image object # # This method resizes a PIL image. Note that an Svs object is converted # to a PIL object during the read operation. # def resize(self, max_dim): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: resizing svs image" % (__FILE__, ndt.__LINE__, Svs.__CLASS_NAME__, ndt.__NAME__)) # gets the size of the image in the tuple (width, height) # size = self.image_d.size # check to see if the dimensions exceed the maximum dimension: # do nothing # if(size[0] < max_dim and size[1] < max_dim): return image # gets the resize percentage # resize_percent = float(1.0 * max_dim / max(size)) # gets the new size of the image # new_size = [int(round(1.0 * dim * resize_percent)) for dim in size] # resize the image object # image_new = self.image_d.resize((new_size[0], new_size[1]), pil.ANTIALIAS) # return the image object # return image_new # # end of class # class: Nil # # This class facilitates manipulation of Python Image Library (PIL) files # and svs files. # class Nil: #-------------------------------------------------------------------------- # # static data declarations # #-------------------------------------------------------------------------- # define a static variable to hold the value # dbgl_d = ndt.Dbgl() vrbl_d = ndt.Vrbl() #-------------------------------------------------------------------------- # # constructors # #-------------------------------------------------------------------------- # method: Nil::constructor # def __init__(self): Nil.__CLASS_NAME__ = self.__class__.__name__ # create class data # self.pil_d = Pil() self.svs_d = Svs() self.itype_d = ERR self.image_d = None #-------------------------------------------------------------------------- # # set/get methods # #-------------------------------------------------------------------------- # method: Nil::set level # # arguments: # level: the level of the hierarchical image to be read # # return: an integer containing the new value # # Note: we always pass this to an Svs object since a Pil object # does not use this value. # def set_level(self, level = SVS_DEF_LEVEL): self.svs_d.set_level(level) return level #-------------------------------------------------------------------------- # # file-based methods # #-------------------------------------------------------------------------- # method: Nil::is_image # # arguments: # fname: input filename # # return: an integer value indicating the type of file # def is_image(self, fname): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: opening pil file" % (__FILE__, ndt.__LINE__, Pil.__CLASS_NAME__, ndt.__NAME__, fname)) # check for an svs image: # note we do this first because it is faster and is_pil # self.itype_d = self.svs_d.is_svs(fname); if self.itype_d == SVS: return SVS # check for a pil image # self.itype_d = self.pil_d.is_pil(fname); if self.itype_d == PIL: return PIL # else: it is not a supported image type # return ERR # method: Nil::resize # # arguments: # max_dim: the maximum dimension of the new image # # return: a new PIL image object # # This method resizes a PIL image. # def resize(self, max_dim): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: resizing image" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) # branch on the type of image and exit gracefully # if self.itype_d == PIL: image_new = self.pil_d.resize(max_dim) if self.pil_d.resize(max_dim) == None: print("Error: %s (line: %s) %s::%s: pil resize error" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return None elif self.itype_d == SVS: image_new = self.svs_d.resize(max_dim) if self.svs_d.resize(max_dim) == None: print("Error: %s (line: %s) %s::%s: svs resize error" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return None else: print("Error: %s (line: %s) %s::%s: unknown type (%d)" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__, self.itype_d)) return None # exit gracefully # return image_new # method: Nil::open # # This method opens an image. It essentially abstracts # the type of image. # def open(self, ifname): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: opening file (%s)" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__, ifname)) # get the type of image # self.itype_d = self.is_image(ifname) if self.itype_d == ERR: print("%s (line: %s) %s::%s: not a supported type (%s)" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__, ifname)) sys.exit(os.EX_SOFTWARE) if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: image type (%d)" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__, self.itype_d)) # branch on the type of image and exit gracefully # if self.itype_d == PIL: return self.pil_d.open(ifname) elif self.itype_d == SVS: return self.svs_d.open(ifname) else: return False # method: Nil::close # # arguments: none # # return: a boolean value indicating status # # This method closes the image files and frees memory. # def close(self): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: closing file" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) # branch on the type of image # if self.itype_d == PIL: if self.pil_d.close() == False: print("Error: %s (line: %s) %s::%s: pil close error" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False elif self.itype_d == SVS: if self.svs_d.close() == False: print("Error: %s (line: %s) %s::%s: svs close error" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False else: return False # exit gracefully # return True # method: Nil::read # # This method reads an image. It essentially abstracts # the type of image. # def read(self): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: reading image" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) # branch on the type of image and exit gracefully # if self.itype_d == PIL: if self.pil_d.read() == False: print("Error: %s (line: %s) %s::%s: pil read error" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False elif self.itype_d == SVS: if self.svs_d.read() == False: print("Error: %s (line: %s) %s::%s: svs read error" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False else: return False # exit gracefully # return True # method: Nil::write # # This method writes an image. Only PIL writing is supported. # def write(self, fname, image, type): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: writing image" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) # branch on the type of image and exit gracefully # if self.itype_d == PIL: if self.pil_d.write(fname, image, type) == False: print("Error: %s (line: %s) %s::%s: pil write error" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False elif self.itype_d == SVS: if self.pil_d.write(fname, image, type) == False: print("Error: %s (line: %s) %s::%s: svs write error" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False else: return False # exit gracefully # return True # method: Nil::resize_file # # arguments: # ofname: output filename # ifname: input filename # max_dim: maximum dimension # type: the image type # # return: a boolean value indicating status # # This method opens an image, reads it, resizes it, # and writes it to a new filename. Note that we always # write a PIL file. # def resize_file(self, ofname, ifname, max_dim = DEF_MAX_DIM, type = DEF_ITYPE): # display informational message # if self.dbgl_d > ndt.BRIEF: print("%s (line: %s) %s::%s: resizing file (%s)" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__, ifname)) # open the image # if self.open(ifname) == ERR: print("Error: %s (line: %s) %s::%s: error opening file (%s)" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__, ifname)) return False # read the image # if self.read() == False: print("Error: %s (line: %s) %s::%s: error reading image" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False # resize the image # image_new = self.resize(max_dim) if image_new == None: print("Error: %s (line: %s) %s::%s: error resizing image" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False # write the image # if self.write(ofname, image_new, type) == False: print("Error: %s (line: %s) %s::%s: error writing image" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False # close the image # if self.close() == False: print("Error: %s (line: %s) %s::%s: error closing file" % (__FILE__, ndt.__LINE__, Nil.__CLASS_NAME__, ndt.__NAME__)) return False # exit gracefully # return True # # end of class # # end of file