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.
The aim is to take a coloured image, and change any pixels within a certain luminosity range to black. For example, if luminosity is the average of a pixel's RGB values, any pixel with a value under 50 is changed to black.
I’ve attempted to begin using PIL and converting to grayscale, but having trouble trying to find a solution that can identify luminosity value and use that info to manipulate a pixel map.
There are many ways to do this, but the simplest and probably fastest is with Numpy, which you should get accustomed to using with image processing in Python:
from PIL import Image
import numpy as np
# Load image and ensure RGB, not palette image
im = Image.open('start.png').convert('RGB')
# Make into Numpy array
na = np.array(im)
# Make all pixels of "na" where the mean of the R,G,B channels is less than 50 into black (0)
na[np.mean(na, axis=-1)<50] = 0
# Convert back to PIL Image to save or display
result = Image.fromarray(na)
result.show()
That turns this:
Into this:
Another slightly different way would be to convert the image to a more conventional greyscale, rather than averaging for the luminosity:
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Calculate greyscale version
grey = im.convert('L')
# Point process over pixels to make mask of darker ones
mask = grey.point(lambda p: 255 if p<50 else 0)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=mask)
Notice that the blue channel is given considerably less significance in the ITU-R 601-2 luma transform that PIL uses (see the lower 114 weighting for Blue versus 299 for Red and 587 for Green) in the formula:
L = R * 299/1000 + G * 587/1000 + B * 114/1000
so the blue shades are considered darker and become black.
Another way would be to make a greyscale and a mask as above. but then choose the darker pixel at each location when comparing the original and the mask:
from PIL import Image, ImageChops
im = Image.open('start.png').convert('RGB')
grey = im.convert('L')
mask = grey.point(lambda p: 0 if p<50 else 255)
res = ImageChops.darker(im, mask.convert('RGB'))
That gives the same result as above.
Another way, pure PIL and probably closest to what you actually asked, would be to derive a luminosity value by averaging the channels:
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Calculate greyscale version by averaging R,G and B
grey = im.convert('L', matrix=(0.333, 0.333, 0.333, 0))
# Point process over pixels to make mask of darker ones
mask = grey.point(lambda p: 255 if p<50 else 0)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=mask)
Another approach could be to split the image into its constituent RGB channels, evaluate a mathematical function over the channels and mask with the result:
from PIL import Image, ImageMath
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Split into RGB channels
(R, G, B) = im.split()
# Evaluate mathematical function over channels
dark = ImageMath.eval('(((R+G+B)/3) <= 50) * 255', R=R, G=G, B=B)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=dark)
I created a function that returns a list with True if the pixel has a luminosity of less than a parameter, and False if it doesn't. It includes an RGB or RGBA option (True or False)
def get_avg_lum(pic,avg=50,RGBA=False):
num=3
numd=4
if RGBA==False:
num=2
numd=3
li=[[[0]for y in range(0,pic.size[1])] for x in range(0,pic.size[0])]
for x in range(0,pic.size[0]):
for y in range(0,pic.size[1]):
if sum(pic.getpixel((x,y))[:num])/numd<avg:
li[x][y]=True
else:
li[x][y]=False
return(li)
a=get_avg_lum(im)
The pixels match in the list, so (0,10) on the image is [0][10] in the list.
Hopefully this helps. My module is for standard PIL objects.
I'm trying to make a simple code that loads an image, divide the value of each pixel by 2 and stores the image. The image is stored in an array [1280][720][3]. After changing the value of each pixel I've chequed that the values are the expected. For some reason the values are correct but when I store the new image and check it, the values of the pixels are not the same as before...
The image is 1280x720 pixels and each pixel has 3 bytes (one for each color rgb)
import matplotlib.image as mpimg
img = mpimg.imread('image.jpg') # (1280, 720, 3)
myImg = []
for row in img:
myRow = []
for pixel in row:
myPixel = []
for color in pixel:
myPixel.append(color // 2)
myRow.append(myPixel)
myImg.append(myRow)
mpimg.imsave("foo.jpg", myImg)
img is a numpy array, so you can just use img / 2. It's also much faster than using a list loop.
myImg = img / 2
mpimg.imsave("foo.jpg", myImg)
I'm pretty new to image processing and python so bear with me
I'm trying to take a big image (5632x2048) which is basically a map of the world with provinces (ripped from Hearts of Iron 4), and each province is colored a different RGB value, and color it with a set of colors, each corresponding to a certain country. I'm currently using this code
import numpy as np
import cv2
import sqlite3
dbPath = 'PATH TO DB'
dirPath = 'PATH TO IMAGE'
con = sqlite3.connect(dbPath)
cur = con.cursor()
im = cv2.imread(dirPath)
cur.execute('SELECT * FROM Provinces ORDER BY id')
provinceTable = cur.fetchall()
for line in provinceTable:
input_rgb = [line[1], line[2], line[3]]
if line[7] == None:
output_rgb = [255,255,255]
else:
output_rgb = line[7].replace('[', '').replace(']','').split(',')
im[np.all(im == (int(input_rgb[0]), int(input_rgb[1]), int(input_rgb[2])), axis=-1)] = (int(output_rgb[0]), int(output_rgb[1]), int(output_rgb[2]))
cv2.imwrite('result.png',im)
The problem I'm running into is that it's painfully slow (50 minutes in and it hasn't finished), due to the fact I'm definitely using numpy wrong by looping through it instead of vectorizing (a concept I'm still new to and have no idea how to do). Google hasn't been very helpful either.
What's the best way to do this?
Edit: forgot to mention that the amount of values I'm replacing is pretty big (~15000)
As I mentioned in the comments, I think you'll want to use np.take(yourImage, LUT) where LUT is a Lookup Table.
So, if you make a dummy image the same shape as yours:
import numpy as np
# Make a dummy image of 5632x2048 RGB values
im = np.random.randint(0,256,(5632,2048,3), np.uint8)
that will be 34MB. Now reshape it to a tall vector of RGB values:
# Make image into a tall vector, as tall as necessary and 3 RGB values wide
v = im.reshape((-1,3))
which will be of shape (11534336, 3) and then flatten that to 24-bit values rather than three 8-bit values with np.dot()
# Make into tall vector of shape 11534336x1 rather than 11534336x3
v24 = np.dot(v.astype(np.uint32),[1,256,65536])
You will now have a 1-D vector of 24-bit pixel values with shape (11534336,)
Now create your RGB lookup table (I am making all 2^24 RGB entries here, you may need less):
RGBLUT = np.zeros((2**24,3),np.uint8)
And set up the LUT. So, supposing you want to map all colours in the original image to mid-grey (128) in the output image:
RGBLUT[:] = 128
Now do the np.dot() thing just the same as we did with the image so we get a LUT with shape (224,1) rather than shape (224,3):
LUT24 = np.dot(RGBLUT.astype(np.uint32), [1,256,65536])
Then do the actual lookup in the table:
result = np.take(LUT24, v24)
On my Mac, that take 334ms for your 5632x2048 image.
Then reshape and convert back to three 8-bit values by shifting and ANDing to undo effect of np.dot().
I am not currently in a position to test the re-assembly, but it will look pretty much like this:
BlueChannel = result & 0xff # Blue channel is bottom 8 bits
GreenChannel = (result>>8) &0 xff # Green channel is middle 8 bits
RedChannel = (result>>16) &0 xff # Red channel is top 8 bits
Now combine those three single channels into a 3-channel image:
RGB = np.dstack(RedChannel, GreenChannel, BlueChannel))
And reshape back from tall vector to dimensions of original image:
RGB = RGB.reshape(im.shape)
As regards setting up the LUT, to something more interesting than mid-grey, if you want to map say orange, i.e. rgb(255,128,0) to magenta, i.e. rgb(255,0,255) you would do something along the lines of:
LUT[np.dot([255,128,0],[1,256,65536])] = [255,0,255] # map orange to magenta
LUT[np.dot([255,255,255],[1,256,65536])] = [0,0,0] # map white to black
LUT[np.dot([0,0,0],[1,256,65536])] = [255,255,255] # map black to white
Keywords: Python, image processing, LUT, RGB LUT 24-bit LUT, lookup table.
Here is one way to do that using Numpy and Python/OpenCV. Here I change red to green.
Input:
import cv2
import numpy as np
# load image
img = cv2.imread('test_red.png')
# change color
result = img.copy()
result[np.where((result==[0,0,255]).all(axis=2))] = [0,255,0]
# save output
cv2.imwrite('test_green.png', result)
# Display various images to see the steps
cv2.imshow('result',result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Result:
You can create a mask of the image first and use that to replace the colors. There's likely a pure numpy way of doing this that is faster, but I don't know it.
This code takes ~0.5 seconds to run. You should expect it to take about half a second for each color replacement.
import cv2
import numpy as np
import time
# make image
res = (5632, 2048, 3);
img = np.zeros(res, np.uint8);
# change black to white
black = (0,0,0);
white = (255,255,255);
# make a mask
start_time = time.time();
mask = cv2.inRange(img, black, black);
print("Mask Time: " + str(time.time() - start_time));
# replace color
start_time = time.time();
img[mask == 255] = white;
print("Replace Time: " + str(time.time() - start_time));
In terms of your code it'll look like this
for line in provinceTable:
input_rgb = [line[1], line[2], line[3]]
input_rgb = (int(input_rgb[0]), int(input_rgb[1]), int(input_rgb[2]))
if line[7] == None:
output_rgb = (255,255,255)
else:
output_rgb = line[7].replace('[', '').replace(']','').split(',')
output_rgb = (int(output_rgb[0]), int(output_rgb[1]), int(output_rgb[2]))
mask = cv2.inRange(im, input_rgb, input_rgb)
im[mask == 255] = output_rgb
I'd like to change a pixel and for some reason this isn't working.
from PIL import Image
import numpy
im = Image.open("art\\PlanetX#1.25.png")
a = numpy.asarray(im)
img = Image.fromarray(a)
pixels = img.load()
pixels[0, 0] = (255, 0, 0, 255)
What should happen is the top left corner of the PNG should be set as red. I get the ValueError: Image is readonly error.
If you want to change just a few odd pixels, you can use the rather slow putpixel() like this:
from PIL import Image
# Create blue 30x15 image
im = Image.new('RGB',(30,15),color='blue')
# Change single pixel at 10,0 to red
im.putpixel((10,0),(255,0,0))
Alternatively, you can convert the entire image to a Numpy array and make many more changes, much faster with Numpy functions:
from PIL import Image
import numpy as np
# Create blue 30x15 image
im = Image.new('RGB',(30,15),color='blue')
# Convert to Numpy array
na = np.array(im)
# Change single pixel at 10,0 to green
na[0,10] = (0,255,0)
# Change whole row to red
na[3] = (255,0,0)
# Change whole column to yellow
na[:,8] = (255,255,0)
# Convert back to PIL Image and save
Image.fromarray(na).save('result.png')