The built-in MacOS screen capture program (command-shift-4) has a nice feature where you can hit the spacebar and capture just a window, like this:
I would like to programmatically look at a directory of images (they are PNGs), determinate if they have the shadow, and automatically crop it. I need this to run on a Mac. I'd like to write this in Python. I am told that Pillow is the correct way to manage images in Python now, but I'm not sure how to read individual pixel and to crop images.
Here are some recommendations regardless of the library you will be using.
There are invariants on the window: 4 corners, title bar with mostly uniform color and 3 disk-shaped buttons.
If you can detect the buttons and the title bar, you can easily find the top corners. The bottom corners are symmetrical to the top corners.
A possible solution
Apply Hough transform to find circles
Find 3 consecutive circles along the horizontal axis (the buttons)
Apply Hough transform to find vertical and horizontal lines
Find a quad containing the 3 circles (title bar)
The 2 top corners of the window are located around the top corners of the title bar.
Form a patch by taking a neighborhood around a corner
Apply an edge detection algorithm in the patch
Reflect the pixels of the patch vertically
Apply patch matching vertically. For example with DP
Repeat the matching for the 2 top corners to find the bottom ones
With the 4 corner you know the bounding box of the window and you can solve the cropping problem
Here is code that uses Python Image Library and Python 2.7 to do the trick:
#!/usr/bin/env
# Removes the shadow from MacOS-Generated screen shots.
import Image,os
if __name__=="__main__":
image = Image.open(os.sys.argv[1])
image = image.convert('RGBA')
(width,height) = image.size
def find_first_non_alpha_x():
for i in range(width):
if image.getpixel((i,height/2))[3]==255:
return i
raise RuntimeError("No non-alpha pixels on midline")
def find_last_non_alpha_x():
for i in range(width-1,0,-1):
if image.getpixel((i,height/2))[3]==255:
return i
raise RuntimeError("No non-alpha pixels on midline")
def find_first_non_alpha_y():
for i in range(height):
if image.getpixel((width/2,i))[3]==255:
return i
raise RuntimeError("No non-alpha pixels on midline")
def find_last_non_alpha_y():
for i in range(height-1,0,-1):
if image.getpixel((width/2,i))[3]==255:
return i
raise RuntimeError("No non-alpha pixels on midline")
x1 = find_first_non_alpha_x()
y1 = find_first_non_alpha_y()
x2 = find_last_non_alpha_x()
y2 = find_last_non_alpha_y()
y = image.crop((x1-1,y1-1,x2+1,y2+1))
y.save(os.sys.argv[1]+"-cropped.png")
Related
So after I've done object detection, I have a few overlapping 2d rectangles, and I want to find size of the area outside of all the rectangles I've created, which has (xyxy) coordinates on each one. How do I find size of area outside that boxes in python?
enter image description here
I have tried to calculate the area by adding the sizes of all the bounding boxes then the sum will be used to subtract the size from the input image. However, it is still constrained by overlapping square lengths so that the measurement is not accurate.
from bbox import BBox2D
box1 = BBox2D([0.20212765957446807, 0.145625, 0.24822695035460993, 0.10875])
box2 = BBox2D([0.6693262411347518, 0.146875, 0.31382978723404253, 0.06875])
print(box2.height * box2.width)
print(box1.height * box1.width)
Try this one.
or simply u can
def _getArea(box):
return (box[2] - box[0]) * (box[3] - box[1])
This should do the trick. Structure of Box: [xmin,ymin,xmax,ymax]
To find the area outside all of the boxes, you will need to do the following things.
Find the area of the entire image
Find the area of all of the rectangles (including overlapping area)
Find the total overlapping area
Subtract the total overlapping area from the total area of the rectangles
Subtract the rectangle area without overlap from the area of the entire image
To find the total overlapping area, you will need to loop through every rectangle on the screen and check if it is overlapping. If it is overlapping another rectangle you will need to find the corners that are overlapping each other. For example in your image, the top left and middle rectangles are overlapping. The two corners you need would be the bottom right corner of the top left rectangle and the top left corner of the middle rectangle. You can then find the area of the rectangle that is made by the overlap.
If you add a little more information in your question, I could type out a simple function that could do this but right now I don't know all the specifications.
How can I highlight part of image? (Location defined as tuple of 4 numbers). You can imagine it like I have image of pc motherboard, and I need to highlight for example part where CPU Socket is located.
Note that for Python 3, you need to use the pillow fork of PIL, which is a mostly backwards compatible fork of the original module but, unlike it, is currently actively being maintained.
Here's some sample code that shows how to do it using the PIL.ImageEnhance.Brightness class.
Doing what you want requires multiple steps:
The portion to be highlighted is cut out of — or cropped from — the image.
An instance of the Brightness class is created from this cropped image.
The cropped image is the lightened by calling the enhance() method of the Brightness instance.
The cropped and now lightened image is pasted back into the location it came from.
To make doing them all easier to repeat, below is a function named highlight_area() to perform them.
Note that I've also added a bonus feature that will optionally outline the highlighted region with a colored border — which you can of course remove if you don't need or want it.
from PIL import Image, ImageColor, ImageDraw, ImageEnhance
def highlight_area(img, region, factor, outline_color=None, outline_width=1):
""" Highlight specified rectangular region of image by `factor` with an
optional colored boarder drawn around its edges and return the result.
"""
img = img.copy() # Avoid changing original image.
img_crop = img.crop(region)
brightner = ImageEnhance.Brightness(img_crop)
img_crop = brightner.enhance(factor)
img.paste(img_crop, region)
# Optionally draw a colored outline around the edge of the rectangular region.
if outline_color:
draw = ImageDraw.Draw(img) # Create a drawing context.
left, upper, right, lower = region # Get bounds.
coords = [(left, upper), (right, upper), (right, lower), (left, lower),
(left, upper)]
draw.line(coords, fill=outline_color, width=outline_width)
return img
if __name__ == '__main__':
img = Image.open('motherboard.jpg')
red = ImageColor.getrgb('red')
cpu_socket_region = 110, 67, 274, 295
img2 = highlight_area(img, cpu_socket_region, 2.5, outline_color=red, outline_width=2)
img2.save('motherboard_with_cpu_socket_highlighted.jpg')
img2.show() # Display the result.
Here's an example of using the function. The original image is shown on the left opposite the one resulting from calling the function on it with the values shown in the sample code.
I'm trying to find a way to transform an image by translating one of its vertexes.
I have already found various methods for transforming an image like rotation and scaling, but none of the methods involved skewing like so:
There is shearing, but it's not the same since it can move two or more of the image's vertex while I only want to move one.
What can I use that can perform such an operation?
I took your "cat-thing" and resized it to a nice size, added some perfectly vertical and horizontal white gridlines and added some extra canvas in red at the bottom to give myself room to transform it. That gave me this which is 400 pixels wide and 450 pixels tall:
I then used ImageMagick to do a "Bilinear Forward Transform" in Terminal. Basically you give it 4 pairs of points, the first pair is where the top-left corner is before the transform and then where it must move to. The next pair is where the top-right corner is originally followed by where it ends up. Then the bottom-right. Then the bottom-left. As you can see, 3 of the 4 pairs are unmoved - only the bottom-right corner moves. I also made the virtual pixel black so you can see where pixels were invented by the transform in black:
convert cat.png -matte -virtual-pixel black -interpolate Spline -distort BilinearForward '0,0 0,0 399,0 399,0 399,349 330,430 0,349 0,349' bilinear.png
I also did a "Perspective Transform" using the same transform coordinates:
convert cat.png -matte -virtual-pixel black -distort Perspective '0,0 0,0 399,0 399,0 399,349 330,430 0,349 0,349' perspective.png
Finally, to illustrate the difference, I made a flickering comparison between the 2 images so you can see the difference:
I am indebted to Anthony Thyssen for his excellent work here which I commend to you.
I understand you were looking for a Python solution and would point out that there is a Python binding to ImageMagick called Wand which you may like to use - here.
Note that I only used red and black to illustrate what is going on (atop the Stack Overflow white background) and where aspects of the result come from, you would obviously use white for both!
The perspective transformation is likely what you want, since it preserves straight lines at any angle. (The inverse bilinear only preserves horizontal and vertical straight lines).
Here is how to do it in ImageMagick, Python Wand (based upon ImageMagick) and Python OpenCV.
Input:
ImageMagick
(Note the +distort makes the output the needed size to hold the full result and is not restricted to the size of the input. Also the -virtual-pixel white sets color of the area outside the image pixels to white. The points are ordered clockwise from the top left in pairs as inx,iny outx,outy)
convert cat.png -virtual-pixel white +distort perspective \
"0,0 0,0 359,0 359,0 379,333 306,376 0,333 0,333" \
cat_perspective_im.png
Python Wand
(Note the best_fit=true makes the output the needed size to hold the full result and is not restricted to the size of the input.)
#!/bin/python3.7
from wand.image import Image
from wand.display import display
with Image(filename='cat.png') as img:
img.virtual_pixel = 'white'
img.distort('perspective', (0,0, 0,0, 359,0, 359,0, 379,333, 306,376, 0,333, 0,333), best_fit=True)
img.save(filename='cat_perspective_wand.png')
display(img)
Python OpenCV
#!/bin/python3.7
import cv2
import numpy as np
# Read source image.
img_src = cv2.imread('cat.png')
# Four corners of source image
# Coordinates are in x,y system with x horizontal to the right and y vertical downward
pts_src = np.float32([[0,0], [359,0], [379,333], [0,333]])
# Four corners of destination image.
pts_dst = np.float32([[0, 0], [359,0], [306,376], [0,333]])
# Get perspecive matrix if only 4 points
m = cv2.getPerspectiveTransform(pts_src,pts_dst)
# Warp source image to destination based on matrix
# size argument is width x height
# compute from max output coordinates
img_out = cv2.warpPerspective(img_src, m, (359+1,376+1), cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255))
# Save output
cv2.imwrite('cat_perspective_opencv.png', img_out)
# Display result
cv2.imshow("Warped Source Image", img_out)
cv2.waitKey(0)
cv2.destroyAllWindows()
How to draw a dotted or dashed line or rectangle with Python PILLOW. Can anyone help me? Using openCV I can do that. But I want it using Pillow.
Thanks to #martineau's comment, I figured out how to draw a dotted line. Here is my code.
cur_x = 0
cur_y = 0
image_width = 600
for x in range(cur_x, image_width, 4):
draw.line([(x, cur_y), (x + 2, cur_y)], fill=(170, 170, 170))
This will draw a dotted line of gray color.
I decided to write up the idea I suggested in the comments - namely to draw the shapes with solid lines and then overlay a thresholded noisy image to obliterate parts of the line.
I made all the noise on a smaller image and then scaled it up so that the noise was "more clumped" instead of tiny blobs.
So this is just the generation of the test image:
#!/usr/local/bin/python3
import numpy as np
from PIL import Image, ImageDraw
# Make empty black image
im = Image.new('L', (640,480))
# Draw white rectangle and ellipse
draw = ImageDraw.Draw(im)
draw.rectangle([20,20,620,460],outline=255)
draw.ellipse([100,100,540,380],outline=255)
And this is generating the noise overlay and overlaying it - you can just delete this sentence and join the two lumps of code together:
# Make noisy overlay, 1/4 the size, threshold at 50%, scale up to full-size
noise = np.random.randint(0,256,(120,160),dtype=np.uint8)
noise = (noise>128)*255
noiseim = Image.fromarray(noise.astype(np.uint8))
noiseim = noiseim.resize((640,480), resample=Image.NEAREST)
# Paste the noise in, but only allowing the white shape outlines to be affected
im.paste(noiseim,mask=im)
im.save('result.png')
The result is this:
The solidly-drawn image is like this:
The noise is like this:
The following function draws a dashed line. It might be slow, but it works and I needed it.
"dashlen" is the length of the dashes, in pixels. -
"ratio" is the ratio of the empty space to the dash length (the higher the value the more empty space you get)
import math # math has the fastest sqrt
def linedashed(x0, y0, x1, y1, dashlen=4, ratio=3):
dx=x1-x0 # delta x
dy=y1-y0 # delta y
# check whether we can avoid sqrt
if dy==0: len=dx
elif dx==0: len=dy
else: len=math.sqrt(dx*dx+dy*dy) # length of line
xa=dx/len # x add for 1px line length
ya=dy/len # y add for 1px line length
step=dashlen*ratio # step to the next dash
a0=0
while a0<len:
a1=a0+dashlen
if a1>len: a1=len
draw.line((x0+xa*a0, y0+ya*a0, x0+xa*a1, y0+ya*a1), fill = (0,0,0))
a0+=step
I know this question is a bit old (4 y.o. at the time of my writing this answer), but as it happened I was in need of drawing a patterned line.
So I concocted my own solution here: https://codereview.stackexchange.com/questions/281582/algorithm-to-traverse-a-path-through-several-data-points-and-draw-a-patterned-li
(Sorry the solution was a bit long, better to just look there. The code works, though, that's why it's in CodeReview SE).
Provide the right "pattern dictionary", where blank segments are represented by setting color to None, and you should be good to go.
I have been using the
x,y,w,h = cv2.boundingBox(cnt)
roi = img[y:y+h,x:x+w]
function in openCv in order to get a portion of the image within a contour.
However, I am now trying to use the openCv function minAreaRect in order to get my bounding box. The function returns the x and y coordinates of each corner, along with the skew angle. Example:
((363.5, 676.0000610351562), (24.349538803100586, 34.46882629394531), -18.434947967529297)
Is there a simple way of extracting this portion of the image? Because I can obviously not do
roi = img[y:y+h,x:x+w]
Because of the skew angle and such. I was going to possibly rotate the entire image and extract the points, but this would take far too long when going through thousands of contours at once.
What I currently get is encompassed within the green rectangle, and what I want is in the red rectangle. I want to extract this portion of the image, but cannot figure out how to select a diagonal rectangle.