Skip to content

Reference

Image Utilities

Provide serveral Image Utility functions.

This module allows the user to use several of the most commonly used image handling functions at Miro AI. Some functions are documented further in the Example section.

Examples:

>>> from toolbox.im_utils import get_pil_im
>>> get_pil_im("https://random.imagecdn.app/500/150").size
(500, 150)

This module contains the following functions:

  • get_pil_im(fp_url_nparray)- Returns a PIL Image object from either a file path, URL, or numpy array

get_im(fp_url_nparray, b_pil_im=False)

returns an image (wrapper around get_pil_im)

Parameters:

Name Type Description Default
fp_url_nparray Union[str, np.ndarray, Image.Image]

filepath, URL, numpy array, or even an PIL Image object

required
b_pil_im bool

if True, returns a PIL Image Object

False

Returns:

Type Description
Union[Image.Image, np.ndarray, None]

An Image represented either in numpy array or as an PIL Image object

Raises:

Type Description
ValueError

An error when fp_url_nparray cannot be converted into an image

Examples:

>>> get_im("https://random.imagecdn.app/500/150").shape
(150, 500, 3)
Source code in toolbox/im_utils.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def get_im(fp_url_nparray: Union[str,np.ndarray,Image.Image], b_pil_im: bool = False) -> Union[Image.Image, np.ndarray, None]:
	''' returns an image (wrapper around get_pil_im)

	Args:
		fp_url_nparray: filepath, URL, numpy array, or even an PIL Image object
		b_pil_im: if True, returns a PIL Image Object

	Returns:
		An Image represented either in numpy array or as an PIL Image object

	Raises:
		ValueError: An error when fp_url_nparray cannot be converted into an image

	Examples:
		>>> get_im("https://random.imagecdn.app/500/150").shape
		(150, 500, 3)
	'''
	pil_im = get_pil_im(fp_url_nparray)
	return pil_im if b_pil_im else np.array(pil_im)

get_pil_im(fp_url_nparray)

return a PIL image object

Parameters:

Name Type Description Default
fp_url_nparray Union[str, np.ndarray, Image.Image]

filepath, URL, numpy array, or even an PIL Image object

required

Returns:

Type Description
Union[Image.Image, None]

A PIL Image object

Raises:

Type Description
ValueError

An error when fp_url_nparray cannot be converted into an image

Examples:

>>> get_pil_im("https://random.imagecdn.app/500/150").size
(500, 150)
Source code in toolbox/im_utils.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def get_pil_im(fp_url_nparray: Union[str,np.ndarray,Image.Image]) -> Union[Image.Image, None]:
	''' return a PIL image object

	Args:
		fp_url_nparray: filepath, URL, numpy array, or even an PIL Image object

	Returns:
		A PIL Image object

	Raises:
		ValueError: An error when fp_url_nparray cannot be converted into an image

	Examples:
		>>> get_pil_im("https://random.imagecdn.app/500/150").size
		(500, 150)
	'''
	import urllib.request as urllib
	from urllib.error import HTTPError, URLError

	im = fp_url_nparray
	pil_im = None
	if isinstance(im, Image.Image):
		pil_im = im
	elif type(im) == np.ndarray:
		pil_im = Image.fromarray(im)
	elif os.path.isfile(im):
		pil_im = Image.open(im)
	elif validators.url(im):
		try:
			r = urllib.Request(im, headers = {'User-Agent': "Miro's Magic Image Broswer"})
			con = urllib.urlopen(r)
			pil_im = Image.open(io.BytesIO(con.read()))
		except HTTPError as e:
			warnings.warn(f'get_pil_im: error getting {im}\n{e}')
		except URLError as e:
			warnings.warn(f'get_pil_im: URL error using {im}\n{e}')
	else:
		raise ValueError(f'get_im: im must be np array, filename, or url')
	return pil_im

im_3c(im)

returns a 3-channel (rgb) image in numpy array

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required

Returns:

Type Description
np.ndarray

a numpy array of shape: (h, w, 3)

Examples:

>>> im = get_im("https://picsum.photos/200/300?grayscale")
>>> im_3c(im).shape
(300, 200, 3)
Source code in toolbox/im_utils.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def im_3c(im:Any)-> np.ndarray:
	'''returns a 3-channel (rgb) image in numpy array

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath

	Returns:
		a numpy array of shape: (h, w, 3)

	Examples:
		>>> im = get_im("https://picsum.photos/200/300?grayscale")
		>>> im_3c(im).shape
		(300, 200, 3)
	'''
	im_rgb_array = get_im(im)
	if im_isgray(im_rgb_array, b_check_rgb = False) :
		im = pil_im_gray2rgb(get_pil_im(im_rgb_array))
		im_rgb_array = np.array(im)
	return im_rgb_array[:,:,:3]

im_add_alpha(im)

add an empty alpha channel for a 3-channel image

for adding an alpha channel with an actual mask, see im_apply_mask

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required

Returns:

Type Description
np.ndarray

a numpy array of shape: (h, w, 4)

Raises:

Type Description
AssertionError

An error will be raise if numpy array is not of shape (h,w,3)

Examples:

>>> im = get_im("https://picsum.photos/200/300")
>>> im_add_alpha(im).shape
(300, 200, 4)
Source code in toolbox/im_utils.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def im_add_alpha(im: Any) -> np.ndarray:
	''' add an **empty alpha channel** for a  3-channel image

	for adding an alpha channel with an actual mask, see `im_apply_mask`

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath

	Returns:
		a numpy array of shape: (h, w, 4)

	Raises:
		AssertionError: An error will be raise if numpy array is not of shape (h,w,3)

	Examples:
		>>> im = get_im("https://picsum.photos/200/300")
		>>> im_add_alpha(im).shape
		(300, 200, 4)
	'''
	im_rgb_array = get_im(im)
	assert len(im_rgb_array.shape) > 2, f'im_rgb_array shape is only length {len(im_rgb_array.shape)}'
	h,w,c = im_rgb_array.shape
	assert c == 3, f'im_rgb_array mmust be 3-channel, found {c} channels'
	return np.concatenate((
			im_rgb_array, np.ones((h, w, 1))+254
			), axis =2).astype('uint8')

im_apply_mask(im, mask_array, mask_gblur_radius=0, bg_rgb_tup=None, bg_blur_radius=None, bg_greyscale=False)

our more advanced image operation with mask implemented using PIL

With a mask, this function can create four different effects: background blurring, background removal (replace with a solid color), greyscaling background, and creating a 4-channel image. Additionally, a blur can be applied to the given mask to soften the edges. For details on picking a sensible blur radius see here. Note that only ONE of [bg_rgb_tup, bg_blur_radius, bg_greyscale] should be provided.

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
mask_array np.ndarray

a numpy array that should be of the same width-height as im

required
mask_gblur_radius int

mask's gaussian blur radius to soften the edges of the mask. Implemented using PIL, defaults to 0 (i.e. no blur applied).

0
bg_rgb_tup Tuple[int, int, int]

if given, return a 3-channel image with color background instead of transparent

None
bg_blur_radius int

if given, return a 3-channel image with GaussianBlur applied to the background

None
bg_greyscale bool

color of the background (part outside the mask) in RGB

False

Returns:

Type Description
np.ndarray

a numpy array in the shape of (h, w, 3) if bg_rgb_tup or bg_blur_radius or bg_greyscale is provided; otherwise a numpy array of shape (h, w, 4) will be returned.

Raises:

Type Description
AssertionError

An error will be raise if image's width-height is not the same as the mask's width-height, or if more than one of [bg_rgb_tup, bg_blur_radius, bg_greyscale] are provided.

Examples:

>>> im = get_im("https://picsum.photos/200/300")
>>> mask = np.zeros(im.shape[:2], dtype = np.uint8)
>>> mask[100:200, 50:150] = 1
>>> im_masked_green = im_apply_mask(im, mask_array = mask, bg_rgb_tup = (50,205,50))
>>> im_masked_gs = im_apply_mask(im, mask_array = mask, bg_greyscale = True)
>>> im_masked_blur = im_apply_mask(im, mask_array = mask, bg_blur_radius = 5, mask_gblur_radius = 2)
>>> im_masked_vanilla = im_apply_mask(im, mask_array = mask)
>>> simple_im_diff(im, im_masked_green)
True
>>> simple_im_diff(im_masked_green, im_masked_gs)
True
>>> simple_im_diff(im_masked_gs, im_masked_blur)
True
>>> im_masked_gs.shape != im_masked_vanilla.shape
True
>>> im_masked_vanilla.shape
(300, 200, 4)
Source code in toolbox/im_utils.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
def im_apply_mask(im : Any, mask_array: np.ndarray, mask_gblur_radius: int = 0,
		bg_rgb_tup: Tuple[int,int,int] = None, bg_blur_radius: int = None,
		bg_greyscale: bool = False,
	) -> np.ndarray:
	''' our more advanced image operation with mask [implemented using PIL](https://stackoverflow.com/questions/47723154/how-to-use-pil-paste-with-mask)

	With a mask, this function can create four different effects:
	background blurring, background removal (replace with a solid color),
	greyscaling background, and creating a 4-channel image.
	Additionally, a blur can be applied to the given mask to
	soften the edges. For details on picking a sensible blur radius
	[see here](https://stackoverflow.com/questions/62968174/for-pil-imagefilter-gaussianblur-how-what-kernel-is-used-and-does-the-radius-par).
	Note that **only ONE** of `[bg_rgb_tup, bg_blur_radius, bg_greyscale]`
	should be provided.

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		mask_array: a numpy array that should be of the same width-height as `im`
		mask_gblur_radius: mask's gaussian blur radius to soften the edges of the mask. [Implemented using PIL](https://stackoverflow.com/questions/62273005/compositing-images-by-blurred-mask-in-numpy), defaults to 0 (i.e. no blur applied).
		bg_rgb_tup: if given, return a 3-channel image with color background instead of transparent
		bg_blur_radius: if given, return a 3-channel image with GaussianBlur applied to the background
		bg_greyscale: color of the background (part outside the mask) in RGB

	Returns:
		a numpy array in the shape of (h, w, 3) if `bg_rgb_tup` or `bg_blur_radius` or `bg_greyscale` is provided; otherwise a numpy array of shape (h, w, 4) will be returned.

	Raises:
		AssertionError: An error will be raise if image's width-height is not the same as the mask's width-height, or if more than one of `[bg_rgb_tup, bg_blur_radius, bg_greyscale]` are provided.

	Examples:
		>>> im = get_im("https://picsum.photos/200/300")
		>>> mask = np.zeros(im.shape[:2], dtype = np.uint8)
		>>> mask[100:200, 50:150] = 1
		>>> im_masked_green = im_apply_mask(im, mask_array = mask, bg_rgb_tup = (50,205,50))
		>>> im_masked_gs = im_apply_mask(im, mask_array = mask, bg_greyscale = True)
		>>> im_masked_blur = im_apply_mask(im, mask_array = mask, bg_blur_radius = 5, mask_gblur_radius = 2)
		>>> im_masked_vanilla = im_apply_mask(im, mask_array = mask)
		>>> simple_im_diff(im, im_masked_green)
		True
		>>> simple_im_diff(im_masked_green, im_masked_gs)
		True
		>>> simple_im_diff(im_masked_gs, im_masked_blur)
		True
		>>> im_masked_gs.shape != im_masked_vanilla.shape
		True
		>>> im_masked_vanilla.shape
		(300, 200, 4)
	'''
	im_rgb_array = get_im(im)
	p_im = Image.fromarray(im_rgb_array)
	h, w, c = im_rgb_array.shape
	assert (h,w) == mask_array.shape[:2], \
		f"mask_array height-width {mask_array.shape} must match im_rgb_array's {(h, w)}"
	assert not(all([bg_rgb_tup, bg_blur_radius, bg_greyscale])), \
		f"only one of bg_rgb_tup, bg_blur_radius, or bg_greyscale call be specified."

	# convert bitwise mask from np to pillow
	# ref: https://note.nkmk.me/en/python-pillow-paste/
	pil_mask = Image.fromarray(np.uint8(255* mask_array))
	pil_mask = pil_mask.filter(
					ImageFilter.GaussianBlur(radius = mask_gblur_radius)
				) if mask_gblur_radius > 0 else pil_mask

	if bg_rgb_tup:
		bg_im = np.zeros([h,w,3], dtype = np.uint8) # black
		bg_im[:,:] = bg_rgb_tup						# apply color

		bg_im = Image.fromarray(bg_im)
		bg_im.paste(p_im, mask = pil_mask)
		p_im = bg_im
	elif bg_blur_radius:
		bg_im = p_im.copy().filter(
					ImageFilter.GaussianBlur(radius = bg_blur_radius)
				)
		bg_im.paste(p_im, mask = pil_mask)
		p_im = bg_im
	elif bg_greyscale:
		bg_im = ImageOps.grayscale(p_im)
		bg_im = np.array(bg_im)
		bg_im = np.stack((bg_im,)*3, axis = -1) 	# greyscale 1-channel to 3-channel

		bg_im =  Image.fromarray(bg_im)
		bg_im.paste(p_im, mask = pil_mask)
		p_im = bg_im
	else:
		p_im.putalpha(pil_mask)

	return np.array(p_im)
#
# def mask_overlap(base_mask, over_mask, get_overlap_mask = False):
# 	'''
# 	compute the percentage of mask union
# 	Args:
# 		get_overlap_mask: if true it will return a mask of only the union
# 	'''
# 	if base_mask.shape != over_mask.shape:
# 		raise ValueError(f'mask_overlap: base_mask shape {base_mask.shape} does not match over_mask {over_mask.shape}')
#
# 	overlap = np.logical_and(base_mask!= 0, over_mask != 0)
# 	score = (overlap== True).sum() / np.count_nonzero(base_mask)
# 	return overlap.astype(np.uint8) if get_overlap_mask else float(score)
#
# def join_binary_masks(list_of_np_binary_masks):
# 	l_masks = list_of_np_binary_masks
# 	for mk in l_masks:
# 		if mk.shape != l_masks[0].shape:
# 			raise ValueError(f'join_binary_masks: all masks must be of the same shape')
#
# 	out_mask = l_masks[0]
# 	for mk in l_masks[1:]:
# 		out_mask += mk
# 	return np.array(out_mask!=0).astype(np.uint8)
#
# def mask_bbox(input_mask, get_json = False):
# 	'''
# 	get the minimum bounding box of a np binary mask
# 	returns y0,y1,x0,x1
# 	'''
# 	rows = np.any(input_mask, axis=1) # y-axis
# 	cols = np.any(input_mask, axis=0) # x-axis
# 	rmin, rmax = np.where(rows)[0][[0, -1]]
# 	cmin, cmax = np.where(cols)[0][[0, -1]]
# 	rmin, rmax, cmin, cmax = list(map(int,[rmin,rmax,cmin,cmax]))
# 	return {'x0': cmin, 'x1': cmax, 'y0': rmin, 'y1': rmax} if get_json else (rmin, rmax, cmin, cmax)
#
#
#
# def plot_colors(hist, centroids, w= 300 , h = 50):
# 	'''
# 	return a pil_im of color given in centroids
# 	'''
# 	# initialize the bar chart representing the relative frequency of each of the colors
# 	bar = np.zeros((50, 300, 3), dtype = "uint8")
# 	startX = 0
#
# 	im = Image.new('RGB', (300, 50), (128, 128, 128))
# 	draw = ImageDraw.Draw(im)
#
# 	# loop over the percentage of each cluster and the color of
# 	# each cluster
# 	for (percent, color) in zip(hist, centroids):
# 		# plot the relative percentage of each cluster
# 		endX = startX + (percent * 300)
# 		xy = (int(startX), 0, int(endX), 50)
# 		fill = tuple(color.astype('uint8').tolist())
# 		draw.rectangle(xy, fill)
# 		startX = endX
#
# 	# return the bar chart
# 	im.resize( (w,h))
# 	return im
#
# def gamma_adjust(pil_im, gamma = 1):
# 	'''
# 	return a PIL Image with gamma correction (brightness)
# 	see: https://www.pyimagesearch.com/2015/10/05/opencv-gamma-correction/
# 	and code: https://note.nkmk.me/en/python-numpy-image-processing/
# 	'''
# 	im = np.array(pil_im)
# 	im_out = 255.0 * (im/ 255.0)**(1/gamma)
# 	return Image.fromarray(im_out.astype(np.uint8))

im_centre_pad(im, target_wh)

resize an image to target_wh while keeping aspect ratio by padding the borders to keep it centered

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
target_wh Tuple[int, int]

target width height in a Tuple

required

Returns:

Type Description
np.ndarray

a numpy array

Examples:

>>> im = get_im("https://picsum.photos/200/300")
>>> im_centre_pad(im, target_wh = (300,300)).shape
(300, 300, 3)
Source code in toolbox/im_utils.py
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
def im_centre_pad(im: Any, target_wh: Tuple[int,int] ) -> np.ndarray:
	'''resize an image to target_wh while keeping aspect ratio by [padding the borders to keep it centered](https://jdhao.github.io/2017/11/06/resize-image-to-square-with-padding/)

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		target_wh: target width height in a Tuple

	Returns:
		a numpy array

	Examples:
		>>> im = get_im("https://picsum.photos/200/300")
		>>> im_centre_pad(im, target_wh = (300,300)).shape
		(300, 300, 3)
	'''
	p_im = get_pil_im(im)
	org_wh = p_im.size
	ratio = max(target_wh)/ max(org_wh)
	new_wh = tuple([int(x * ratio) for x in org_wh])
	p_im = p_im.resize(new_wh, Image.LANCZOS)
	out_im =  Image.new('RGB', target_wh)

	w,h = target_wh
	w_, h_ = new_wh
	paste_xy = ((w-w_)//2, (h-h_)//2)
	out_im.paste(p_im, paste_xy)
	return np.array(out_im)

im_color_mask(im, mask_array, rgb_tup=(91, 86, 188), alpha=0.5)

draw mask over the input image

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
mask_array np.ndarray

a numpy array that should be of the same width-height as im

required
rgb_tup Tuple[int, int, int]

color of the mask in RGB

(91, 86, 188)
alpha float

level of transparency 0 being totally transparent, 1 solid color

0.5

Returns:

Type Description
np.ndarray

a numpy array in the shape of (h, w, 3)

Raises:

Type Description
AssertionError

An error will be raise if image's width-height is not the same as the mask's width-height

Examples:

>>> im = get_im("https://picsum.photos/200/300")
>>> mask = np.ones(im.shape[:2], dtype = np.uint8)
>>> im_masked = im_color_mask(im, mask_array = mask)
>>> simple_im_diff(im, im_masked)
True
>>> im_masked.shape == im.shape
True
Source code in toolbox/im_utils.py
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def im_color_mask(im: Any, mask_array: np.ndarray,
		rgb_tup: Tuple[int,int,int] = (91,86,188), alpha: float = 0.5
	) -> np.ndarray:
	''' draw mask over the input image

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		mask_array: a numpy array that should be of the same width-height as `im`
		rgb_tup: color of the mask in RGB
		alpha: level of transparency 0 being totally transparent, 1 solid color

	Returns:
		a numpy array in the shape of (h, w, 3)

	Raises:
		AssertionError: An error will be raise if image's width-height is not the same as the mask's width-height

	Examples:
		>>> im = get_im("https://picsum.photos/200/300")
		>>> mask = np.ones(im.shape[:2], dtype = np.uint8)
		>>> im_masked = im_color_mask(im, mask_array = mask)
		>>> simple_im_diff(im, im_masked)
		True
		>>> im_masked.shape == im.shape
		True
	'''
	im_rgb_array = get_im(im)
	assert im_rgb_array.shape[:2] == mask_array.shape[:2], \
		f'image is shape {im_rgb_array.shape[:2]} which is different than mask shape {mask_array.shape[:2]}'

	bg_im = np.zeros(im_rgb_array.shape, dtype = np.uint8) # create color
	bg_im[:,:]= rgb_tup
	im = Image.composite( Image.fromarray(bg_im), Image.fromarray(im_rgb_array),
						Image.fromarray(mask_array * int(alpha * 255))
						)
	return np.array(im)

im_crop(im, x0, y0, x1, y1)

crops an image (numpy array) given a bounding box coordinates

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
x0 int

top left x value

required
y0 int

top left y value

required
x1 int

bottom right x value

required
y1 int

bottom right y value

required

Returns:

Type Description
np.ndarray

An Image representation in an numpy array

Raises:

Type Description
AssertionError

An error will be raised for nonsensical x or y values

Examples:

>>> im = get_im("https://random.imagecdn.app/500/150")
>>> bbox = {'x0': 100, 'y0': 50, 'x1': 200, 'y1': 100}
>>> im_crop(im, **bbox).shape
(50, 100, 3)
Source code in toolbox/im_utils.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def im_crop(im: Any, x0: int, y0: int, x1:int, y1: int) -> np.ndarray:
	''' crops an image (numpy array) given a bounding box coordinates

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		x0: top left x value
		y0: top left y value
		x1: bottom right x value
		y1: bottom right y value

	Returns:
		An Image representation in an numpy array

	Raises:
		AssertionError: An error will be raised for nonsensical x or y values

	Examples:
		>>> im = get_im("https://random.imagecdn.app/500/150")
		>>> bbox = {'x0': 100, 'y0': 50, 'x1': 200, 'y1': 100}
		>>> im_crop(im, **bbox).shape
		(50, 100, 3)
	'''
	im_rgb_array = get_im(im)
	assert x1> x0, f'x1 ({x1}) cannot be greater than x0 ({x0})'
	assert y1> y0, f'y1 ({y1}) cannot be greater than y0 ({y0})'
	h,w,c = im_rgb_array.shape
	assert h>y0 and w>x0, f'x0 ({x0}) or y0 ({y0}) number out of bound w: {w}; h: {h}'
	return im_rgb_array[int(y0): int(y1), int(x0):int(x1), :]

im_draw_bbox(im, x0, y0, x1, y1, color='black', width=3, caption=None, use_bbv=False, bbv_label_only=False)

returns a numpy image of an image with bounding box drawn on top

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
x0 int

bounding box top-left x location

required
y0 int

bounding box top-left y location

required
x1 int

bounding box bottom-right x location

required
y1 int

bounding box bottom-right y location

required
color str

HTML color name as supported by PIL's ImageColor

'black'
width int

width of the bounding box lines

3
caption str

label for the bounding box

None
use_bbv bool

use the package bbox_visualizer instead of PIL

False
bbv_label_only bool

draw flag with label on the bounding box's centroid only

False

Returns:

Type Description
np.ndarray

a numpy array

Examples:

>>> im = get_im("https://picsum.photos/200/300")
>>> bbox = {'x0': 10, 'y0': 10, 'x1': 190, 'y1': 290}
>>> im_with_bbox = im_draw_bbox(im, caption = "hello world", **bbox)
>>> simple_im_diff(im, im_with_bbox)
True
Source code in toolbox/im_utils.py
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def im_draw_bbox(im: Any, x0: int, y0: int, x1: int, y1:int,
		color: str = 'black', width:int = 3, caption: str = None,
		use_bbv: bool = False, bbv_label_only: bool = False
	) -> np.ndarray:
	''' returns a numpy image of an image with bounding box drawn on top

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		x0: bounding box top-left x location
		y0: bounding box top-left y location
		x1: bounding box bottom-right x location
		y1: bounding box bottom-right y location
		color: [HTML color name](https://www.w3schools.com/colors/colors_names.asp) as supported by [PIL's ImageColor](https://pillow.readthedocs.io/en/stable/reference/ImageColor.html)
		width: width of the bounding box lines
		caption: label for the bounding box
		use_bbv: use the package [`bbox_visualizer`](https://github.com/shoumikchow/bbox-visualizer) instead of PIL
		bbv_label_only: draw flag with label on the bounding box's centroid only

	Returns:
		a numpy array

	Examples:
		>>> im = get_im("https://picsum.photos/200/300")
		>>> bbox = {'x0': 10, 'y0': 10, 'x1': 190, 'y1': 290}
		>>> im_with_bbox = im_draw_bbox(im, caption = "hello world", **bbox)
		>>> simple_im_diff(im, im_with_bbox)
		True
	'''
	x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1)
	if use_bbv:
		import bbox_visualizer as bbv
		im = get_im(im)
		if bbv_label_only:
			if caption:
				im_array = bbv.draw_flag_with_label(im,
								label = caption,
								bbox = [x0,y0,x1,y1],
								line_color = ImageColor.getrgb(color),
								text_bg_color = ImageColor.getrgb(color)
							)
			else:
				raise ValueError(f'im_draw_bbox: bbv_label_only is True but caption is None')
		else:
			im_array = bbv.draw_rectangle(im,
							bbox = [x0, y0, x1, y1],
							bbox_color = ImageColor.getrgb(color),
							thickness = int(width)
						)
			im_array = bbv.add_label(im_array, label = caption,
							bbox = [x0,y0,x1,y1],
							text_bg_color = ImageColor.getrgb(color)
						) if caption else im_array
	else:
		pil_im = get_pil_im(im).copy()
		draw = ImageDraw.Draw(pil_im)
		draw.rectangle([(x0, y0), (x1, y1)], outline = color, width = int(width))
		if caption:
			pil_im = im_write_text(pil_im, text = caption, x = x0, y = y0,
						tup_font_rgb= ImageColor.getrgb(color))
		im_array = np.array(pil_im)
	return im_array

im_isgray(im, b_check_rgb=False)

check if an image is grayscale

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
b_check_rgb bool

check all the pixels if the image has 3 channels

False

Returns:

Type Description
bool

True or False

Examples:

>>> im = get_im("https://picsum.photos/200/300?grayscale")
>>> im_isgray(im, b_check_rgb = False)
True
>>> im_isgray("https://picsum.photos/200/300", b_check_rgb = True)
False
Source code in toolbox/im_utils.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def im_isgray(im: Any, b_check_rgb: bool = False) -> bool:
	''' [check if an image is grayscale](https://stackoverflow.com/a/58791118/14285096)

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		b_check_rgb: check all the pixels if the image has 3 channels

	Returns:
		True or False

	Examples:
		>>> im = get_im("https://picsum.photos/200/300?grayscale")
		>>> im_isgray(im, b_check_rgb = False)
		True
		>>> im_isgray("https://picsum.photos/200/300", b_check_rgb = True)
		False
	'''
	im_rgb_array = get_im(im)
	if len(im_rgb_array.shape) < 3: return True
	if im_rgb_array.shape[2]  == 1: return True
	if b_check_rgb:
		r, g, b = im_rgb_array[:,:,0], im_rgb_array[:,:,1], im_rgb_array[:,:,2]
		if (b==g).all() and (b==r).all(): return True
	return False

im_mask_bbox(im, l_bboxes, mask_rgb_tup=(0, 0, 0), b_mask_bg=True)

Mask the input image with the provided bounding boxes

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
l_bboxes List[dict]

List of bounding boxes

required
mask_rgb_tup Tuple[int, int, int]

tuple of mask rgb value

(0, 0, 0)
b_mask_bg bool

if True area outside the bounding boxes will be set to mask_rgb; otherwise, the area inside the bounding boxes will be set to mask_rgb

True

Returns:

Type Description
np.ndarray

a numpy array

Examples:

>>> im_org = get_im("https://picsum.photos/200/300")
>>> bbox = {'x0': 10, 'y0': 10, 'x1': 190, 'y1': 290}
>>> im_masked = im_mask_bbox(im_org, l_bboxes= [bbox])
>>> simple_im_diff(im_org, im_masked)
True
>>> im_org.shape == im_masked.shape
True
Source code in toolbox/im_utils.py
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
def im_mask_bbox(im: Any, l_bboxes: List[dict],
		mask_rgb_tup: Tuple[int, int, int] = (0,0,0),
		b_mask_bg: bool = True
	) -> np.ndarray:
	''' Mask the input image with the provided bounding boxes

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		l_bboxes: List of bounding boxes
		mask_rgb_tup: tuple of mask rgb value
		b_mask_bg: if True area outside the bounding boxes will be set to mask_rgb; otherwise, the area inside the bounding boxes will be set to mask_rgb

	Returns:
		a numpy array

	Examples:
		>>> im_org = get_im("https://picsum.photos/200/300")
		>>> bbox = {'x0': 10, 'y0': 10, 'x1': 190, 'y1': 290}
		>>> im_masked = im_mask_bbox(im_org, l_bboxes= [bbox])
		>>> simple_im_diff(im_org, im_masked)
		True
		>>> im_org.shape == im_masked.shape
		True
	'''
	im_rgb_array = get_im(im)
	h,w, c = im_rgb_array.shape
	bg_im = np.zeros([h,w,3], dtype = np.uint8) # black
	bg_im[:,:] = mask_rgb_tup # apply color
	im = np.copy(im_rgb_array)

	for bbox in l_bboxes:
		bg_im[bbox['y0']: bbox['y1'], bbox['x0']: bbox['x1']] = im_crop(im_rgb_array, **bbox)
		im[bbox['y0']: bbox['y1'], bbox['x0']: bbox['x1']] = mask_rgb_tup
	return bg_im if b_mask_bg else im

im_max_long_edge(im, long_edge, resample_algo=Image.LANCZOS)

Return an image whose long edge is no longer than the given size

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
resample_algo int

default to LANCZOS because it gives the best downscaling quality

Image.LANCZOS

Returns:

Type Description
np.ndarray

a numpy array

Examples:

>>> im = get_im("https://picsum.photos/200/300")
>>> im_max_long_edge(im, long_edge = 500).shape
(300, 200, 3)
Source code in toolbox/im_utils.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def im_max_long_edge(im: Any, long_edge: int,
		resample_algo: int = Image.LANCZOS) -> np.ndarray:
	''' Return an image whose long edge is no longer than the given size

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		resample_algo: default to LANCZOS because it gives [the best downscaling quality](https://pillow.readthedocs.io/en/stable/handbook/concepts.html#filters-comparison-table)

	Returns:
		a numpy array

	Examples:
		>>> im = get_im("https://picsum.photos/200/300")
		>>> im_max_long_edge(im, long_edge = 500).shape
		(300, 200, 3)
	'''
	w, h = propose_wh(im, max_long_edge = long_edge)
	im = get_pil_im(im).resize(size =(w,h), resample = resample_algo)
	return np.array(im)

im_min_short_edge(im, short_edge, resample_algo=Image.LANCZOS)

Return an image whose short edge is no shorter than the given size

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
resample_algo int

default to LANCZOS because it gives the best downscaling quality

Image.LANCZOS

Returns:

Type Description
np.ndarray

a numpy array

Examples:

>>> im = get_im("https://picsum.photos/200/300")
>>> im_min_short_edge(im, short_edge = 300).shape
(450, 300, 3)
Source code in toolbox/im_utils.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def im_min_short_edge(im: Any, short_edge: int,
		resample_algo: int = Image.LANCZOS) -> np.ndarray:
	''' Return an image whose short edge is no shorter than the given size

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		resample_algo: default to LANCZOS because it gives [the best downscaling quality](https://pillow.readthedocs.io/en/stable/handbook/concepts.html#filters-comparison-table)

	Returns:
		a numpy array

	Examples:
		>>> im = get_im("https://picsum.photos/200/300")
		>>> im_min_short_edge(im, short_edge = 300).shape
		(450, 300, 3)
	'''
	w, h = propose_wh(im, min_short_edge = short_edge)
	im = get_pil_im(im).resize(size =(w,h), resample = resample_algo)
	return np.array(im)

im_to_bytes(im, format=None, quality='keep')

Returns an Image representation in bytes format

This function creates a mutable bytearray object and uses the PIL.Image.save() function to output the image to it. Note that the PIL.Image.tobytes() is not recommended. For more on working with binary data, see here.

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
format str

image format as supported by PIL

None
quality Union[int, str]

compression quality passed to Image.save(), defaults to "keep" (retain image quality, only for JPEG format); more details available here

'keep'

Returns:

Type Description
bytes

a bytes representation of the provide image

Examples:

>>> im_bytes = im_to_bytes("https://picsum.photos/200/300")
>>> type(im_bytes)== bytes
True
Source code in toolbox/im_utils.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def im_to_bytes(im: Any, format: str = None, quality: Union[int, str] = "keep") -> bytes:
	''' Returns an Image representation in bytes format

	This function creates a mutable bytearray object and uses the `PIL.Image.save()` function
	to output the image to it. Note that the `PIL.Image.tobytes()` is [not recommended](https://stackoverflow.com/a/58949303/14285096).
	For more on working with binary data, see [here](https://www.devdungeon.com/content/working-binary-data-python#video_bytes_bytearray).

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		format: image format as [supported by PIL](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html)
		quality: compression quality passed to `Image.save()`, defaults to "keep" (retain image quality, only for JPEG format); more details available [here](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#jpeg-saving)

	Returns:
		a bytes representation of the provide image

	Examples:
		>>> im_bytes = im_to_bytes("https://picsum.photos/200/300")
		>>> type(im_bytes)== bytes
		True
	'''
	imgByteArr = io.BytesIO()
	pil_im = get_pil_im(im)

	# from the PIL doc:
	# If a file object was used instead of a filename, this parameter should always be used.
	img_format = format if format else pil_im.format
	img_format = img_format if img_format else 'jpeg'
	pil_im.save(imgByteArr, format = img_format, quality = quality)
	imgByteArr = imgByteArr.getvalue()
	return imgByteArr

im_write_text(im, text, x, y, tup_font_rgb=(255, 255, 255), tup_bg_rgb=None)

draw text over image and returns it as a numpy array

font_size is not currently supported because PIL's default font cannot change in size; for simplicity, the work-arounds (such as this) is not implemented.

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
text str

text to be written

required
x int

text's top-left x location

required
y int

text's top-left y location

required
tup_font_rgb Tuple[int, int, int]

RGB values for font color, defaults to white

(255, 255, 255)
tup_bg_rgb Union[Tuple[int, int, int], None]

RGB values for text's background color

None

Returns:

Type Description
np.ndarray

a numpy array

Examples:

>>> im = get_im("https://picsum.photos/200/300")
>>> im_with_text = im_write_text(im, text = "hello world", x = 10, y = 10)
>>> simple_im_diff(im, im_with_text)
True
Source code in toolbox/im_utils.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
def im_write_text(im: Any, text: str, x:int, y:int,
		tup_font_rgb: Tuple[int,int,int] = (255,255,255),
		tup_bg_rgb: Union[Tuple[int,int,int], None] = None
	) -> np.ndarray:
	''' draw text over image and returns it as a numpy array

	font_size is not currently supported because PIL's default font
	[cannot change in size](https://github.com/python-pillow/Pillow/issues/6622);
	for simplicity, the work-arounds (such as [this](https://stackoverflow.com/a/48381516/14285096))
	is not implemented.

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		text: text to be written
		x: text's top-left x location
		y: text's top-left y location
		tup_font_rgb: RGB values for font color, defaults to white
		tup_bg_rgb: RGB values for text's background color

	Returns:
		a numpy array

	Examples:
		>>> im = get_im("https://picsum.photos/200/300")
		>>> im_with_text = im_write_text(im, text = "hello world", x = 10, y = 10)
		>>> simple_im_diff(im, im_with_text)
		True
	'''
	pil_im = get_pil_im(im).copy()

	# font = ImageFont.truetype(font_path, size = font_size, encoding = 'unic')
	font = ImageFont.load_default()
	draw = ImageDraw.Draw(pil_im)
	w, h = draw.textsize(text, font)
	if isinstance(tup_bg_rgb, tuple):
		draw.rectangle(xy =(x,y,x+w,y+h), fill = tup_bg_rgb, outline = tup_bg_rgb)
	draw.text((x,y), text, tup_font_rgb, font = font)
	return np.array(pil_im)

image_stack(im1, im2, do_vstack=False, split_pct=None, black_line_thickness=5, resample=Image.LANCZOS, debug=False)

Concat two images into one with a border separating the two

Parameters:

Name Type Description Default
im1 Any

the left or top image

required
im2 Any

the right or bottom image (will be resize to match im1's dimension); Aspect ratio might change!

required
do_vstack bool

do vertical stack, else, horizontal stack

False
split_pct float

Percentage of left image to show, right image will fill up the remainder. If none, both left and right images will be shown in full

None
black_line_thickness int

how many pixels wide is the black line separating the two images

5
resample int

one of PIL Image resampling methods

Image.LANCZOS
debug bool

print out which combine algo will be used on the two resized images

False

Returns:

Type Description
np.ndarray

a numpy array

Raises:

Type Description
AssertionError

An error will be raise split_pct is greater than or equal to one

Examples:

>>> im1 = get_im("https://picsum.photos/200/300")
>>> im2 = get_pil_im("https://picsum.photos/200/500")
>>> image_stack(im1, im2, do_vstack = True, black_line_thickness = 5).shape
(805, 200, 3)
Source code in toolbox/im_utils.py
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
def image_stack(im1: Any, im2: Any, do_vstack: bool = False, split_pct: float = None,
				black_line_thickness: int = 5,
				resample: int = Image.LANCZOS, debug: bool = False
	) -> np.ndarray:
	''' Concat two images into one with a border separating the two

	Args:
		im1: the left or top image
		im2: the right or bottom image (will be resize to match im1's dimension); **Aspect ratio** might change!
		do_vstack: do vertical stack, else, horizontal stack
		split_pct: Percentage of left image to show, right image will fill up the remainder. If none, both left and right images will be shown in full
		black_line_thickness: how many pixels wide is the black line separating the two images
		resample: one of [PIL Image resampling methods](https://note.nkmk.me/en/python-pillow-image-resize/)
		debug: print out which combine algo will be used on the two resized images

	Returns:
		a numpy array

	Raises:
		AssertionError: An error will be raise split_pct is greater than or equal to one

	Examples:
		>>> im1 = get_im("https://picsum.photos/200/300")
		>>> im2 = get_pil_im("https://picsum.photos/200/500")
		>>> image_stack(im1, im2, do_vstack = True, black_line_thickness = 5).shape
		(805, 200, 3)
	'''
	# input validation
	if split_pct:
		assert split_pct < 1, f"split_pct must be float and less than or equal to 1."

	img_arr_1, img_arr_2 = get_im(im1), get_im(im2)
	h, w, c = img_arr_1.shape
	_h, _w, _c = img_arr_2.shape
	if do_vstack:
		_h *= w/ _w
		_w = w
		np_black_line = np.zeros(shape = [black_line_thickness, w, 3], dtype = np.uint8)
	else:
		_w *= h/ _h
		_h = h
		np_black_line = np.zeros(shape = [h, black_line_thickness, 3], dtype = np.uint8)

	# image resize operation
	a_end = split_pct if split_pct else 1
	b_start = split_pct if split_pct else 0

	b_img = Image.fromarray(img_arr_2).resize(size = (int(_w),int(_h)), resample = resample)
	if do_vstack:
		a_img = img_arr_1[:int(h * a_end ), :, :]
		b_img = np.array(b_img)[int(_h * b_start):,:,:]
	else:
		a_img = img_arr_1[:, :int(w * a_end ), :]
		b_img = np.array(b_img)[:,int(w * b_start):,:]

	# alpha channel
	if c != _c:
		warnings.warn(f'image_stack: img_arr_1 channel {c} and img_arr_2 {_c} does not match. adding alpha to both.')
		a_img = im_add_alpha(a_img)
		b_img = im_add_alpha(b_img)
		np_black_line = im_add_alpha(np_black_line)

	np_func = np.vstack if do_vstack else np.hstack
	img_comb = np_func([a_img, np_black_line, b_img])
	if debug:
		print(f'applied {np_func} on im1 (shape: {a_img.shape}) and im2 (shape: {b_img.shape})')
	return img_comb

pil_im_gray2rgb(pil_im)

convert a grayscale Pillow Image to RGB format

Parameters:

Name Type Description Default
pil_im Image.Image

a PIL Image Object

required

Returns:

Type Description
Image.Image

A PIL Image Object in RGB format

Examples:

>>> pil_im = get_pil_im("https://picsum.photos/200/300?grayscale")
>>> pil_im = pil_im_gray2rgb(pil_im)
>>> im_isgray(np.array(pil_im), b_check_rgb = False)
False
Source code in toolbox/im_utils.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def pil_im_gray2rgb(pil_im: Image.Image) -> Image.Image:
	''' convert a grayscale Pillow Image to RGB format

	Args:
		pil_im: a PIL Image Object

	Returns:
		A PIL Image Object in RGB format

	Examples:
		>>> pil_im = get_pil_im("https://picsum.photos/200/300?grayscale")
		>>> pil_im = pil_im_gray2rgb(pil_im)
		>>> im_isgray(np.array(pil_im), b_check_rgb = False)
		False
	'''
	return pil_im.copy().convert('RGB'
			) if im_isgray( np.array(pil_im), b_check_rgb = False) else pil_im

propose_wh(im, max_long_edge=None, min_short_edge=None)

returns a Tuple of Width and Height keeping the aspect ratio

Parameters:

Name Type Description Default
im Any

an image representation in numpy array, PIL Image object, URL, or filepath

required
max_long_edge int

max long edge of the resized image

None
min_short_edge int

min short edge of the resize image

None

Returns:

Type Description
Tuple[int, int]

A Tuple of resized Width Hieght (keeping aspect ratio)

Raise

AssertionError: Either max_long_edge or min_short_edge must be provided but not both

Examples:

>>> propose_wh("https://random.imagecdn.app/500/150", max_long_edge = 250)
(250, 75)
>>> propose_wh("https://random.imagecdn.app/500/150", min_short_edge = 100)
(500, 150)
Source code in toolbox/im_utils.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def propose_wh(im: Any , max_long_edge: int = None, min_short_edge: int = None) -> Tuple[int,int]:
	''' returns a Tuple of Width and Height keeping the aspect ratio

	Args:
		im: an image representation in numpy array, PIL Image object, URL, or filepath
		max_long_edge: max long edge of the resized image
		min_short_edge: min short edge of the resize image

	Returns:
		A Tuple of resized Width Hieght (keeping aspect ratio)

	Raise:
		AssertionError: Either max_long_edge or min_short_edge must be provided but not both

	Examples:
		>>> propose_wh("https://random.imagecdn.app/500/150", max_long_edge = 250)
		(250, 75)
		>>> propose_wh("https://random.imagecdn.app/500/150", min_short_edge = 100)
		(500, 150)
	'''
	assert any([max_long_edge, min_short_edge]) and not(all([max_long_edge, min_short_edge])), \
		f"Either max_long_edge or min_short_edge must be provided but not both"

	w_ , h_ = get_pil_im(im).size
	wh_ratio = w_/h_
	if w_ > h_:
		if max_long_edge:
			w = max_long_edge if w_ > max_long_edge else w_
			h = int(w/wh_ratio)
		else:
			h = min_short_edge if h_ < min_short_edge else h_
			w = int(h * wh_ratio)
	else:
		if max_long_edge:
			h = max_long_edge if h_ > max_long_edge else h_
			w = int(h * wh_ratio)
		else:
			w = min_short_edge if w_ < min_short_edge else w_
			h = int(w/wh_ratio)
	return int(w), int(h)

simple_im_diff(im_1, im_2, n_pixels_allowed=0)

compare two images and check for pixel differences using PIL

Parameters:

Name Type Description Default
im_1 Any

first image representation in numpy array, PIL Image object, URL, or filepath

required
im_2 Any

second image representation in numpy array, PIL Image object, URL, or filepath

required
n_pixels_allowed int

threshold to number of pixel difference allowed between im_1 and im_2

0

Returns:

Type Description
bool

True if two images are different (above n_pixels_allowed) and False otherwise

Examples:

>>> simple_im_diff("https://picsum.photos/200/300","https://picsum.photos/200/300")
True
Source code in toolbox/im_utils.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def simple_im_diff(im_1: Any, im_2: Any, n_pixels_allowed: int = 0) -> bool:
	''' compare two images and check for pixel differences [using PIL](https://stackoverflow.com/a/73433784/14285096)

	Args:
		im_1: first image representation in numpy array, PIL Image object, URL, or filepath
		im_2: second image representation in numpy array, PIL Image object, URL, or filepath
		n_pixels_allowed: threshold to number of pixel difference allowed between im_1 and im_2

	Returns:
		True if two images are different (above `n_pixels_allowed`) and False otherwise

	Examples:
		>>> simple_im_diff("https://picsum.photos/200/300","https://picsum.photos/200/300")
		True
	'''
	if get_im(im_1).shape != get_im(im_2).shape: return True

	from PIL import ImageChops
	im_1, im_2 = get_pil_im(im_1), get_pil_im(im_2)
	diff = ImageChops.difference(im_1,im_2)
	channels = diff.split()
	for c in channels:
		if len(set(c.getdata()))> n_pixels_allowed: return True
	return False