Downscaling a tiff image by aggregating 5x5 pixels based on stats - python

How to downscale a tiff image of 10m resolution and create a new image of 50m where each pixel is stats from the first image?
The initial tiff image is a binary classification map - meaning each pixel (10m) belongs either to class "water" (value =0) or class "ice" (value=1).
I would like to create a new image, where each pixel is the percentage of water in a 5 x 5 block of the initial map, meaning each pixel of the new image will have a 50 m resolution and represents the ratio or percentage of "water" pixel on every 5x5 pixel of the former map. You can see the example here: Example
Here is an image sample (can be downloaded from google drive):
https://drive.google.com/uc?export=download&id=19hWQODERRsvoESiUZuL0GQHg4Mz4RbXj

Your image is saved in a rather odd format, using a 32-bit float to represent just two classes of data which could be represented in a single bit, so I converted it to PNG with ImageMagick using:
magick YOURIMAGE.TIF -alpha off image.png
Many Python libraries will stutter on your actual TIFF so maybe think about using a different way of writing it.
Once that is done, the code might look something like this:
#!/usr/bin/env python3
from PIL import Image
import numpy as np
# Set size of tiles for subsampling
tileX = tileY = 5
# Open image and convert to greyscale and thence to Numpy array
im = Image.open('image.png').convert('L')
na = np.array(im)
# Round height and width down to next lower multiple of tile sizes
h = (na.shape[0] // tileY) * tileY
w = (na.shape[1] // tileX) * tileX
# Create empty output array to fill
res = np.empty((h//tileY,w//tileX), np.uint8)
pxPerTile = tileX * tileY
for yoffset in range(0,h,tileY):
for xoffset in range(0,w,tileX):
# Count ice pixels in this 5x5 tile
nonZero = np.count_nonzero(na[yoffset:yoffset+tileY, xoffset:xoffset+tileX])
percent = int((100.0 * (pxPerTile - nonZero))/pxPerTile)
res[yoffset//tileY, xoffset//tileX] = percent
# Make Numpy array back into PIL Image and save
Image.fromarray(res.astype(np.uint8)).save('result.png')
On reflection, you can probably do it faster and more simply with cv2.resize() and a decimation of 0.2 on both axes and interpolation cv2.INTER_AREA

I did a version in pyvips:
#!/usr/bin/python3
import sys
import pyvips
image = pyvips.Image.new_from_file(sys.argv[1])
# label (0 == water, 1 == ice) is in the first band
label = image[0]
# average 5x5 areas
label = label.shrink(5, 5)
# turn into a percentage of water
water_percent = 100 * (1 - label)
# ... and save
water_percent.write_to_file(sys.argv[2])
I can run it on your test image like this:
$ ./average.py ~/pics/meltPondClassifiedAndS12.tif x.png
To make this (rather dark) output:

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 apply a transformation to a certain tonal range in VIPS/Python

I have to apply various transformations to different tonal ranges of 16-bit tiff files in VIPS (and Python). I have managed to do so, but I am new to VIPS and I am not convinced I am doing this in an efficient manner. These images are several hundred megabytes each, and cutting each excess step can save me a few seconds per image.
I wonder if there is a more efficient way to achieve the same results I obtain from the code below, for instance using lookup tables (I couldn't really figure out how they work in VIPS). The code separates the shadows in the red channel and passes them through a transformation.
im = Vips.Image.new_from_file("test.tiff")
# Separate the red channel
band = im[0]
# Find the tone limit for the bottom 5%
lim = band.percent(5)
# Create a mask using the tone limit
mask = (band <= lim)
# Convert the mask to 16 bits
mask = mask.cast(band.BandFmt, shift = True)
# Run the transformation on the image and keep only the shadow areas
new_shadows = (65535 * (shadows / lim * 0.1)) & mask
After running more or less similar codes for each tonal range (highlight, shadows, midtones, I add all the resulting images together to reconstruct the original band:
new_band = (new_shadows.add(new_highlights).add(new_midtones)).cast(band.BandFmt)
I made you a demo program showing how to do something like this with the vips histogram functions:
import sys
import pyvips
im = pyvips.Image.new_from_file(sys.argv[1])
# find the image histogram
#
# we'll get a uint image, one pixel high and 256 or
# 65536 pixels across, it'll have three bands for an RGB image source
hist = im.hist_find()
# find the normalised cumulative histogram
#
# for a 16-bit source, we'll have 65535 as the right-most element in each band
norm = hist.hist_cum().hist_norm()
# search from the left for the first pixel > 5%: the position of this pixel
# will give us the pixel value that 5% of pixels fall below
#
# .profile() gives back a pair of [column-profile, row-profile], we want index 1
# one. .getpoint() reads out a pixel as a Python array, so for an RGB Image
# we'll have something like [19.0, 16.0, 15.0] in shadows
shadows = (norm > 5.0 / 100.0 * norm.width).profile()[1].getpoint(0, 0)
# Now make an identity LUT that matches our original image
lut = pyvips.Image.identity(bands=im.bands,
ushort=(im.format == "ushort"))
# do something to the shadows ... here we just brighten them a lot
lut = (lut < shadows).ifthenelse(lut * 100, lut)
# make sure our lut is back in the original format, then map the image through
# it
im = im.maplut(lut.cast(im.format))
im.write_to_file(sys.argv[2])
It does a single find-histogram operation on the source image, then a single map-histogram operation, so it should be fast.
This is just adjusting the shadows, you'll need to extend it slightly to do midtones and highlights as well, but you can do all three modifications from the single initial histogram, so it shouldn't be any slower.
Please open an issue on the libvips tracker if you have any more questions:
https://github.com/libvips/libvips/issues

How to apply salt and pepper noise to only specific part of the image using python?

I have a image of size (1200 X 1000) and I am creating multiple patches (using sliding window of 256 X 256 with a stride of 10) out of it. My ultimate goal to supply the patches to the convolutional neural networks. I wish to introduce some salt and pepper noise to the patches generated out of the image. The image is nothing but screenshot of a webpage. Now I wish to make sure that the salt and pepper noise which I am adding doesn't fall on the HTML object regions of the patch which is generated. For e.g. Suppose I have radiobutton, textbox, selection dropwdown and buttons in the patch, I need to make sure that the noise generated shouldn't fall on these objects in the patch. Other than that, it could fall inside any other region in the patch.
I have written code for salt and pepper noise as follows:
import numpy as np
import os
import cv2
def noisy(image):
row,col,ch = image.shape
s_vs_p = 0.5
amount = 0.004
out = image
# Salt mode
num_salt = np.ceil(amount * image.size * s_vs_p)
coords = [np.random.randint(0, i - 1, int(num_salt))
for i in image.shape]
out[coords] = 1
# Pepper mode
num_pepper = np.ceil(amount* image.size * (1. - s_vs_p))
coords = [np.random.randint(0, i - 1, int(num_pepper))
for i in image.shape]
out[coords] = 0
return out
I have the coordinates of the html objects in the json file and have read and stored it into list of objects in my program. It contains X-Coord, Y-Coord, Width, Height and Type of the HTML object.
I have created a label matrix which is replica of the original image which has 5 classes:
0 : it is the default class value (i.e. is the region of the image excluding the HTML objects)
1: it is the value stored in the matrix for Textbox in the image
2: it is the value stored in the matrix for Button in the image
3: it is the value stored in the matrix for RadioButton in the image
4: it is the value stored in the matrix for selection in the image
So each of these values will represent specific HTML objects of the screen shot image in my Label Matrix.
Now, using the Label Matrix, How do I ensure that salt and pepper noise doesn't fall in the regions of the HTML objects in the patch created, is my challenge here.
It's kind of crude, but why don't you apply the noise uniformly on a copy of the original picture, then copy the patches of each object from the original image back on top of that noisy image?
EDIT after you rephrased your question
Basically, you need to test whether coords falls on a place where your mask (your "label matrix") is equal to 0.
Here is what you could do (or something in that vein, I can't say my code is very pretty):
out = np.zeros((26,26))
# salt coordinates
coords = [np.random.randint(0,26,50), np.random.randint(0,26,50)]
# mask - 0 are regions where salt can be applied, otherwise don't touch
mask = np.zeros(out.shape)
mask[:13,:13] = 1
mask[-13:,-13:] = 2
# where does the salt coordinates land on the mask
a = mask[coords]
# find points where mask is 0
b, = np.nonzero(a==0)
# copy from coords only where mask is 0
valid_coords = np.array(coords)[:,b]
# apply salt on valid coordinates
out[valid_coords.tolist()]=1

Categories

Resources