Convert rgb to black and white in python - python

I want to use Python to convert a color image into a black and white image, but I should not use the library. But opening a file with a library is allowed, but converting it to black and white should not be done with a library. I know the CV2 library does this, but I want it to do without the library.

Assuming the image can be loaded as list in with a size of [x,y,3], where the last dimension contains the 3 color bytes as separate elements.
from PIL import Image
import numpy as np
raw_img = Image.open("your_image_here")
img = raw_img.load()
x,y = raw_img.size
threshold = 300
bw_img = [[0]*y]*x # blank image
for i in range(x):
for j in range(y):
if img[i,j] < threshold:
bw_img[i][j] = 0
else:
bw_img[i][j] = 1
Image.fromarray(np.asarray(bw_img),mode=1).save("your_nwe_image.bmp")
An image can be treated as 2D array/list with and additional dimension for the color values of each channel RGB.
This checks for every pixel whether the sum of the color values is above a threshold, and assigns the pixel as white. How you want to determine if a pixel will be white exactly or not is up to you.
But honestly doing this without a library is not really useful, even numpy is much faster and efficient for such a task.

Related

Using python to fill an image with one color in steps from 0% to 100%

I have a black image that I need to fill with a new color.
I want to generate new images starting from 1% to 100% (generating an
image for every 1% filled).
Examples for 4 fill-ratios
Heart image filled with 1%, 5%, 10% and 15%
Research I did
I did a lot of research on the internet and the closest I came was this link:
Fill an image with color but keep the alpha (Color overlay in PIL)
However, as I don't have much experience with Python for image editing, I couldn't move forward or modify the code as needed.
Edit:
I was trying with this code from the link
from PIL import Image
import numpy as np
# Open image
im = Image.open('2746646.png')
# Make into Numpy array
n = np.array(im)
# Set first three channels to red
n[..., 0:3] = [ 255, 0, 0 ]
# Convert back to PIL Image and save
Image.fromarray(n).save('result.png')
But it only generates a single image (as if it were 100%, I need 100 images with 1% filled in each one).
Updated Answer
Now you have shared your actual starting image, it seems you don't really want to replace black pixels, but actually opaque pixels. If you split your image into its constituent RGBA channels and lay them out left-to-right R,G,B then A, you can see you want to fill where the alpha (rightmost) channel is white, rather than where the RGB channels are black:
That changes the code to this:
#!/usr/bin/env python3
from PIL import Image
import numpy as np
# Load image, ensure not palettised, and make into Numpy array
im = Image.open('muscle.png').convert('RGBA')
# Make Numpy array
RGBA = np.array(im)
# Get RGB part
RGB = RGBA[..., :3]
# Get greyscale version of image as Numpy array
alpha = RGBA[..., 3]
# Find X,Y coordinates of all black pixels in image
blkY, blkX = np.where(alpha==255)
# Just take one entry per row, even if multiple black pixels in it
uniqueRows = np.unique(blkY)
# How many rows are there with black pixels in?
nUniqueRows = len(uniqueRows)
for percent in range(2,101):
# Work out filename based on percentage
filename = f'result-{percent:03d}.png'
# How many rows do we need to fill?
nRows = int(nUniqueRows * percent/100.0)
# Which rows are they? Negative index because filling bottom-up.
rows = uniqueRows[-nRows:]
print(f'DEBUG: filename: {filename}, percent: {percent}, nRows: {nRows}, rows: {rows}')
# What are the indices onto blkY, blkX ?
indices = np.argwhere(np.isin(blkY, rows))
# Make those pixels black
RGB[blkY[indices.ravel()], blkX[indices.ravel()], :3] = [0,255,0]
res = Image.fromarray(RGBA).save(filename)
Original Answer
That was fun! This seems to work - though it's not that efficient. It is not a true "floodfill", see note at end.
#!/usr/bin/env python3
from PIL import Image
import numpy as np
# Load image, ensure not palettised, and make into Numpy array
im = Image.open('heart.png').convert('RGB')
# Make Numpy array
na = np.array(im)
# Get greyscale version of image as Numpy array
grey = np.array(im.convert('L'))
# Find X,Y coordinates of all black pixels in image
blkY, blkX = np.where(grey==0)
# Just take one entry per row, even if multiple black pixels in it
uniqueRows = np.unique(blkY)
# How many rows are there with black pixels in?
nUniqueRows = len(uniqueRows)
for percent in range(1,101):
# Work out filename based on percentage
filename = f'result-{percent:03d}.png'
# How many rows do we need to fill?
nRows = int(nUniqueRows * percent/100.0)
# Which rows are they? Negative index because filling bottom-up.
rows = uniqueRows[-nRows:]
# print(f'DEBUG: filename: {filename}, percent: {percent}, nRows: {nRows}, rows: {rows}')
# What are the indices onto blkY, blkX ?
indices = np.argwhere(np.isin(blkY, rows))
# Make those pixels green
na[blkY[indices.ravel()], blkX[indices.ravel()], :] = [0,255,0]
res = Image.fromarray(na).save(filename)
Note that this isn't actually a true "flood fill" - it is more naïve than that - because it doesn't seem necessary for your image. If you add another shape, it will fill that too:

Simplest way to convert a low-resolution black and white picture to a matrix

I have a set of very low-resolution pictures (in .png but I can easily convert them to something else). They all only have black or white pixels, like QR codes.
What I want is to be able to read them as binary matrix (a 1 for a black pixel and a zero for a white one).
I don't need anything more fancy than that, what should I use?
Hi you can use PIL to read the image, and then numpy to convert it to a matrix
from PIL import Image
import numpy as np
im = Image.read("imageName.ext")
im_mat = np.asarray(im)
Alternatively you can do all in one step with opencv
import cv2
img = cv2.imread("imageName.ext")
in both cases you will have a matrix with size WxHxC with H the height in pixels, W the widht and c the number of channels (3 or 4 depending if there's an alpha for transparency).
If your image is black and white and you only want a matrix with size WxH take one channel with
img = img_mat[:,:,0] #8-bit matrix
and last you can binarize that givving an umbral or just by comparing
bin = img> 128
or
bin = img == 255
I corrected this last line I had a typo in it

How to downscale an image without losing discrete values?

I have an image of a city with discrete colors (Green=meadow, black=buildings, white/yellow=roads). Using Pillow, I import the picture in my (Python) program and convert it to a Numpy array with discrete values for the colors (i.e. green pixels become 1's, black pixels become 2's, etc).
I want to downscale the resolution of the image (for computational purposes) while retaining as much information as possible. However, using Pillow's resize() method, colors deviate from these discrete values. How can I downscale this image while (most importantly) retaining the discrete colors and (also important) with losing as little information as possible?
Here an example of the image: https://i.imgur.com/6Tef55H.png
EDIT: per request, some code:
from PIL import Image
import Numpy as np
picture = Image.open(some_image.png)
width, height = picture.size
pic_array = np.zeros(width,height)
# Turn the image into discrete values
for i in range(0,width):
for j in range(0,height):
red, green, blue = picture.getpixel((i,j))
if red == a and green == b and blue == c:
#An example of how discrete colors are converted to values
pic_array[i][j] = 1
Scaling can be done in two ways:
1) Scaling the original image using Pillow's resize library or
2) rescaling the final array using something like:
scaled_array = pic_array[0:width:5, 0:height,5]
Option 1 is "well" in terms of retaining information but loses discrete values, while option 2 does it the other way around.
I was interested in this question and wrote some code to try out some ideas - specifically the "mode" filter suggested by #jasonharper in the comments. So, I programmed it up.
First of all the input image is not 4 nicely defined classes, but actually has 6,504 different colours, so I made a palette of 4 colours using ImageMagick like this:
magick xc:black xc:white xc:yellow xc:green +append palette.png
Here it is enlarged - in reality is 4x1 pixels:
Then I mapped the colours in the image to the palette of 4 discrete colours:
magick map.png +dither -remap palette.png start.png
Then I tried this code to calculate the median and the mode of each 3x3 window:
#!/usr/bin/env python3
from PIL import Image
import numpy as np
from scipy import stats
from skimage.util import view_as_blocks
# Open image and make into Numpy array
im = Image.open('start.png')
na = np.array(im)
# Make a view as 3x3 blocks - crop anything not a multiple of 3
block_shape=(3,3)
view = view_as_blocks(na[:747,:], block_shape)
flatView = view.reshape(view.shape[0], view.shape[1], -1) # now (249,303,9)
# Get median of each 3x3 block
resMedian = np.median(flatView, axis=2).astype(np.uint8)
Image.fromarray(resMedian*60).save('resMedian.png') # arbitrary scaling by 60 for contrast
# Get mode of each 3x3 block
resMode = stats.mode(flatView, axis=2)[0].reshape((249,303)).astype(np.uint8)
Image.fromarray(resMode*60).save('resMode.png') # arbitrary scaling by 60 for contrast
Here is the result of the median filter:
And here is the result of the "mode" filter which is indeed better IMHO:
Here is animated comparison:
If anyone wants to take the code and adapt it to try new ideas, please feel free!

How to get color of all pixels in an area in python

I'm trying to write a program on linux that does something if the pixels in an area aren't all the same color, for example:
if color not "255, 255, 255":
#do something
this is what i have for one pixel:
import time, pyautogui
time.clock()
image = pyautogui.screenshot()
color = image.getpixel((1006, 553))
print(time.clock())
print(color)
I know how to get the color of a pixel using .getpixel() but that only gets one pixel
Basically, how do i get the color of an area of pixels when i know all the pixels in that area are the same color.
Also, as quick as possible, like 0.5s or under.
I keep recommending it, but the scikit-image library is pretty great, and they have some really solid documentation and examples. I would recommend a combo of that and using numpy arrays directly. It is just a lot faster when working directly with pixels.
You will have to convert the PIL image to a numpy array...but this should work with that:
import pyautogui
import numpy as np
image = pyautogui.screenshot()
np_image = np.array(image)
You can slice the image:
red_slice = np_image[0:50, 0:50,0]
red_mask = red_slice == 200
This would give you the values for red in the upper right 50x50 pixel area. red_mask is an array of True/False values whether each red value in that area is equal to 200. This can be repeated for the other channels as you see fit.

Python Imaging Library- Merging one type of pixel into another image

Basically, I have two images. One is comprised of white and black pixels, the black pixels making up a word, and the other image that I'm trying to paste the black pixels on top of. I've pasted the code below, however I'm aware that there's an issue with the "if pixels [x,y] == (0, 0, 0):' being a tuple and not an indice, however I'm uncertain of how to get it to look for black pixels with other means.
So essentially I need to find, and remember the positions of, the black pixels so that I can paste them onto the first image. Any help is very much appreciated!
image_one = Image.open (image_one)
image_two = Image.open (image_two)
pixels = list(image_two.getdata())
for y in xrange(image_two.size[1]):
for x in xrange(image_two.size[0]):
if pixels[x,y] == (0, 0, 0):
pixels = black_pixels
black_pixels.append()
image = Image.open (image_one);
image_one.putdata(pixels)
image.save(image_one+ "_X.bmp")
del image_one, image_two;
You're almost there. I am not too familiar with the PIL class, but instead of calling the getdata method, let's use getpixel directly on the image object, and directly set the results into the output image. That eliminates the need to store the set of pixels to overwrite. However, there may be cases beyond what you've listed here where such an approach would be necessary. I created a random image and then set various pixels to black. For this test I used a different condition - if the R channel of the image is greater than 50. You can comment that out and use the other test, which tests for tuple (R,G,B) == (0,0,0) which will work fine.
imagea = PIL.Image.open('temp.png')
imageb = PIL.Image.open('temp.png')
for y in xrange(imagea.size[1]):
for x in xrange(imagea.size[0]):
currentPixel = imagea.getpixel((x,y))
if currentPixel[0] > 50:
#if currentPixel ==(0,0,0):
#this is a black pixel, you can directly modify image 2 now
imageb.putpixel((x,y),(0,0,0))
imageb.save('outputfile.png')
An alternative way to do this is just to multiply the two images together. Any pixel that's black in the binary image will be black in the result (multiply by zero) and any pixel that's white in the binary image will be unchanged from the other image in the result (multiply by one).
PIL can do this,
from PIL import Image, ImageChops
image_one = Image.open("image_one.bmp")
image_two = Image.open("image_two.bmp")
out = ImageChops.multiply(image_one, image_two)
out.save("output.bmp")

Categories

Resources