Source code for gavo.utils.imgtools

"""
Some miscellaneous helpers for making images and such.

As this may turn into a fairly expensive import, this should *not* be imported
by utils.__init__.   Hence, none of these functions are in gavo.api or
gavo.utils.
"""

#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import io

from PIL import Image
import numpy

from gavo.utils.dachstypes import (Filename, NDArray)


def _normalizeForImage(pixels: NDArray, gamma: float) -> NDArray:
	"""helps jpegFromNumpyArray and friends.
	"""
	pixels = numpy.flipud(pixels)
	pixMax, pixMin = numpy.max(pixels), numpy.min(pixels)
	return numpy.asarray(numpy.power(
		(pixels-pixMin)/(pixMax-pixMin), gamma)*255, 'uint8')


[docs]def jpegFromNumpyArray(pixels: NDArray, gamma: float=0.25) -> bytes: """returns a normalized JPEG for numpy pixels. pixels is assumed to come from FITS arrays, which are flipped wrt to jpeg coordinates, which is why we're flipping here. The normalized intensities are scaled by v^gamma; we believe the default helps with many astronomical images """ f = io.BytesIO() Image.fromarray(_normalizeForImage(pixels, gamma) ).save(f, format="jpeg") return f.getvalue()
[docs]def colorJpegFromNumpyArrays( rPix: NDArray, gPix: NDArray, bPix: NDArray, gamma: float=0.25 ) -> bytes: """as jpegFromNumpyArray, except a color jpeg is built from red, green, and blue pixels. """ pixels = numpy.array([ _normalizeForImage(rPix, gamma), _normalizeForImage(gPix, gamma), _normalizeForImage(bPix, gamma)]).transpose(1,2,0) f = io.BytesIO() Image.fromarray(pixels, mode="RGB").save(f, format="jpeg") return f.getvalue()
[docs]def scaleNumpyArray(arr: NDArray, destSize: int) -> NDArray: """returns the numpy array arr scaled down to approximately destSize. """ origWidth, origHeight = arr.shape size = max(origWidth, origHeight) scale = max(1, size//destSize+1) destWidth, destHeight = origWidth//scale, origHeight//scale # There's very similar code in fitstools.iterScaledRows # -- it would be nice to refactor things so this can be shared. img = numpy.zeros((destWidth, destHeight), 'float32') for rowInd in range(destHeight): wideRow = (numpy.sum( arr[:,rowInd*scale:(rowInd+1)*scale], 1, 'float32' )/scale)[:destWidth*scale] # horizontal scaling via reshaping to a matrix and then summing over # its columns. newRow = numpy.sum( numpy.transpose(wideRow.reshape((destWidth, scale))), 0)/scale img[:,rowInd] = newRow return img
[docs]def getScaledPNG(srcFile: Filename, newWidth: int) -> bytes: """returns PNG bytes that is a scaled version of an image in srcFile. srcFile must correspond to something that PIL can read. Since scaling really sucks for non-RGB images, we unconditionally convert whatever we get to 3-band RGB. """ im = Image.open(srcFile).convert("RGB") scale = newWidth/float(im.size[0]) scaled = im.resize((newWidth, int(im.size[1]*scale)), Image.LANCZOS) f = io.BytesIO() scaled.save(f, format="png") return f.getvalue()