Convert multi-channel numpy array to photoshop PSD file - python

I have a numpy array with 3 RGB channels and two alpha channels. (I am using python)
I want to convert it to a Photoshop .psd file, so I can later apply transformations in photoshop on the annotated alpha layers.
I guess it a very simple task but I haven't find any way to do it from the packages I found by googling it.
I guess it should be something in the lines of the following:
>> im.shape
(.., .., 4)
psd = PSD()
psd.add_layers_from_numpy(im, names=["R", "G", "B", "alpha-1", "alpha-2")
with open(of, 'wb') as f:
psd.write(f)
If you know how to do this, please let me know.
Thanks in advance!

I opened your PSD file in Photoshop and saved it as a TIFF. I then checked with tiffinfo and determined that your file is saved as RGB with 3 layers of "Extra samples":
tiffinfo MULTIPLE_ALPHA.tif
TIFF Directory at offset 0x8 (8)
Subfile Type: (0 = 0x0)
Image Width: 1000 Image Length: 1430
Resolution: 72, 72 pixels/inch
Bits/Sample: 8
Compression Scheme: None
Photometric Interpretation: RGB color <--- HERE
Extra Samples: 3<unspecified, unspecified, unspecified> <--- HERE
Orientation: row 0 top, col 0 lhs
Samples/Pixel: 6
Rows/Strip: 1430
Planar Configuration: single image plane
Software: Adobe Photoshop 21.2 (Macintosh)
DateTime: 2020:09:08 19:34:38
You can load that into Python with:
import tifffile
import numpy as np
# Load TIFF saved by Photoshop
im = tifffile.imread('MULTIPLE_ALPHA.tif')
# Check shape
print(im.shape) # prints (1430, 1000, 6)
# Save with 'tifffile'
tifffile.imwrite('saved.tif', im, photometric='RGB')
And now check that Photoshop looks at and treats the tifffile image the same as your original:
You may want to experiment with the compress parameter. I noticed your file comes out as 8.5MB uncompressed, but as 2.4MB of lossless Zip-compressed data if I use:
tifffile.imwrite('saved.tif', im, photometric='RGB', compress=1)
Note that reading/writing with compression requires you to install imagecodecs:
pip install imagecodecs
Note that I am not suggesting it is impossible with a PSD-writer package, I am just saying I believe you can get what you want with a TIFF.
Keywords: Image processing, Photoshop, PSD, TIFF, save multi-channel, multi-layer, multi-image, multiple alpha, tifffile, Python

Related

Python Pillow - RLE compression of BMP image

I'm working on a script, which builds an image, combines it with another image and saves it locally as an 8-bit BMP-file.
The image is then read by a ESP32 microcontroller, but the problem is that due to memorylimitations, the allowed file size is somewhat limited.
As a consequence, I made a BMP decoder for the ESP32, which supports RLE. In theory, the allowed number of bytes can still be exceeded, but only text and simple icons are to be read, so it will most likely never happen.
It uses Pillow for image processing, which now supports RLE-compression from version 9.1.0
https://github.com/python-pillow/Pillow/blob/main/docs/handbook/image-file-formats.rst
Pillow reads and writes Windows and OS/2 BMP files containing 1, L, P,
or RGB data. 16-colour images are read as P images. Support for
reading 8-bit run-length encoding was added in Pillow 9.1.0. Support
for reading 4-bit run-length encoding was added in Pillow 9.3.0.
Here's the part of the code, that combines two existing images into a new one and saves them:
img_buf = io.BytesIO() # Start converting from Matplotlib to PIL
# Supported: eps, jpeg, jpg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff, webp
plt.savefig(img_buf, format='png', transparent=True)
graph = Image.open(img_buf)
# Create empty, 8-bit canvas
new_image = Image.new('P',(600,448), (255,255,255)) # P = 8-bit indexed
new_image.paste(img,(0,0)) # Insert image 1 into canvas
new_image.paste(graph,(0,200)) # Insert image 2 into canvas at y:200
new_image.save("../output/priceeast.bmp", compression=1) # According to the docs, 1 = RLE
It saves the image, alright, but not RLE-encoded and I can't work out, how to enable it... or is RLE only supported when reading BMP, not saving?
UPDATE:
I added this line below:
subprocess.call('magick ../output/priceeast.png -type palette -compress RLE ../output/priceeast.bmp ', shell=True)
Pillow does not support writing BMP files with compression, which can be determined by investigating the source. BmpImagePlugin._write:
# bitmap info header
fp.write(
o32(header) # info header size
+ o32(im.size[0]) # width
+ o32(im.size[1]) # height
+ o16(1) # planes
+ o16(bits) # depth
+ o32(0) # compression (0=uncompressed)
+ o32(image) # size of bitmap
+ o32(ppm[0]) # resolution
+ o32(ppm[1]) # resolution
+ o32(colors) # colors used
+ o32(colors) # colors important
)
We can see here that the compression field in the output file header is hard-coded to none, indicating that compression is not supported when writing a file.
If you would like a work-around, ImageMagick can convert any image format to 8-bit RLE BMP like this:
magick INPUTIMAGE.xxx -type palette -compress RLE result.bmp # where XXX is PNG, JPG, TIFF, GIF, TGA etc
Check the result with exiftool like this:
exiftool -filename -filesize -compression result.bmp
File Name : result.bmp
File Size : 1168 bytes
Compression : 8-Bit RLE
Note that there are Python bindings to ImageMagick via wand, so you can achieve the same effect in Python like this:
#!/usr/bin/env python3
# You may need this before running on macOS
# export MAGICK_HOME=/opt/homebrew
from wand.image import Image
# Load image , or create pseudo image
with Image(filename='gradient:red-blue') as img:
img.type = 'palette'
img.compression = 'rle'
img.save(filename='result.bmp')

Weird interaction with Python PIL image.save quality parameter

This is just a part of the project I'm currently working on. I am trying to convert picture into text, then from text back to the picture without any loss or extra size.
First, I open the picture, read the pixels, and write them down. Pictures are size NxN.
from PIL import Image
import sys
import zlib
def rgb_to_hex(rgb):
return '%02x%02x%02x' % rgb
N = im.width
im = Image.open(r"path\\pic.png")
px = im.load()
read_pixels = ""
for i in range(N):
for j in range(N):
read_pixels += rgb_to_hex(px[j, i,])
Then, transform the string into bytes.
data = bytes.fromhex(read_pixels)
img = Image.frombytes("RGB", (N,N), data)
img.save("path\\new.png",quality = 92)
According to the Pillow official documentation they are saying that quality goes from 0 - 100 and values over 95 should be avoided. If there is nothing set, the default value is 75.
For example I used this picture.
The original photo when downloaded takes up 917 KB. When the picture is converted by the program, the new picture takes up 911 KB. Then I take my new picture (911KB) and run that one by the same program and I get back the same size 911KB this one did not shrink by a few KB and I do not know why. Why does this weird interaction happen only when I put original picture of 917 KB? Is there a way I could get 100% of the original quality.
I also tried this on some random 512x512 .jpg picture. Original size of that picture is 67.4KB, next "generation" of that picture is 67.1KB and one after that is 66.8KB. Also if I change quality to 93 or above (when using .jpg) the size goes up by a lot (at quality = 100, size > 135KB). I was 'playing' around with quality value and found out closest to the same size is 92 (<93 puts some extra KB for .jpg).
So with quality 92 .PNG the size stays the same after the first "generation" but with .jpg the size (and potentially quality) goes down.
Is there something I am missing in my code? My best guess is that .PNG stores some extra information about the picture which is lost in the conversion, but not sure why the .jpg pictures decrease in size every generation. I tried putting 92.5 quality but the function does not accept decimal numbers as parameters.
Quick takeaways from the following explanations...
The quality parameter for PIL.Image.save isn't used when saving PNGs.
JPEG is generationally-lossy so as you keep re-saving images, they will likely degrade in quality because the algorithm will introduce more artifacting (among other things)
PNG is lossless and the file size differences you're seeing are due to PIL stripping metadata when you re-save your image.
Let's look at your PNG file first. PNG is a lossless format - the image data you give it will not suffer generational loss if you were to open it and re-save it as PNG over and over again.
The quality parameter isn't even recognized by the PNG plugin to PIL - if you look at the PngImagePlugin.py/PngStream._save method it is never referenced in there.
What's happening with your specific sample image is that Pillow is dropping some metadata when you re-save it in your code.
On my test system, I have your PNG saved as sample.png, and I did a simple load-and-save with the following code and save it as output.png (inside ipython)
In [1]: from PIL import Image
In [2]: img = Image.open("sample.png")
In [3]: img.save("output.png")
Now let's look at the differences between their metadata with ImageMagick:
#> diff <(magick identify -verbose output.png) <(magick identify -verbose sample.png)
7c7,9
< Units: Undefined
---
> Resolution: 94.48x94.48
> Print size: 10.8383x10.8383
> Units: PixelsPerCentimeter
74c76,78
< Orientation: Undefined
---
> Orientation: TopLeft
> Profiles:
> Profile-exif: 5218 bytes
76,77c80,81
< date:create: 2022-08-12T21:27:13+00:00
< date:modify: 2022-08-12T21:27:13+00:00
---
> date:create: 2022-08-12T21:23:42+00:00
> date:modify: 2022-08-12T21:23:31+00:00
78a83,85
> exif:ImageDescription: IMGP5493_seamless_2.jpg
> exif:ImageLength: 1024
> exif:ImageWidth: 1024
84a92
> png:pHYs: x_res=9448, y_res=9448, units=1
85a94,95
> png:text: 1 tEXt/zTXt/iTXt chunks were found
> png:text-encoded profiles: 1 were found
86a97
> unknown: nomacs - Image Lounge 3.14
90c101
< Filesize: 933730B
---
> Filesize: 939469B
93c104
< Pixels per second: 42.9936MP
---
> Pixels per second: 43.7861MP
You can see there are metadata differences - PIL didn't retain some of the information when re-saving the image, especially some exif properties (you can see this PNG was actually converted from a JPG and the EXIF metadata was preserved in the conversion).
However, if you re-save the image with original image's info data...
In [1]: from PIL import Image
In [2]: img = Image.open("sample.png")
In [3]: img.save("output-with-info.png", info=img.info)
You'll see that the two files are exactly the same again:
❯ sha256sum output.png output-with-info.png
37ad78a7b7000c9430f40d63aa2f0afd2b59ffeeb93285b12bbba9c7c3dec4a2 output.png
37ad78a7b7000c9430f40d63aa2f0afd2b59ffeeb93285b12bbba9c7c3dec4a2 output-with-info.png
Maybe Reducing PNG File Size
While lossless, the PNG format does allow for reducing the size of the image by specifying how aggressive the compression is (there are also more advanced things you could do like specifying a compression dictionary).
PIL exposes these options as optimize and compress_level under PNG options.
optimize
If present and true, instructs the PNG writer to make the
output file as small as possible. This includes extra
processing in order to find optimal encoder settings.
compress_level
ZLIB compression level, a number between 0 and 9: 1 gives
best speed, 9 gives best compression, 0 gives no
compression at all. Default is 6. When optimize option is
True compress_level has no effect (it is set to 9 regardless
of a value passed).
And seeing it in action...
from PIL import Image
img = Image.open("sample.png")
img.save("optimized.png", optimize=True)
The resulting image I get is about 60K smaller than the original.
❯ ls -lh optimized.png sample.png
-rw-r--r-- 1 wkl staff 843K Aug 12 18:10 optimized.png
-rw-r--r-- 1 wkl staff 918K Aug 12 17:23 sample.png
JPEG File
Now, JPEG is a generationally-lossy image format - as you save it over and over, you will keep losing quality - it doesn't matter if your subsequent generations save it at even higher qualities than the previous ones, you've lost data already from the previous saves.
Note that the likely reason why you saw file sizes balloon if you used quality=100 is because libjpeg/libjpeg-turbo (which are the underlying libraries used by PIL for JPEG) do not do certain things when the quality is set that high, I think it doesn't do quantization which is an important step in determining how many bits are needed to compress.

pixel encoding using PIL

I have a naive question, but after a long day, I am not still able to get my answer.
I am currently loading my png image using PIL, it works well. However, some of my png
images are 16-bit per pixel. I am trying desperately to query this information, but I am not able to get it, using PIL. Indeed, if I am simply using the file system binary it works.
$ file flower_16b.png
flower_16b.png: PNG image data, 660 x 600, 16-bit/color RGB, non-interlaced
However in my python code:
img = Image.open(filename, "r")
print(img.mode)
I get RGB. Following the documentation PIL RGB means (3x8-bit pixels, true color), it look likes the image has been casted. So does it exist a way to get the depth of an image, using PIL or an other python module ?
PIL/Pillow doesn't support 48-bit images like that. One option might be OpenCV but be aware it comes as BGR not RGB:
import cv2
# Read with whatever bit depth is specified in the image file
BGR = cv2.imread('image.png', cv2.IMREAD_ANYDEPTH|cv2.IMREAD_ANYCOLOR)
# Check dtype and number of channels
print(BGR.dtype, BGR.shape)
dtype('uint16'), (768, 1024, 3)
Another option may be pyvips, which works a slightly different way, but has some good benefits:
import pyvips
im = pyvips.Image.new_from_file('image.png', access="sequential")
print(im)
<pyvips.Image 1024x768 ushort, 3 bands, rgb16>
If you are really, really stuck and can't/won't install OpenCV or pyvips, you have a couple more options with ImageMagick...
You could reduce your 3 RGB channels (16-bits each) to 3 RGB channels (8-bits each) with:
magick input.png PNG24:output.png # then open "output.png" with PIL
Or, you could separate the 3 RGB channels into 3 separate 16-bit files and process them separately with PIL/Pillow:
magick input.png -separate channel-%d.png
and you will get the red channel as a 16-bit image in channel-0.png which you can open with PIL/Pillow, the green as channel-1.png and the blue as channel-2.png

Write additionnals images in a tiff file without erasing existing images

What I'm actually doing is saving images in a tiff file, using imageio.mimwrite(). But in my script, I open and close the file several times, so it erases existing images before saving news images. I want to keep existing images in the tiff file, and just add new images without erasing the previous ones. I did not find anything in the documentation which can helps me.
I'm actually using this :
imageio.mimwrite("example.tiff", image, format=".tiff")
image is an array which contains arrays of integers, each array representing an image.
This code opens example.tiff, erase existing images (if they exist), and write news images. But I want to add like open("file.txt", "a") does.
I made three differently sized TIFF images with ImageMagick like this for testing:
convert -size 640x480 xc:green green.tif
convert -size 1024x768 xc:blue blue.tif
convert -size 400x100 gradient:cyan-yellow gradient.tif
Then I used the tool tiffcp which is distributed with the TIFF library and the -a option to append the blue and gradient image to the green one like this:
tiffcp -a blue.tif gradient.tif green.tif
If I then check the contents of green.tiff with ImageMagick identify, I see it looks correct:
magick identify green.tif
green.tif[0] TIFF 640x480 640x480+0+0 16-bit sRGB 6.49355MiB 0.000u 0:00.000
green.tif[1] TIFF 1024x768 1024x768+0+0 16-bit sRGB 0.000u 0:00.000
green.tif[1] TIFF 400x100 400x100+0+0 16-bit sRGB 0.000u 0:00.000
And if I preview the file, all three images are there with the correct sizes and colours:
So, I am suggesting you consider using subprocess.run() to shell out to tiffcp.
With tifffile writing one page at a time (in the example a CTYX multipage), you can just write straight from a n-D array if you have enough RAM for it with tifffile.imwrite(filename,array)
https://pypi.org/project/tifffile/ :
import tifffile as tf
with tf.TiffWriter("filenametest.tiff",
#bigtiff=True,
#If you want to add on top of an existing tiff file (slower) uncomment below
#append = True,
imagej=False,) as tif:
for time in range(rgb.shape[1]):
tif.save(rgb[:,time,:,:].,
#compress= 3,
photometric='minisblack',
metadata= None,
contiguous=False,
)
tif.close()
With python-bioformats:
https://pythonhosted.org/python-bioformats/
bioformats.write_image(pathname, pixels, pixel_type, c=0, z=0, t=0, size_c=1, size_z=1, size_t=1, channel_names=None)[source]
if you have:
4 time points with
3 color zstacks (11 Z's)
with XY at 1024 pixels.
with independent numpy arrays being [3,11,1024,1024] (one for each timepoint),
16bits,
and named: a,b,c,d.
This should do the trick
import bioformats as bf
import numpy as np
#do something here to load a,b,c, and d
i=0
for t in [a,b,c,d]:
for c in range(3):
for z in range(11):
#t here is the numpy array
bf.write_image('/path/to/file.tiff'
, t
, bf.PT_UINT16
, c = c, z = z , t = i, size_c= 3, size_z=11, size_t=4
)
i+=1

How can one specify dpi when saving image as tif via scipy.misc.imsave?

I am taking a screen-shot (PNG format) resizing it, and writing it back out in TIF format, via scipy.misc module (imread, imresize, imsave functions). The TIF format image is to be fed into Tesseract-OCR. However, Tesseract is complaining that the dpi specified in the TIF file's metadata is 0. How can one specify this when saving the image via scipy.misc.imsave or any other method?
Without analyzing where your problems exactly come from, the approach of Mark (maybe that's enough for you; maybe not; i can imagine there is something else in your code which might be the reason) can be emulated by using Pillow (and i don't see an option for this within scipy's wrapper).
Actually, instead of rewriting tags as he does, we care about these while doing our original task. In practice both approaches should be okay.
With a very high probability, scipy is already using Pillow under the hood (Note that Pillow (https://python-pillow.org/) is not a dependency of SciPy, but the image manipulation functions indicated in the list below are not available without it.; this list contains imsave).
from scipy.misc import ascent # test image
import PIL.Image
scipy_img = ascent().astype('uint8')
arr2im = PIL.Image.fromarray(scipy_img)
arr2im.save('test.tif', format='TIFF',
dpi=(100., 100.), # there still seems to be a bug when using int's here
compression='tiff_lzw',)
Checking with exiftool:
ExifTool Version Number : 10.63
File Name : test.tif
...
Image Width : 512
Image Height : 512
Bits Per Sample : 8
Compression : LZW
...
X Resolution : 100
Y Resolution : 100
...
Resolution Unit : inches
Image Size : 512x512
Megapixels : 0.262
Please file this one under "any other method" :-)
You can set the resolution with exiftool like this:
exiftool SomeImage.tif -xresolution=300 -yresolution=300 -resolutionunit=inches
Check it with ImageMagick:
identify -verbose SomeImage.tif
Image: SomeImage.tif
Format: TIFF (Tagged Image File Format)
Mime type: image/tiff
Class: DirectClass
Geometry: 100x100+0+0
Resolution: 300x300
Print size: 0.333333x0.333333
...
...
I am suggesting you shell out to run this command with os.system().
A Python wrapper exists, but I have never used it and cannot vouch for it.

Categories

Resources