How to simply do np.where not includes np.where - python

I wrote a program to change the color of the skin in the photo.
First I get a skin mask, then I convert BGR image to HSV. add V channel value in mask. Like this:
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
H, S, V = cv2.split(hsv)
NV = np.where(skin_mask > 0, V + skin_mask / 255 * 50, V).astype(np.uint8)
NHSV = cv2.cvtColor(cv2.merge([H, S, NV]), cv2.COLOR_HSV2BGR)
But some original white pixel become to black, I think maybe V + skin_mask / 255 * 50 let pixel over 255.
so I try to:
NV = np.where(skin_mask > 0, np.where(V + skin_mask / 255 * 50 > 255, 255, V + skin_mask / 255 * 50), V).astype(np.uint8)
It's work. but ugly.
I want to know how to beautify this writing, don't use np.where includes np.where. Thank you very much!!!

It may be more elegant to use skin_mask as a mask, instead of applying arithmetic like skin_mask / 255 * 50.
You may solve it using cv2.add:
NV = V.copy()
cv2.add(NV, np.full_like(V, 50), NV, mask=skin_mask)
Advantages of using cv2.add over NumPy arithmetic:
cv2.add supports mask argument (mask element values are usually 0 and 255).
cv2.add clips the result to the valid range of uint8 [0, 255], without overflow.
Code I used for testing the solution:
import numpy as np
import cv2
# Build sample skin_mask
skin_mask = np.zeros((100, 80), np.uint8)
skin_mask[30:70, 30:60] = 255
# Build sample V matrix
V = np.full_like(skin_mask, 60)
V[40:80, 40:80] = 220
# Sum using cv2.add
NV = V.copy()
cv2.add(NV, np.full_like(V, 50), NV, mask=skin_mask)
# Sum using NumPy (used as reference for testing).
refNV = np.where(skin_mask > 0, np.minimum(V + skin_mask / 255 * 50, 255), V).astype(np.uint8) # Reference
if np.any(NV != refNV):
print('There is a bug: NV != refNV') # Should not enter here
# Show the images
cv2.imshow('skin_mask', skin_mask)
cv2.imshow('V', V)
cv2.imshow('NV', NV)
cv2.imshow('refNV', refNV)
cv2.waitKey()
cv2.destroyAllWindows()

You still need to detect the overflow. A slightly cleaner way consists in using np.minimum or np.clip:
brighter_V = np.minimum(V + skin_mask / 255 * 50, 255)
NV = np.where(skin_mask > 0, brighter_V, V).astype(np.uint8)
The np.clip method is even more generic, in case values can overflow below 0:
brighter_V = np.clip(V + skin_mask / 255 * 50, 0, 255)
NV = np.where(skin_mask > 0, brighter_V, V).astype(np.uint8)
(the break in two lines is just my personal coding preference)

Related

Image goes black after thresholding

I am trying to extract blood network from this face image: Face image
For such task, i am using the P&M anisotropic diffusion found in this question: Anisotropic diffusion 2d images. Then i am using tophat transform followed by blackhat transform, afterwards i use a simple threshold to set to 255 all pixel that has an intensity value of 100.
The problem is that, after i use the threshold and try to open the image, whatever way i try, the image is displayed as fully black:
In short, my goal is to extract the blood vessels using P&M anisotropic diffusion with structuring element of flat disk of 5x5, then apply tophat and blackhat, respectively and a simple threshold and actually be able to view the image afterwards.
Here's my code on how i am trying it:
import cv2
import import cv2 numpy as np
import warnings
face_img=mpimg.imread('path')
def anisodiff(img, niter=1, kappa=50, gamma=0.1, step=(1., 1.), option=1):
if img.ndim == 3:
m = "Only grayscale images allowed, converting to 2D matrix"
warnings.warn(m)
img = img.mean(2)
img = img.astype('float32')
imgout = img.copy()
deltaS = np.zeros_like(imgout)
deltaE = deltaS.copy()
NS = deltaS.copy()
EW = deltaS.copy()
gS = np.ones_like(imgout)
gE = gS.copy()
for ii in range(niter):
deltaS[:-1, :] = np.diff(imgout, axis=0)
deltaE[:, :-1] = np.diff(imgout, axis=1)
if option == 1:
gS = np.exp(-(deltaS/kappa)**2.)/step[0]
gE = np.exp(-(deltaE/kappa)**2.)/step[1]
elif option == 2:
gS = 1./(1.+(deltaS/kappa)**2.)/step[0]
gE = 1./(1.+(deltaE/kappa)**2.)/step[1]
E = gE*deltaE
S = gS*deltaS
NS[:] = S
EW[:] = E
NS[1:, :] -= S[:-1, :]
EW[:, 1:] -= E[:, :-1]
imgout += gamma*(NS+EW)
return imgout
new_img = anisodiff(face_img, niter=1, kappa=20, gamma=0.1, step=(1., 1.), option=1)
filterSize =(3, 3)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,
filterSize)
input_image = new_img
first_tophat_img = cv2.morphologyEx(input_image,
cv2.MORPH_TOPHAT,
kernel)
filterSize =(3, 3)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,
filterSize)
second_tophat_img = cv2.morphologyEx(input_image,
cv2.MORPH_BLACKHAT,
kernel)
ret, thresh1 = cv2.threshold(second_tophat_img, 200, 255, cv2.THRESH_BINARY)
Even when i set the threshold to 254 for instance, the image goes black.
I executed a simple MATLAB implementation, and got a nice result.
MATLAB code:
I = imread('02_giorgos_1_f_M_30_830.tif');
I = im2double(uint8(I));
J = imdiffusefilt(I);
K = imtophat(J, ones(3));
figure;imshow(imadjust(K, stretchlim(K)));
Result:
I don't know if you know MATLAB, but I used the default arguments of imdiffusefilt (equivalent to anisodiff in your code).
Default MATLAB arguments are equivalent to:
Input image is in range [0, 1] and not [0, 255].
niter=5 (note: you used only 1 iteration and it's not enough).
kappa=0.1
gamma=0.125
MATLAB default is 8 neighbors connectivity (not 4 neighbors as used in anisodiff).
8 neighbors connectivity:
For getting same result as in MATLAB, I implemented an 8 neighbors connectivity Anisotropic diffusion (based on MATLAB source code).
Note: with 4 neighbors connectivity it's working, but result is not so nice as using 8 neighbors.
Displaying the output image:
In order to display the output image correctly, I used imadjust(K, stretchlim(K)).
The command stretches the range of the input image such that percentile 1 goes to 0, and percentile 99 goes to 1 (linear stretch).
One more thing:
Instead of using fixed threshold of 200, I used percentile 95 threshold:
t = np.percentile(first_tophat_img, 95)
ret, thresh1 = cv2.threshold(first_tophat_img, t, 255,
cv2.THRESH_BINARY)
Here is the code (uses cv2.imshow for testing):
import cv2
import numpy as np
import matplotlib.image as mpimg
import warnings
face_img = mpimg.imread('02_giorgos_1_f_M_30_830.tif')
def anisodiff8neighbors(img, niter=5, kappa=0.1, gamma=0.125):
""" See https://www.mathworks.com/help/images/ref/imdiffusefilt.html
Anisotropic diffusion filtering with 8 neighbors
Range of img is assumed to be [0, 1] (not [0, 255]).
"""
if img.ndim == 3:
m = "Only grayscale images allowed, converting to 2D matrix"
warnings.warn(m)
img = img.mean(2)
img = img.astype('float32')
imgout = img.copy()
for ii in range(niter):
# MATLAB source code is commented
#paddedImg = padarray(I, [1 1], 'replicate');
padded_img = np.pad(imgout, (1, 1), 'edge')
#diffImgNorth = paddedImg(1:end-1,2:end-1) - paddedImg(2:end,2:end-1);
#diffImgEast = paddedImg(2:end-1,2:end) - paddedImg(2:end-1,1:end-1);
#diffImgNorthWest = paddedImg(1:end-2,1:end-2) - I;
#diffImgNorthEast = paddedImg(1:end-2,3:end) - I;
#diffImgSouthWest = paddedImg(3:end,1:end-2) - I;
#diffImgSouthEast = paddedImg(3:end,3:end) - I;
diff_img_north = padded_img[0:-1, 1:-1] - padded_img[1:, 1:-1]
diff_img_east = padded_img[1:-1, 1:] - padded_img[1:-1, 0:-1]
diff_img_north_west = padded_img[0:-2, 0:-2] - imgout
diff_img_north_east = padded_img[0:-2, 2:] - imgout
diff_img_south_west = padded_img[2:, 0:-2] - imgout
diff_img_south_east = padded_img[2:, 2:] - imgout
#case 'exponential'
#conductCoeffNorth = exp(-(abs(diffImgNorth)/gradientThreshold).^2);
#conductCoeffEast = exp(-(abs(diffImgEast)/gradientThreshold).^2);
#conductCoeffNorthWest = exp(-(abs(diffImgNorthWest)/gradientThreshold).^2);
#conductCoeffNorthEast = exp(-(abs(diffImgNorthEast)/gradientThreshold).^2);
#conductCoeffSouthWest = exp(-(abs(diffImgSouthWest)/gradientThreshold).^2);
#conductCoeffSouthEast = exp(-(abs(diffImgSouthEast)/gradientThreshold).^2);
conduct_coeff_north = np.exp(-(np.abs(diff_img_north)/kappa)**2.0)
conduct_coeff_east = np.exp(-(np.abs(diff_img_east)/kappa)**2.0)
conduct_coeff_north_west = np.exp(-(np.abs(diff_img_north_west)/kappa)**2.0)
conduct_coeff_north_east = np.exp(-(np.abs(diff_img_north_east)/kappa)**2.0)
conduct_coeff_south_west = np.exp(-(np.abs(diff_img_south_west)/kappa)**2.0)
conduct_coeff_south_east = np.exp(-(np.abs(diff_img_south_east)/kappa)**2.0)
#fluxNorth = conductCoeffNorth .* diffImgNorth;
#fluxEast = conductCoeffEast .* diffImgEast;
#fluxNorthWest = conductCoeffNorthWest .* diffImgNorthWest;
#fluxNorthEast = conductCoeffNorthEast .* diffImgNorthEast;
#fluxSouthWest = conductCoeffSouthWest .* diffImgSouthWest;
#fluxSouthEast = conductCoeffSouthEast .* diffImgSouthEast;
flux_north = conduct_coeff_north * diff_img_north
flux_east = conduct_coeff_east * diff_img_east
flux_north_west = conduct_coeff_north_west * diff_img_north_west
flux_north_east = conduct_coeff_north_east * diff_img_north_east
flux_south_west = conduct_coeff_south_west * diff_img_south_west
flux_south_east = conduct_coeff_south_east * diff_img_south_east
#% Discrete PDE solution
#I = I + diffusionRate * (fluxNorth(1:end-1,:) - fluxNorth(2:end,:) + ...
# fluxEast(:,2:end) - fluxEast(:,1:end-1) + (1/(dd^2)).* fluxNorthWest + ...
# (1/(dd^2)).* fluxNorthEast + (1/(dd^2)).* fluxSouthWest + (1/(dd^2)).* fluxSouthEast);
imgout = imgout + gamma * (flux_north[0:-1,:] - flux_north[1:,:] +
flux_east[:,1:] - flux_east[:,0:-1] + 0.5*flux_north_west +
0.5*flux_north_east + 0.5*flux_south_west + 0.5*flux_south_east)
return imgout
#new_img = anisodiff(face_img, niter=1, kappa=20, gamma=0.1, step=(1., 1.), option=1)
face_img = face_img.astype(float) / 255;
#new_img = anisodiff(face_img, niter=5, kappa=0.1, gamma=0.125, step=(1., 1.), option=1)
new_img = anisodiff8neighbors(face_img, niter=5, kappa=0.1, gamma=0.125)
filterSize =(3, 3)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,
filterSize)
input_image = new_img
first_tophat_img = cv2.morphologyEx(input_image,
cv2.MORPH_TOPHAT,
kernel)
# Use percentile 95 (of image) as threshold instead of fixed threshold 200
t = np.percentile(first_tophat_img, 95)
ret, thresh1 = cv2.threshold(first_tophat_img, t, 255, cv2.THRESH_BINARY)
cv2.imshow('thresh1', thresh1)
filterSize =(3, 3)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,
filterSize)
second_tophat_img = cv2.morphologyEx(input_image,
cv2.MORPH_BLACKHAT,
kernel)
#ret, thresh1 = cv2.threshold(second_tophat_img, 200, 255, cv2.THRESH_BINARY)
# Use percentile 95 (of image) as threshold instead of fixed threshold 200
t = np.percentile(second_tophat_img, 95)
ret, thresh2 = cv2.threshold(second_tophat_img, t, 255, cv2.THRESH_BINARY)
cv2.imshow('thresh2', thresh2)
lo, hi = np.percentile(first_tophat_img, (1, 99))
first_tophat_img_stretched = (first_tophat_img.astype(float) - lo) / (hi-lo) # Apply linear "stretch" - lo goes to 0, and hi goes to 1
cv2.imshow('first_tophat_img_stretched', first_tophat_img_stretched)
cv2.waitKey()
cv2.destroyAllWindows()
Result:
thresh1:
thresh2:
first_tophat_img_stretched:

Manipulate RGB values in image

I would like to apply a simple algebraic operation to the RBG values of an image, that I have loaded via PIL. My current version works, but is slow:
from PIL import Image
import numpy as np
file_name = '1'
im = Image.open('data/' + file_name + '.jpg').convert('RGB')
pixels = np.array(im)
s = pixels.shape
p = pixels.reshape((s[0] * s[1], s[2]))
def update(ratio=0.5):
p2 = np.array([[min(rgb[0] + rgb[0] * ratio, 1), max(rgb[1] - rgb[1] * ratio, 0), rgb[2]] for rgb in p])
img = Image.fromarray(np.uint8(p2.reshape(s)))
img.save('result/' + file_name + '_test.png')
return 0
update(0.5)
Has someone a more efficient idea?
Make use of NumPy's vectorized operations to get rid of the loop.
I modified your original approach to compare performance between the following, different solutions. Also, I added a PIL only approach using ImageMath, if you want to get rid of NumPy completely.
Furthermore, I assume, there is/was a bug:
p2 = np.array([[min(rgb[0] + rgb[0] * ratio, 1), max(rgb[1] - rgb[1] * ratio, 0), rgb[2]] for rgb in p])
You actually do NOT convert to float, so it should be 255 instead of 1 in the min call.
Here's, what I've done:
import numpy as np
from PIL import Image, ImageMath
import time
# Modified, original implementation; fixed most likely wrong compare value in min (255 instead of 1)
def update_1(ratio=0.5):
pixels = np.array(im)
s = pixels.shape
p = pixels.reshape((s[0] * s[1], s[2]))
p2 = np.array([[min(rgb[0] + rgb[0] * ratio, 255), max(rgb[1] - rgb[1] * ratio, 0), rgb[2]] for rgb in p])
img = Image.fromarray(np.uint8(p2.reshape(s)))
img.save('result_update_1.png')
return 0
# More efficient vectorized approach using NumPy
def update_2(ratio=0.5):
pixels = np.array(im)
pixels[:, :, 0] = np.minimum(pixels[:, :, 0] * (1 + ratio), 255)
pixels[:, :, 1] = np.maximum(pixels[:, :, 1] * (1 - ratio), 0)
img = Image.fromarray(pixels)
img.save('result_update_2.png')
return 0
# More efficient approach only using PIL
def update_3(ratio=0.5):
(r, g, b) = im.split()
r = ImageMath.eval('min(float(r) / 255 * (1 + ratio), 1) * 255', r=r, ratio=ratio).convert('L')
g = ImageMath.eval('max(float(g) / 255 * (1 - ratio), 0) * 255', g=g, ratio=ratio).convert('L')
Image.merge('RGB', (r, g, b)).save('result_update_3.png')
return 0
im = Image.open('path/to/your/image.png')
t1 = time.perf_counter()
update_1(0.5)
print(time.perf_counter() - t1)
t1 = time.perf_counter()
update_2(0.5)
print(time.perf_counter() - t1)
t1 = time.perf_counter()
update_3(0.5)
print(time.perf_counter() - t1)
The performance on a [400, 400] RGB image on my machine:
1.723889293 s # your approach
0.055316339 s # vectorized NumPy approach
0.062502050 s # PIL only approach
Hope that helps!

Masking an Image by Manipulating Pixels through Conditions

I have read an image in python using RGBA color space. The size of an image is 640 by 960 and is stored to an array called img_array . Now each element in the array contains [R,G,B,A], say for example [21,34,53,255]. I want to filter my image pixels by turning pixels into black [0,0,0,255] which does not satisfy the conditional below.
R > 95 and G > 40 and B > 20 and R > G and R > B and | R - G | > 15 and A > 15
How will I do it in python? All I know is to set pixels to black which is not within the lower and upper boundaries using cv2.inrange(). Below is my sample code:
#import the necessary packages
import imutils
import numpy as np
import argparse
import cv2
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image",help = "path to the image file")
args = vars(ap.parse_args())
#read image with alpha channel
img_array = cv2.imread(args["image"], -1)
rgba_lower_bound = np.array([0, 59, 59,2], dtype = "uint8")
rgba_upper_bound = np.array([20, 255, 255,255], dtype = "uint8")
skinMask = cv2.inRange(img_array, rgb_lower_bound, rgb_upper_bound)
skin = cv2.bitwise_and(img_array, img_array, mask = skinMask)
cv2.imshow("images", skin)
Please help me with this.
Assuming R, G, B, A are all numpy arrays with the same shape, created by something like:
R, G, B, A = cv2.split(img_array)
simply create a mask using the same conditionals; since they're numpy arrays, use & instead of and:
mask = (R > 95) & (G > 40) & (B > 20) & (R > G) & (R > B) & (abs(R - G) > 15) & (A > 15)
Then to set everything not satisfying the condition to black:
img_array[~mask] = [0, 0, 0, 255]
Note here the mask will be two-channel, and will be broadcasted to all the channels in img_array. Also note ~ inverts a numpy boolean array, so this is indexing by wherever mask is False, which is what you want.
Some more info on transparency: if the alpha channel is 0, that means fully transparent, and if it's 255 (for a unsigned 8-bit image), that means opaque. If you want the image to be transparent at those locations instead of black, you can just invert the mask, turn it into a uint8 array, and then merge it back into one image, like so:
R, G, B, A = cv2.split(img_array)
mask = (R > 95) & (G > 40) & (B > 20) & (R > G) & (R > B) & (abs(R - G) > 15) & (A > 15)
new_A = 255*(~mask).astype(np.uint8)
new_img_array = cv2.merge([R, G, B, new_A])
This way you're not losing any of the color information in R, G, B should you want to keep it.
You could do something like this:
def set_to_black(T, image):
# grab the image dimensions
h = image.shape[0]
w = image.shape[1]
# loop over the image, pixel by pixel
for y in range(0, h):
for x in range(0, w):
if (conditional) # I couldn't be bothered to write it all out
# Set the pixel to black
image[y, x] = [0, 0, 0, 255]
# return the thresholded image
return image

Denoise a "Lang-Stereotest"

I'm trying to denoise a "Lang-Stereotest" (so it's called in Germany...) like this one:
here
I have used some filters as you can see in my source code:
(some code before...)
# Blur
output = cv2.blur(image, (10, 10))
img = Image.fromarray(output, 'RGB')
img.save("images/Filters/" + filePath.split('/')[1].split('.')[0] + " - Blur.jpg")
# Bilareal
output = cv2.bilateralFilter(image, 50, 50, 50)
img = Image.fromarray(output, 'RGB')
img.save("images/Filters/" + filePath.split('/')[1].split('.')[0] + " - Bilateral.jpg")
# MedianBlur
output = cv2.medianBlur(image, 5)
img = Image.fromarray(output, 'RGB')
img.save("images/Filters/" + filePath.split('/')[1].split('.')[0] + " - MedianBlur.jpg")
# Weighted
output = cv2.addWeighted(image, 5, image, -5, 128)
img = Image.fromarray(output, 'RGB')
img.save("images/Filters/" + filePath.split('/')[1].split('.')[0] + " - Weighted.jpg")
# Try to combine...
output = ... # here I want to combine the filters to gain best results..
img.save("images/Filters/" + filePath.split('/')[1].split('.')[0] + " - Best.jpg")
(some code after...)
As a result I got Bilateral:
[Blur], [Median Blur]
(I'll add "Blur" and "Median Blur" once I hit 10 reputation.... Sorry)
Ofcourse the results are far away from perfect and I also know, that there is no hundred percent solution but I think that it should significantly better..
Maybe someone of you have an idea on how to get a better result!
I have two approaches in mind
FIRST - Brute-Force approach
Here I manually set a threshold level below which all pixel values are 0 i.e; black
ret,th = cv2.threshold(gray, 100, 255, 1)
It looks pretty OK. But we can go further.
SECOND - Calculative approach
Here I set a threshold based on the median value of the gray scale image. This is a method statisticians use for separating data into different classes in data science. So I thought 'Why not try it out for images?'
Here is the code snippet for that:
sigma = 0.33
v = np.median(gray)
threshold = (1.0 - sigma) * v
for i in range(gray1.shape[0]):
for j in range(gray1.shape[1]):
if (gray[i, j] < threshold):
gray1[i, j] = 0
else:
gray[i, j] = 255
cv2.imwrite('gray1.jpg',gray1)
Yes, it does not look so perfect, but this is where I could go.
From here on it is up to you. You can apply medianfiltering followed by somemorphological` operations to attain what you want.
EDIT
I just copied the gray image into gray1 as reference to be used in the for loop.
Here is the complete code for a better understanding:
import cv2
import numpy as np
filename = '1.jpg'
img = cv2.imread(filename)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
gray1 = gray
sigma = 0.33
v = np.median(gray)
threshold = (1.0 - sigma) * v
for i in range(gray1.shape[0]):
for j in range(gray1.shape[1]):
if (gray[i, j] < threshold):
gray1[i, j] = 0
else:
gray[i, j] = 255
cv2.imwrite('gray1.jpg',gray1)
Hope this helped!!!!!!
:)
This is in response to your second image.
I performed histogram equalization of the gray scale image as mentioned in the comments:
equ = cv2.equalizeHist(gray)
I then applied binary threshold followed by dilation:
ret,th = cv2.threshold(equ, 50, 255, 0)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
dilate = cv2.morphologyEx(th, cv2.MORPH_DILATE, kernel, 3)
To reduce noise and spores in the image:
close = cv2.morphologyEx(dilate, cv2.MORPH_CLOSE, kernel, 3)
I inverted the image followed by morphological close:
ret,th1 = cv2.threshold(close, 50, 255, 1)
kernel1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
opened = cv2.morphologyEx(th1, cv2.MORPH_CLOSE, kernel1, 3)
I then performed morphological dilation:
dd = cv2.morphologyEx(opened, cv2.MORPH_DILATE, kernel1, 3)
This is the maximum I could get to.
Now you can find contours and eliminate the small dots falling below a certain area.
:)

Is there any function in openCV or another library that can tile squares within an arbitrary contour?

I have images where I've found some contours around dogs, e.g.:
I want to tile squares/rectangles inside of the contour. Is there an openCV (or other library) function for this? I'm using Python. I'd like it to look something like this:
I was able to solve this by first drawing rectangles over the entire image, then checking which ones were in the area with the dog:
# the image here is stored as the variable fg
# with b, g, r, and alpha channels
# the alpha channel is masking the dog part of the image
import cv2
b, g, r, a = cv2.split(fg)
fgcp = fg.copy()
h, w = fg.shape[:2]
h -= 1
w -= 1 # avoid indexing error
rectDims = [10, 10] # w, h of rectangles
hRects = h / rectDims[0]
wRects = w / rectDims[1]
for i in range(wRects):
for j in range(hRects):
pt1 = (i * rectDims[0], j * rectDims[1])
pt2 = ((i + 1) * rectDims[0], (j + 1) * rectDims[1])
# alpha is 255 over the part of the dog
if a[pt1[1], pt1[0]] == 255 and a[pt2[1], pt2[0]] == 255:
cv2.rectangle(fgcp, pt1, pt2, [0, 0, 255], 2)
cv2.imshow('', fgcp), cv2.waitKey(0)
It's not necessarily the ideal implementation, but it works well enough.

Categories