I am trying to implement a basic stenography technique in which I am replacing the LSB of the carrier image with the MSB of the message image. The LSB can belong to any of the RGB channel.
My approach is quite naive as I am looping the message_matrix and storing its MSB of a particular RGB channel in the corresponding LSB of the carrier_matrix. As the image size is not more than 1024 * 1024 the time complexity of performing this operation is O(n^2) but since I am using python the time taken is very high as compared to Java.
Can the same operation be performed in a more optimised way ?
def hide_using_bpcs(self, carrier_path, message_path, index, color_index):
carrier_matrix = self.image_to_matrix(carrier_path)
message_matrix = self.image_to_matrix(message_path) #use np.zeros
for row_index, row in enumerate(message_matrix):
for pixel_index, pixel in enumerate(row):
color = message_matrix[row_index][pixel_index][color_index]
msb = (color & 0xff) >> 7
carrier_pixel = carrier_matrix[
row_index][pixel_index][color_index]
carrier_matrix[row_index][pixel_index][
color_index] = self.set_bit(carrier_pixel, index, msb)
stegano_image = self.matrix_to_image(carrier_matrix)
return stegano_image
Now, for displaying a particular bit plane let say (Red 0), I am setting all the values of Green and Blue plane as 0 and retaining only the value of LSB (or the 0 bit) of the red color in the image. I have gone through some implementations done using openCV like [b,g,r = cv2.split(img)] but this is only splitting the image in 3 channels. What I want is to split a particular channel let say Red further into 8 Variations by retaining the value at the corresponding position.
def display_bit_plane(self, path, color_index, color_bit):
matrix = self.image_to_matrix(path)
matrix = matrix.astype(int)
result_matrix = self.image_to_matrix(path)
mask = 1 << color_bit
for row_index, row in enumerate(matrix):
for pixel_index, pixel in enumerate(row):
for iterator in range(0, 3):
result_matrix[row_index][pixel_index][iterator] = 0
color = matrix[row_index][pixel_index][color_index]
result_matrix[row_index][pixel_index][color_index] = self.set_bit(0, 7, ((color & mask) != 0))
stegano_image = self.matrix_to_image(result_matrix)
return stegano_image
I am using NumPy array for performing all the computations. However iterating it in usual way is very costly. Please provide some optimisation in the above two functions, so that these operations can be done in less than 1 second.
Edit 1 :
I have optimised the second function of retrieving the bit plane. If it can be further simplified please do tell. Color_index represents R, G, B as 0, 1, 2 respectively and color_bit is the bit position from 0-7.
def display_bit_plane_optimised(self, path, color_index, color_bit):
message_matrix = self.image_to_matrix(path)
change_index = [0, 1, 2]
change_index.remove(color_index)
message_matrix[:, :, change_index] = 0
mask = 1 << color_bit
message_matrix = message_matrix & mask
message_matrix[message_matrix == 1] = 1 << 7
stegano_image = self.matrix_to_image(message_matrix)
return stegano_image
Anything that applies to the whole array can be vectorised. If you want to apply an operation only on a part of the array, slice it.
I'm providing complete code so not to make assumptions about image_to_matrix() and matrix_to_image() methods. Take it from there.
I've kept your logic intact otherwise, but if you're only intending to embed the secret in the LSB, you can ditch pixel_bit, set its value to zero and simplify whatever constants result out of it. For example, in embed() you'd simply get mask = 0xfe, while any bitshifts by 0 are inconsequential.
import numpy as np
from PIL import Image
class Steganography:
def embed(self, cover_file, secret_file, color_plane, pixel_bit):
cover_array = self.image_to_matrix(cover_file)
secret_array = self.image_to_matrix(secret_file)
# every bit except the one at `pixel_bit` position is 1
mask = 0xff ^ (1 << pixel_bit)
# shift the MSB of the secret to the `pixel_bit` position
secret_bits = ((secret_array[...,color_plane] >> 7) << pixel_bit)
height, width, _ = secret_array.shape
cover_plane = (cover_array[:height,:width,color_plane] & mask) + secret_bits
cover_array[:height,:width,color_plane] = cover_plane
stego_image = self.matrix_to_image(cover_array)
return stego_image
def extract(self, stego_file, color_plane, pixel_bit):
stego_array = self.image_to_matrix(stego_file)
change_index = [0, 1, 2]
change_index.remove(color_plane)
stego_array[...,change_index] = 0
stego_array = ((stego_array >> pixel_bit) & 0x01) << 7
exposed_secret = self.matrix_to_image(stego_array)
return exposed_secret
def image_to_matrix(self, file_path):
return np.array(Image.open(file_path))
def matrix_to_image(self, array):
return Image.fromarray(array)
When I run it, it all completes within a second.
plane = 0
bit = 1
cover_file = "cover.jpg"
secret_file = "secret.jpg"
stego_file = "stego.png"
extracted_file = "extracted.png"
S = Steganography()
S.embed(cover_file, secret_file, plane, bit).save(stego_file)
S.extract(stego_file, plane, bit).save(extracted_file)
Notes
Your display_bit_plane_optimised() was reasonably optimised, but it had a bug if color_bit was anything but 0. The line
message_matrix = message_matrix & mask
zeros every other bit, but unless color_bit is 0, the values will be some other power of 2. So when you come to
message_matrix[message_matrix == 1] = 1 << 7
no pixel is modified. If you want to keep your way, you have to change the last line to
message_matrix[message_matrix != 0] = 1 << 7
My approach was simply to bring the embedded bit to the LSB position, zero out every other bit and then shift it to the MSB position with no conditionals.
Related
I want to get a 2d mask array of green color.
Code:
W = 100; H = 100
Map = np.zeros((W,H,3), dtype=int)
Map[1,1] = [0,255,0]
MResult = (Map[:,:] == [0,255,0])
A = np.zeros((W,H))
A[MResult] = 1
Not works.
I think there is also a conceptual mistake in your code, the height should be the first dimension, so Map should be np.zeros((H,W,3), dtype='int'). Also note that dtype is a string, not the python built-in int.
Now returning to the problem, you need to use numpy's smart indexing to better manipulate the arrays. It seems that you want to set the green channel to 255 in Map, you would do so by doing Map[:,:,1] = 255 note that we use : to say "all the elements in the row and column, and in channel 1 should be set to 255".
Then we get a binary mask for A, we should do MResult = Map[:,:,1] == 255, and in the end we simply do the A[MResult] = 1 as you did.
To check only green channel is not good idea,
we get white color too.
But code:
import numpy as np
W = 100; H = 100
Map = np.zeros((W,H,3), dtype=int)
Map[1,1] = [0,255,0]
A = np.zeros((W,H))
A[(0 == Map[:,:,0]) & (255 == Map[:,:,1]) & (0 == Map[:,:,2])] = 1
now to works.
I've been looking for hours to find a question similar, but nothing has satisfied me.
My problem is: I've a PIL image (representing a canal) already converted into a Numpy array (using the "L" mode of PIL), and I'd like to retrieve the white pixels whose neighbor are black (their indexes in fact), without using for loops (the image is really huge).
I thought of np.where but I don't know how I should use it to solve my problem, and I also don't know if it would be faster than using for loops (because my aim would be reaching this goal with the fastest solution).
I hope I'm clear enough, and I thank you in advance for your response!
EDIT: for example, with this image (a simple canal, it is already a black and white image, so the image.convert('L') isn't really useful here, but the code should be generic if possible), I'd do something like that:
import numpy as np
from PIL import Image
image = Image.open(canal)
image = image.convert("L")
array = np.asarray(image)
l = []
for i in range(1, len(array) - 1):
for j in range(1, len(array[0]) - 1):
if array[i][j] == 255 and (array[i+1][j] == 0 or array[i-1][j] == 0 or array[i][j+1] == 0 or array[i][j-1] == 0):
l.append((i, j))
and I'd hope to obtain l as fast as possible :)
I've colored the pixels I need in red in the next image: here.
EDIT2: thank you all for the help, it worked!
You could use the numba just-in-time compiler to speed up your loop.
from numba import njit
#njit
def find_highlow_pixels(img):
pixels = []
for j in range(1, img.shape[0]-1):
for i in range(1, img.shape[1]-1):
if (
img[j, i] == 255 and (
img[j-1, i]==0 or img[j+1,i]==0 or
img[j, i-1]==0 or img[j, i+1]==0
)
):
pixels.append((j, i))
return pixels
Another possibility that came to my mind would be using the minimum filter. However, I would expect it to be slower than the first proposed solution, but could be useful to build more on top of it.
import numpy as np
from scipy.ndimage import minimum_filter
# create a footprint that only takes the neighbours into account
neighbours = (np.arange(9) % 2 == 1).reshape(3,3)
# create a mask of relevant pixels, img should be your image as array
mask = np.logical_and(
img == 255,
minimum_filter(img, footprint=neighbours) == 0
)
# get indexes
indexes = np.where(mask)
# as list
list(zip(*indexes))
If memory space is not considered, I prefer manipulation of masks like the following.
# Step 1: Generate two masks of white and black.
mask_white = img == 255
mask_black = img == 0
# Step 2: Apply 8-neighborhood dilation on black mask
# if you want to use numpy only, you need to implement dilation by yourself.
# define function of 8-neighborhood dilation
def dilate_8nb(m):
index_row, index_col = np.where(m)
ext_index_row = np.repeat(index_row,9)
ext_index_col = np.repeat(index_col,9)
ext_index_row.reshape(-1,9)[:, :3] += 1
ext_index_row.reshape(-1,9)[:, -3:] -= 1
ext_index_col.reshape(-1,9)[:, ::3] += 1
ext_index_col.reshape(-1,9)[:, 2::3] -= 1
ext_index_row = np.clip(ext_index_row, 0, m.shape[0]-1)
ext_index_col = np.clip(ext_index_col, 0, m.shape[1]-1)
ret = m.copy()
ret[ext_index_row, ext_index_col] = True
return ret
ext_mask_black = dilate_8nb(mask_black)
# or just using dilation in scipy
# from scipy import ndimage
# ext_mask_black = ndimage.binary_dilation(mask_black, structure=ndimage.generate_binary_structure(2, 2))
# Step 3: take the intersection of mask_white and ext_mask_black
mask_target = mask_white & ext_mask_black
# Step 4: take the index using np.where
l = np.where(mask_target)
# modify this type to make it consistency with your result
l = list(zip(*l))
I have some code, which works as intended, however takes about 4 and a half hours to run, I understand that there are about 50 billion calculations my poor pc needs to do but I thought it would be worth asking!
This code gets an image, and wants to find every possible region of 331*331 pixels in the given image, and find how many black pixels there are in each, I will use this data to create a heatmap of black pixel density, and also a list of all of the values found:
image = Image.open(self.selectedFile)
pixels = list(image.getdata())
width, height = image.size
pixels = [pixels[i * width:(i+1) * width] for i in range(height)]
#print(pixels)
rightShifts = width - 331
downShifts = height - 331
self.totalRegionsLabel['text'] = f'Total Regions: {rightShifts * downShifts}'
self.blackList = [0 for i in range(0, rightShifts*downShifts)]
self.heatMap = [[] for i in range(0, downShifts)]
for x in range(len(self.heatMap)):
self.heatMap[x] = [0 for i in range(0, rightShifts)]
for x in range(rightShifts):
for y in range(downShifts):
blackCount = 0
for z in range(x + 331):
for w in range(y + 331):
if pixels[z][w] == 0:
blackCount += 1
self.blackList[x+1*y] = blackCount
self.heatMap[x][y] = blackCount
print(self.blackList)
You have several problems here, as I pointed out. Your z/w loops are always starting at the upper left, so by the time you get towards the end, you're summing the entire image, not just a 331x331 subset. You also have much confusion in your axes. In an image, [y] is first, [x] is second. An image is rows of columns. You need to remember that.
Here's an implementation as I suggested above. For each column, I do a full sum on the top 331x331 block. Then, for every row below, I just subtract the top row and add the next row below.
self.heatMap = [[0]*rightShifts for i in range(downShifts)]
for x in range(rightShifts):
# Sum up the block at the top.
blackCount = 0
for row in range(331):
for col in range(331):
if pixels[row][x+col] == 0:
blackCount += 1
self.heatMap[0][x] = blackCount
for y in range(1,downShifts):
# To do the next block down, we subtract the top row and
# add the bottom.
for col in range(331):
blackCount += pixels[y+330][x+col] - pixels[y-1][x+col]
self.heatMap[y][x] = blackCount
You could tweak this even more by alternating the columns. So, at the bottom of the first column, scoot to the right by subtracting the first column and adding the next new column. then scoot back up to the top. That's a lot more trouble.
The two innermost for-loops seem to be transformable to some numpy code if using this package is not an issue. It would give something like:
pixels = image.get_data() # it is probably already a numpy array
# Get an array filled with either True or False, with True whenever pixel is black:
pixel_is_black = (pixels[x:(x+331), y:(y+331)] == 0)
pixel_is_black *= 1 # Transform True and False to respectively 1 and 0. Maybe not needed
self.blackList[x+y] = pixel_is_black.sum() # self explanatory
This is the simplest optimization I can think of, you probably can do much better with clever numpy tricks.
I would recommend using some efficient vector computations through the numpy and opencv libraries.
First, binarize your image so that black pixels are set to zero, and any other color pixels (gray to white) are set to 1. Then, apply a 2D filter to the image of shape 331 x 331 where each value in the filter kernel is (1 / (331 x 331) - this will take the average of all the values in each 331x331 area and assign it to the center pixel.
This gives you a heatmap, where each pixel value is the proportion of non-black pixels in the surrounding 331 x 331 region. A darker pixel (value closer to zero) means more pixels in that region are black.
For some background, this approach uses image processing techniques called image binarization and box blur
Example code:
import cv2
import numpy as np
# setting up a fake image, with some white spaces, gray spaces, and black spaces
img_dim = 10000
fake_img = np.full(shape=(img_dim, img_dim), fill_value=255, dtype=np.uint8) # white
fake_img[: img_dim // 3, : img_dim // 3] = 0 # top left black
fake_img[2 * img_dim // 3 :, 2 * img_dim // 3 :] = 0 # bottom right black
fake_img[img_dim // 3 : 2 * img_dim // 3, img_dim // 3 : 2 * img_dim // 3] = 127 # center gray
# show the fake image
cv2.imshow("", fake_img)
cv2.waitKey()
cv2.destroyAllWindows()
# solution to your problem
binarized = np.where(fake_img == 0, 0, 1) # have 0 values where black, 1 values else
my_filter = np.full(shape=(331, 331), fill_value=(1 / (331 * 331))) # set up filter
heatmap = cv2.filter2D(fake_img, 1, my_filter) # apply filter, which takes average of values in 331x331 block
# show the heatmap
cv2.imshow("", heatmap)
cv2.waitKey()
cv2.destroyAllWindows()
I ran this on my laptop, with a huge (fake) image of 10000 x 10000 pixels, almost instantly.
Sorry I should have deleted this post before you all put the effort in, however, some of these workarounds are really smart and interesting, I ended up coming up with a solution independently that is the same as what Tim Robbers first suggested, I used the array I had and built a second one on which every item in a row is the number of black cells preceding it, and then for each row in a region instead of scanning every item, just scan the preceding value and the final value and you are good:
image = Image.open(self.selectedFile).convert('L') #convert to luminance mode as RGB information is irrelevant
pixels = list(image.getdata()) #get the value of every pixel in the image
width, height = image.size
pixels = [pixels[i * width:(i+1) * width] for i in range(height)] #split the pixels array into a two dimensional array with the dimensions to match the image
#This program scans every possible 331*331 square starting from the top left, so it will move right width - 331 pixels and down height - 331 pixels
rightShifts = width - 331
downShifts = height - 331
self.totalRegionsLabel['text'] = f'Total Regions: {rightShifts * downShifts}' #This wont update till the function has completed running
#The process of asigning new values to values in an array is faster than appending them so this is why I prefilled the arrays:
self.heatMap = [[] for i in range(0, downShifts)]
for x in range(len(self.heatMap)):
self.heatMap[x] = [0 for i in range(0, rightShifts)]
cumulativeMatrix = [] #The cumulative matrix replaces each value in each row with how many zeros precede it
for y in range(len(pixels)):
cumulativeMatrix.append([])
cumulativeMatrix[y].append(0)
count = 0
for x in range(len(pixels[y])):
if pixels[y][x] == 0:
count += 1
cumulativeMatrix[y].append(count)
regionCount = 0
maxValue = 0 #this is the lowest possible maximum value
minValue = 109561 #this is the largest possible minimum value
self.blackList = []
#loop through all possible regions
for y in range(downShifts):
for x in range(rightShifts):
blackPixels = 0
for regionY in range(y, y + 331):
lowerLimit = cumulativeMatrix[regionY][x]
upperLimit = cumulativeMatrix[regionY][x+332]
blackPixels += (upperLimit - lowerLimit)
if blackPixels > maxValue:
maxValue = blackPixels
if blackPixels < minValue:
minValue = blackPixels
self.blackList.append(blackPixels)
self.heatMap[y][x] = blackPixels
regionCount += 1
This brought run time to under a minute and thus solved my problem, however, thank you for your contributions I have learned a lot from reading them!
Try to look into the map() function. It uses C to streamline iterations.
You can speed up your for loops like this:
pixels = list(map(lambda i: x[i*width:(i+1)*width], range(height)))
Here is a portion of the MATLAB code I was able to run (excluding other non-important variables). For context, the full MATLAB program simulates band excitation response in from an atomic force microscope (not relevant to code error)
IO_rate = 4E6; %[samples/sec]
w1 = 200E3; % lower edge of band
w2 = 400E3; % upper edge of band
N_pixels = 128; % number of pixels along a line scan
N_points_per_pixel = 2^13; % number of data points per pixel
w_vec = -IO_rate/2: IO_rate/N_points_per_pixel : IO_rate/2-IO_rate/N_points_per_pixel; %frequency vector over a pixel
% build drive signal, define in the Fourier domain
D_vec = zeros(size(w_vec));
D_vec( ((abs(w_vec)<w2) + (abs(w_vec)>w1)) == 2 ) = 1; % drive bins located within upper and lower band edges
band_ind = find( (((w_vec)<w2) + ((w_vec)>w1)) == 2 );
Now I am in the process of converting the code to Python. This is what I have so far
IO_rate = 4E6; #[samples/sec]
w1 = 200E3; # lower edge of band
w2 = 400E3; # upper edge of band
N_pixels = 128; # number of pixels along a line scan
N_points_per_pixel = 2^13; # number of data points per pixel
w_vec = np.arange(-IO_rate/2, IO_rate/2-IO_rate/N_points_per_pixel, IO_rate/N_points_per_pixel)
D_vec = np.zeros(np.size(w_vec))
However, now I am completely lost as to how I would convert the line D_vec( ((abs(w_vec)<w2) + (abs(w_vec)>w1)) == 2 ) = 1; to Python. It isn't my MATLAB code but it looks like it's attempting to assign a value to a function call, I'm not sure why nor am I sure what the line actually does. Does anyone have an idea of how I would convert this line to Python?
The statement tmp = ((abs(w_vec)<w2) + (abs(w_vec)>w1)) == 2 in MATLAB returns a logical (true/false) array where tmp[i] is true if w_vec[i] < w2 and w_vec[i] > w1. Matlab implicitly converts true/false values to 0 or 1, so checking if the sum is equal to 2 is equivalent to checking if both sub-conditions are satisfied.
Once we have this array, we can use it to set the corresponding entries in Dvec to 1.
Note that ^ is the XOR operator in Python, and not the power operator. a ^ b in MATLAB is equivalent to a**b or pow(a,b) in Python.
Here's my conversion:
IO_rate = 4E6; #[samples/sec]
w1 = 200E3; # lower edge of band
w2 = 400E3; # upper edge of band
N_pixels = 128; # number of pixels along a line scan
N_points_per_pixel = pow(2,13); # number of data points per pixel
#Note the +1 in the second argument of arange
w_vec = np.arange(-IO_rate/2, IO_rate/2-IO_rate/N_points_per_pixel + 1, IO_rate/N_points_per_pixel);
D_vec = np.zeros(np.size(w_vec));
#Find the indices that satisfy both conditions
ind = (abs(w_vec)<w2) & (abs(w_vec)>w1);
D_vec[ind] = 1; #assign those indices to 1.
band_ind = np.nonzero(((w_vec)<w2) & ((w_vec)>w1));
The Numpy idiom to create a new array, whose shape matches a first array and whose values depend on a logical test on the first array values is numpy.where.
As I've answered in another question of yours,
D = np.where(np.logical_and(np.abs(v)>low, np.abs(v)<hi), 1, 0)
I have an RGB image which I am loading into a 2D array using PIL
img = Image.open(path)
imgData = numpy.array(img)
I need to efficiently translate this into a 2D array of RGB tuples (in some sense a 3D array) the same size containing a rough 'classification' of each pixel - 'red', 'green', 'white' or 'other' - at each index based on which 'colour region' they lie within. This is for purposes of image recognition.
My current implementation uses a element-wise for loop but is very slow (an 8MP image takes 1+ minutes):
for i in range(cols): # for every col
for j in range(rows): # for every row
r,g,b = imgData[i,j]
if b > 220: # white
n = 3
elif r > 230: # red
n = 2
else: # green
n = 1
mapData[i,j] = n
(I realise that the order of the if statements here affects the precedence of the classifications - this is not a major issue for now although I would prefer to define the colour spaces exclusively)
I am running Python 3.6.4 and happy to use NumPy or not. Having done a bunch of research, it seems like there are a number of faster and more 'pythonic' and vectorised ways to do this but I have not been able to get any working.
Any help would be much appreciated
Thanks!
Using np.where makes this pretty fast.
mapData = np.where(imgData[:,:,2] > 220, 3, np.where(imgData[:,:,0]>230, 2, 1))
But when applying this to a picture the only results where ones. Did I miss anything or should the cases be made in a different way?
Your algorithm as of the moment can be captured like this:
r, g, b = imgData[...,0], imgData[...,1], imgData[...,2]
mapData = np.ones_like(r, dtype=int)
mapData[r > 230] = 2
mapData[b > 220] = 3
Note the order of operations in assigning these numbers.
Colour classification is usually done by treating RGB colours as vectors. Normalize each one to the magnitude, then find the distance to your target colour.
For example, the skin detector in smartcrop.js works like this (using pyvips):
def pythag(im):
return sum([x ** 2 for x in im]) ** 0.5
skin = [0.78, 0.57, 0.44]
score = 1 - pythag(img / pythag(img) - skin)
Now score is a float image with values in 0 - 1 which is 1 for pixels most likely to be skin-coloured. Note that it ignores brightness: you'll need another rule to chop off very dark areas.
In your case I guess you'd need an array set of target vectors, then compute all the colour probabilities, and finally label the output pixel with the index of the highest-scoring vector. Something like:
import sys
import pyvips
def pythag(im):
return sum([x ** 2 for x in im]) ** 0.5
def classify(img, target):
return 1 - pythag(img / pythag(img) - target)
# find [index, max] of an array of pyvips images
def argmax(ar):
if len(ar) == 1:
return [0, ar[0]]
else:
index, mx = argmax(ar[:-1])
return [(ar[-1] > mx).ifthenelse(len(ar) - 1, index),
(ar[-1] > mx).ifthenelse(ar[-1], mx)]
skin = [0.78, 0.57, 0.44]
red = [1, 0, 0]
green = [0, 1, 0]
targets = [red, green, skin]
# we're not doing any coordinate transformations, so we can stream the image
img = pyvips.Image.new_from_file(sys.argv[1], access="sequential")
scores = [classify(img, x) for x in targets]
index, mx = argmax(scores)
index.write_to_file(sys.argv[2])
(plug: pyvips is typically 2x or 3x faster than numpy and needs much less memory)