I am trying to convert an image from PNG to EPS using Pillow. The following code gives an error:
from PIL import Image
Image.open("Image1.png").save("Image1.eps", fmt='EPS')
Which reads:
Traceback (most recent call last):
File "C:/Users/pbreach/Dropbox/Personal/FigureConversion/convert.py", line 15, in <module>
convert_image(in_name, out_name, fmt='EPS')
File "C:/Users/pbreach/Dropbox/Personal/FigureConversion/convert.py", line 4, in convert_image
Image.open(in_name).save(out_name, fmt)
File "C:\Users\pbreach\Continuum\Anaconda3\lib\site-packages\PIL\Image.py", line 1826, in save
save_handler(self, fp, filename)
File "C:\Users\pbreach\Continuum\Anaconda3\lib\site-packages\PIL\EpsImagePlugin.py", line 362, in _save
raise ValueError("image mode is not supported")
ValueError: image mode is not supported
Is EPS really not supported? In the documentation EPS is second on the list of fully supported formats. Is there anything that I need to do if this is not the case?
Weirdly enough, if I do Image.open("Image1.png").save("Image1.jpg", fmt='EPS') it works but saves to JPG.
Pillow supports EPS, but cannot write it with alpha channel (RGBA, LA)
https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html?highlight=eps#eps:
Pillow identifies EPS files containing image data, and can read files
that contain embedded raster images (ImageData descriptors). If
Ghostscript is available, other EPS files can be read as well. The EPS
driver can also write EPS images. The EPS driver can read EPS images
in L, LAB, RGB and CMYK mode, but Ghostscript may convert the images
to RGB mode rather than leaving them in the original color space. The
EPS driver can write images in L, RGB and CMYK modes.
Helped for me to convert the image to RGB mode before saving
from PIL import Image
fig = Image.open("Image1.png")
if fig.mode in ('RGBA', 'LA'):
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html?highlight=eps#eps
print('Current figure mode "{}" cannot be directly saved to .eps and should be converted (e.g. to "RGB")'.format(fig.mode))
fig = fig.convert('RGB')
out_fig = "Image1.eps"
fig.save(out_fig)
fig.close()
But sometimes I had problems: got black background in .eps instead of transparent .png. For me helped remove_transparency() function from https://stackoverflow.com/a/35859141/7444782 to substitute the transparent background to a specified color (white by default)
from PIL import Image
def remove_transparency(im, bg_color=(255, 255, 255)):
"""
Taken from https://stackoverflow.com/a/35859141/7444782
"""
# Only process if image has transparency (http://stackoverflow.com/a/1963146)
if im.mode in ('RGBA', 'LA') or (im.mode == 'P' and 'transparency' in im.info):
# Need to convert to RGBA if LA format due to a bug in PIL (http://stackoverflow.com/a/1963146)
alpha = im.convert('RGBA').split()[-1]
# Create a new background image of our matt color.
# Must be RGBA because paste requires both images have the same format
# (http://stackoverflow.com/a/8720632 and http://stackoverflow.com/a/9459208)
bg = Image.new("RGBA", im.size, bg_color + (255,))
bg.paste(im, mask=alpha)
return bg
else:
return im
fig = Image.open("Image1.png")
if fig.mode in ('RGBA', 'LA'):
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html?highlight=eps#eps
print('Current figure mode "{}" cannot be directly saved to .eps and should be converted (e.g. to "RGB")'.format(fig.mode))
fig = remove_transparency(fig)
fig = fig.convert('RGB')
out_fig = "Image1.eps"
fig.save(out_fig)
fig.close()
It might be that you have a png with an alpha channel. EPS in PIL does not support transparency in raster images.
So if you remove the alpha channel by im[:,:,0:2] it might just work like a charm.
It will be more than one line, however.
Related
I am trying to process an image file into something that can be displayed on a Black/White/Red e-ink display, but I am running into a problem with the output resolution.
Based on the example code for the display, it expects two arrays of bytes (one for Black/White, one for Red), each 15,000 bytes. The resolution of the e-ink display is 400x300.
I'm using the following Python script to generate two BMP files: one for Black/White and one for Red. This is all working, but the file sizes are 360,000 bytes each, which won't fit in the ESP32 memory. The input image (a PNG file) is 195,316 bytes.
The library I'm using has a function called EPD_4IN2B_V2_Display(BLACKWHITEBUFFER, REDBUFFER);, which wants the full image (one channel for BW, one for Red) to be in memory. But, with these image sizes, it won't fit on the ESP32. And, the example uses 15KB for each color channel (BW, R), so I feel like I'm missing something in the image processing necessary to make this work.
Can anyone shed some light on what I'm missing? How would I update the Python image-processing script to account for this?
I am using the Waveshare 4.2inch E-Ink display and the Waveshare ESP32 driver board. A lot of the Python code is based on this StackOverflow post but I can't seem to find the issue.
import io
import traceback
from wand.image import Image as WandImage
from PIL import Image
# This function takes as input a filename for an image
# It resizes the image into the dimensions supported by the ePaper Display
# It then remaps the image into a tri-color scheme using a palette (affinity)
# for remapping, and the Floyd Steinberg algorithm for dithering
# It then splits the image into two component parts:
# a white and black image (with the red pixels removed)
# a white and red image (with the black pixels removed)
# It then converts these into PIL Images and returns them
# The PIL Images can be used by the ePaper library to display
def getImagesToDisplay(filename):
print(filename)
red_image = None
black_image = None
try:
with WandImage(filename=filename) as img:
img.resize(400, 300)
with WandImage() as palette:
with WandImage(width = 1, height = 1, pseudo ="xc:red") as red:
palette.sequence.append(red)
with WandImage(width = 1, height = 1, pseudo ="xc:black") as black:
palette.sequence.append(black)
with WandImage(width = 1, height = 1, pseudo ="xc:white") as white:
palette.sequence.append(white)
palette.concat()
img.remap(affinity=palette, method='floyd_steinberg')
red = img.clone()
black = img.clone()
red.opaque_paint(target='black', fill='white')
black.opaque_paint(target='red', fill='white')
red_image = Image.open(io.BytesIO(red.make_blob("bmp")))
black_image = Image.open(io.BytesIO(black.make_blob("bmp")))
red_bytes = io.BytesIO(red.make_blob("bmp"))
black_bytes = io.BytesIO(black.make_blob("bmp"))
except Exception as ex:
print ('traceback.format_exc():\n%s',traceback.format_exc())
return (red_image, black_image, red_bytes, black_bytes)
if __name__ == "__main__":
print("Running...")
file_path = "testimage-tree.png"
with open(file_path, "rb") as f:
image_data = f.read()
red_image, black_image, red_bytes, black_bytes = getImagesToDisplay(file_path)
print("bw: ", red_bytes)
print("red: ", black_bytes)
black_image.save("output/bw.bmp")
red_image.save("output/red.bmp")
print("BW file size:", len(black_image.tobytes()))
print("Red file size:", len(red_image.tobytes()))
As requested, and in the event that it may be useful for future reader, I write a little bit more extensively what I've said in comments (and was verified to be indeed the reason of the problem).
The e-ink display needs usually a black&white image. That is 1 bit per pixel image. Not a grayscale (1 channel byte per pixel), even less a RGB (3 channels/bytes per pixel).
I am not familiar with bi-color red/black displays. But it seems quite logical that it behave just like 2 binary displays (one black & white display, and one black-white & red display). Sharing the same location.
What your code seemingly does is to remove all black pixels from a RGB image, and use it as a red image, and remove all red pixels from the same RDB image, and use it as a black image. But since those images are obtained with clone they are still RGB images. RGB images that happen to contain only black and white pixels, or red and white pixels, but still RGB image.
With PIL, it is the mode that control how images are represented in memory, and therefore, how they are saved to file.
Relevant modes are RGB, L (grayscale aka 1 linear byte/channel per pixel), and 1 (binary aka 1 bit per pixel).
So what you need is to convert to mode 1. Usind .convert('1') method on both your images.
Note that 400x300×3 (uncompressed rgb data for your image) is 360000, which is what you got. 400×300 (L mode for same image) is 120000, and 400×300/8 (1 mode, 1 bit/pixel) is 15000, which is precisely the expected size as you mentioned. So that is another confirmation that, indeed, 1 bit/pixel image is expected.
I try to use OCR (Optical Character Reader) for a lot of documents of the same type. I use pdf2image library for Python. But when it sees pdfs with AutoCAD shx text it captures the bounding boxes around text as well. At first they are not visible on pdf. You need to click on text to see the boxes. But they appear in jpg result after conversion.
Here's an image of a part of pdf document:
crop from actual pdf
And here's the output of conversion: pdf after conversion
I expect somethink like that:
pdf after conversion how it should be
Here's my function for pdf2image conversion:
def get_image_for_blueprint(path, dpi):
"""Create image for full blueprint from path
path: path to pdf file
"""
from pdf2image import convert_from_bytes
images = convert_from_bytes(open(path, 'rb').read(), dpi=dpi) # Actual conversion function
for i in images:
width, height = i.size
if width < height:
continue
else:
print(i.size) # tuple : (width, height)
image = i.resize((4096, int(height / width * 4096)))
enhancer = ImageEnhance.Sharpness(image)
image = enhancer.enhance(2) # Sharpness
enhancer = ImageEnhance.Color(image)
image = enhancer.enhance(0) # black and white
enhancer = ImageEnhance.Contrast(image)
image = enhancer.enhance(2) # Contrast
image = np.asarray(image) # array
image = image.astype(np.uint8)
return image
I found solutions to be made in AutoCAD before saving the document but I can not really find a way to get rid of these boxes having pdf only.(in python or c++)
Maybe it's possible to resolve using any other programming language library or additional software.
Can someone please explain why do I get this inconsistency in rgb values after saving the image.
import imageio as io
image = 'img.jpg'
type = image.split('.')[-1]
output = 'output' + type
img = io.imread(image)
print(img[0][0][1]) # 204
img[0][0][1] = 255
print(img[0][0][1]) # 255
io.imwrite(output, img, type, quality = 100)
imgTest = io.imread(output)
print(imgTest[0][0][1]) # 223
# io.help('jpg')
Image used = img.jpg
The reason that pixels are changed when loading a jpeg image and then saving it as a jpeg again is that jpeg uses lossy compression. To save storage space for jpeg images, pixel values are saved in a dimension-reduced representation. You can find some information about the specific algorithm here.
The advantage of lossy compression is that the image size can significantly be reduced, without the human eye noticing any changes. However, without any additional methods, we will not retrieve the original image after saving it in jpg format.
An alternative that does not use lossy compression is the png format, which we can verify by converting your example image to png and runnning the code again:
import imageio as io
import numpy as np
import matplotlib.pyplot as plt
image = '/content/drive/My Drive/img.png'
type = image.split('.')[-1]
output = 'output' + type
img = io.imread(image)
print(img[0][0][1]) # 204
img[0][0][1] = 255
print(img[0][0][1]) # 255
io.imwrite(output, img, type)
imgTest = io.imread(output)
print(imgTest[0][0][1]) # 223
# io.help('jpg')
Output:
204
255
255
We can also see that the png image takes up much more storage space than the jpg image
import os
os.path.getsize('img.png')
# output: 688444
os.path.getsize('img.jpg')
# output: 69621
Here is the png image:
there is a defined process in the imageio
imageio reads in a structure of RGB, if you are trying to save it in the opencv , you need to convert this RGB to BGR. Also, if you are plotting in a matplotlib, it varies accordingly.
the best way is,
Read the image in imageio
convert RGB to BGR
save it in the opencv write
I'm creating a simple GIF animation using PIL:
from PIL import Image, ImageDraw, ImageFont
images = []
for x, i in enumerate(range(10)):
image = Image.new(mode="RGB", size=(320, 60), color="orange")
draw = ImageDraw.Draw(image)
fnt = ImageFont.truetype('font.ttf', size=10)
draw.text((10, 10), ("%s" % x), fill=(0, 0, 255), font=fnt)
images.append(image)
images[0].save("result/pil.gif", save_all=True, append_images=images[1:], duration=1000, loop=0, format="GIF")
The problem is that whenever I use Draw.text, image's background is getting some kind of white noze:
I found some info that I have to use getpalette from the first frame and putpalette for all the other frames like this:
for x, i in enumerate(range(10)):
image = Image.new(mode="RGB", size=(320, 60), color="orange")
if x == 0:
palette = image.getpalette()
else:
image.putpalette(palette)
But it just gives me: ValueError: illegal image mode.
What's the reason of the noizy background and how can I fix it?
UPD I was able to fix the background by changing image mode to "P", but in this case my fonts became unreadable. These are examples with RGB mode (fonts are well) and P mode (fonts are awful):
Why am I getting either nice background or nice fonts but not both? Is there a workaround?
This is dithering that happens, because gif can contain only colors from palette of size 256. Most likely PIL uses very basic algorithm to convert from RGB format to indexed format, which is required by gif. As your image contains colors #ff9900 and #ffcc00, then palette presumably consists of hex values 00, 33, 66, 99, cc, ff for each byte and has size 6x6x6 = 216, which fits nicely into 256 possible values. 'orange' has value of #ffa500 and can't be represented by such palette, so the background is filled by nearest available colors.
You can try to use color '#ff9900' instead of 'orange'. Hopefully this color can be represented by palette, as it is present in your noisy image.
You can also try to convert from RGB to indexed format using your own palette as an argument in quantize method, as suggested in this answer. Adding the following line results in nice solid background:
image = image.quantize(method=Image.MEDIANCUT)
Or you can just save RGB image with PNG format. In this case it will be saved as APNG image.
images[0].save("pil.png", save_all=True, append_images=images[0:],duration=1000, loop=0, format="PNG")
user13044086 has given a generalised version of the problem, the specifics are that a gif is a palletised format, to convert your original RGB to a palette pillow needs to restrict it from "true color" to just 256 colors.
To do that, it will convert the image to mode L, and as you can see in the documentation if no transformation matrix is given that's just an alias for calling quantize with the default parameters. The relevant default parameter here is dither, meaning if the image has more than 256 colors try to "emulate" missing ones by having individual dots of nearby colors.
Now you might complain that you don't have more than 256 colors, but you probably do due to font antialiasing, and algorithms don't necessarily have "taste" so instead of dithering in the letter, it dithers very visibly in the background.
You can mitigate this issue by explicitly quantizing providing an explicit method as suggested, or just disabling dithering (which will probably yield lower-quality results but will certainly be faster):
images.append(image.quantize(dither=Image.NONE))
Manually crafting your own palette and passing that to quantize could also work.
I am using the Python Pillow lib to change an image before sending it to device.
I need to change the image to make sure it meets the following requirements
Resolution (width x height) = 298 x 144
Grayscale
Color Depth (bits) = 4
Format = .png
I can do all of them with the exception of Color Depth to 4 bits.
Can anyone point me in the right direction on how to achieve this?
So far, I haven't been able to save 4-bit images with Pillow. You can use Pillow to reduce the number of gray levels in an image with:
import PIL.Image as Image
im = Image.open('test.png')
im1 = im.point(lambda x: int(x/17)*17)
Assuming test.png is a 8-bit graylevel image, i.e. it contains values in the range 0-255 (im.mode == 'L'), im1 now only contains 16 different values (0, 17, 34, ..., 255). This is what ufp.image.changeColorDepth does, too. However, you still have a 8-bit image. So instead of the above, you can do
im2 = im.point(lambda x: int(x/17))
and you end up with an image that only contains 16 different values (0, 1, 2, ..., 15). So these values would all fit in an uint4-type. However, if you save such an image with Pillow
im2.save('test.png')
the png will still have a color-depth of 8bit (and if you open the image, you see only really dark gray pixels). You can use PyPng to save a real 4-bit png:
import png
import numpy as np
png.fromarray(np.asarray(im2, np.uint8),'L;4').save('test4bit_pypng.png')
Unfortunately, PyPng seems to take much longer to save the images.
using changeColorDepth function in ufp.image module.
import ufp.image
import PIL
im = PIL.Image.open('test.png')
im = im.convert('L') # change to grayscale image
im.thumbnail((298, 144)) # resize to 294x144
ufp.image.changeColorDepth(im, 16) # change 4bits depth(this function change original PIL.Image object)
#if you will need better convert. using ufp.image.quantizeByImprovedGrayScale function. this function quantized image.
im.save('changed.png')
see example : image quantize by Improved Gray Scale. [Python]