mirror of
https://codeberg.org/privacy1st/blur-exif-face-tags
synced 2024-12-22 01:16:04 +01:00
multiple improvements
- resize image - copy (more) image metadata - orientate image - add text to image
This commit is contained in:
parent
d577396768
commit
01abab3424
@ -11,7 +11,7 @@
|
||||
|
||||
2) Copy all pictures to be modified in a folder
|
||||
|
||||
> See [example/](example/)
|
||||
> See [example](example)
|
||||
|
||||
3) Run this python script on that folder with a list of names of persons to be blurred
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
>
|
||||
> This results in the following picture:
|
||||
>
|
||||
> ![](example/unsplash_blurred.jpg)
|
||||
> ![](example/unsplash[blurred].jpg)
|
||||
|
||||
|
||||
## Credits
|
||||
|
32
area.py
Normal file
32
area.py
Normal file
@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import exif
|
||||
|
||||
|
||||
class NormalizedRectangle:
|
||||
"""
|
||||
x, y, width and height are normalized: Their values are in the range [0, 1].
|
||||
"""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
width: float
|
||||
height: float
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
@staticmethod
|
||||
def of_exif_image_region(region: exif.ExifImageRegion) -> NormalizedRectangle:
|
||||
if region.area_unit == 'normalized':
|
||||
return NormalizedRectangle(
|
||||
x=region.rectangle[0],
|
||||
y=region.rectangle[1],
|
||||
width=region.rectangle[2],
|
||||
height=region.rectangle[3]
|
||||
)
|
||||
else:
|
||||
raise Exception()
|
103
blur.py
103
blur.py
@ -1,103 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from PIL import ImageDraw
|
||||
from PIL import ImageFilter
|
||||
from PIL import Image
|
||||
|
||||
import exif
|
||||
|
||||
|
||||
class NormalizedRectangle:
|
||||
"""
|
||||
x, y, width and height are normalized: Their values are in the range [0, 1].
|
||||
"""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
width: float
|
||||
height: float
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
@staticmethod
|
||||
def of_exif_image_region(region: exif.ExifImageRegion) -> NormalizedRectangle:
|
||||
if region.area_unit == 'normalized':
|
||||
return NormalizedRectangle(x=region.rectangle[0], y=region.rectangle[1],
|
||||
width=region.rectangle[2], height=region.rectangle[3])
|
||||
else:
|
||||
raise Exception
|
||||
|
||||
|
||||
def blur_rectangle0(image_src: exif.Image, region: exif.ExifImageRegion, image_dst: Path = None) -> Path:
|
||||
if region.area_unit == 'normalized':
|
||||
return blur_rectangle1(image_src,
|
||||
normalized_rectangles=[NormalizedRectangle.of_exif_image_region(region=region)],
|
||||
image_dst=image_dst)
|
||||
else:
|
||||
raise Exception(f'Unknown area_unit: {region.area_unit}')
|
||||
|
||||
|
||||
def blur_rectangle1(image_src: exif.Image, normalized_rectangles: List[NormalizedRectangle], image_dst: Path = None) -> Path:
|
||||
return blur_rectangle2(image_src.get_image_file(),
|
||||
normalized_rectangles,
|
||||
image_dst=image_dst)
|
||||
|
||||
|
||||
def blur_rectangle2(image_src: Path, normalized_rectangles: List[NormalizedRectangle], image_dst: Path = None) -> Path:
|
||||
if len(normalized_rectangles) == 0:
|
||||
raise Exception('No rectangles to blur')
|
||||
|
||||
# Open an image
|
||||
im = Image.open(image_src)
|
||||
|
||||
# Create mask
|
||||
mask = Image.new('L', im.size, 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
|
||||
# For each rectangle: Draw a white rectangle to the mask
|
||||
for normalized_rectangle in normalized_rectangles:
|
||||
# Calculate top left and lower right corners of rectangle
|
||||
im_width, im_height = im.size
|
||||
x1 = im_width * normalized_rectangle.x
|
||||
y1 = im_height * normalized_rectangle.y
|
||||
x2 = x1 + im_width * normalized_rectangle.width
|
||||
y2 = y1 + im_height * normalized_rectangle.height
|
||||
|
||||
draw.rectangle([(x1, y1), (x2, y2)], fill=255)
|
||||
|
||||
# Save the mask
|
||||
mask.save('mask.png')
|
||||
|
||||
# Blur image
|
||||
blurred = im.filter(ImageFilter.GaussianBlur(52))
|
||||
|
||||
# Paste blurred region and save result
|
||||
im.paste(blurred, mask=mask)
|
||||
|
||||
# Save image
|
||||
if image_dst is None:
|
||||
image_dst = get_image_dst(image_src)
|
||||
im.save(image_dst)
|
||||
|
||||
return image_dst
|
||||
|
||||
|
||||
def get_image_dst(image: Path):
|
||||
return image.parent.joinpath(f'{image.stem}{stem_suffix()}{image.suffix}')
|
||||
|
||||
|
||||
def stem_suffix():
|
||||
"""
|
||||
Modified images will be saved with a different filename.
|
||||
This suffix will be added to their stem.
|
||||
"""
|
||||
|
||||
# return ' [blurred]'
|
||||
return '_blurred'
|
BIN
example/unsplash[blurred].jpg
Normal file
BIN
example/unsplash[blurred].jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 237 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.0 MiB |
75
exif.py
75
exif.py
@ -1,57 +1,6 @@
|
||||
from pathlib import Path
|
||||
from typing import List, AnyStr
|
||||
import exec
|
||||
|
||||
|
||||
class Image:
|
||||
"""
|
||||
An image may be just a single file (e.g. "IMG_001.RAF") or
|
||||
multiple files ("IMG_002.JPG" (image-data) and
|
||||
"IMG_002.JPG.xmp" (additional metadata)).
|
||||
"""
|
||||
|
||||
files: List[Path]
|
||||
|
||||
def __init__(self, image_file: Path):
|
||||
if image_file.is_file():
|
||||
self.files = [image_file]
|
||||
image_file_name = image_file.name # file.name ==> file basename
|
||||
for sibling in image_file.parent.iterdir():
|
||||
sibling_stem = sibling.stem # file.stem ==> file basename without extension
|
||||
if sibling_stem == image_file_name:
|
||||
self.files += [sibling]
|
||||
if len(self.files) == 0:
|
||||
raise Exception
|
||||
else:
|
||||
raise Exception(f'{image_file}')
|
||||
|
||||
def get_image_file(self):
|
||||
return self.files[0]
|
||||
|
||||
def get_xmp_file(self):
|
||||
"""
|
||||
:return: The sidecar xmp file, if it exists. Otherwise None is returned.
|
||||
"""
|
||||
|
||||
for file in self.files:
|
||||
file_extension = file.suffix[1:]
|
||||
if file_extension.lower() == 'xmp':
|
||||
return file
|
||||
return None
|
||||
|
||||
def get_metadata_file(self):
|
||||
"""
|
||||
If a sidecar xmp file exists, it is preferred over the image file itself.
|
||||
|
||||
:return: A file containing the image metadata.
|
||||
"""
|
||||
metadata = self.get_xmp_file()
|
||||
if metadata is None:
|
||||
metadata = self.get_image_file()
|
||||
return metadata
|
||||
|
||||
def __str__(self):
|
||||
return f'Image: {self.__dict__}'
|
||||
from image_files import ImageFiles
|
||||
from subprocess_util import execute_save
|
||||
|
||||
|
||||
class ExifImageRegion:
|
||||
@ -60,15 +9,15 @@ class ExifImageRegion:
|
||||
For example a face tag.
|
||||
"""
|
||||
|
||||
image: Image
|
||||
image_files: ImageFiles
|
||||
|
||||
name: str # 'John'
|
||||
r_type: str # 'Face'
|
||||
area_unit: str # 'normalized'
|
||||
rectangle: List[float] # [ x-coordinate, y-coordinate, width, height ]
|
||||
rectangle: list[float] # [ x-coordinate, y-coordinate, width, height ]
|
||||
|
||||
def __init__(self, image: Image, name, r_type, area_unit, rectangle):
|
||||
self.image = image
|
||||
def __init__(self, image_files: ImageFiles, name, r_type, area_unit, rectangle):
|
||||
self.image_files = image_files
|
||||
self.name = name
|
||||
self.r_type = r_type
|
||||
self.area_unit = area_unit
|
||||
@ -78,13 +27,13 @@ class ExifImageRegion:
|
||||
return f'ExifImageRegion: {self.__dict__}'
|
||||
|
||||
|
||||
def get_exif_image_regions(image: Image) -> List[ExifImageRegion]:
|
||||
def get_exif_image_regions(image: ImageFiles) -> list[ExifImageRegion]:
|
||||
img_metadata_file: Path = image.get_metadata_file()
|
||||
|
||||
names_str = exec.execute_save(['exiftool', '-RegionName', str(img_metadata_file)])
|
||||
r_types_str = exec.execute_save(['exiftool', '-RegionType', str(img_metadata_file)])
|
||||
area_units_str = exec.execute_save(['exiftool', '-RegionAreaUnit', str(img_metadata_file)])
|
||||
rectangles_str = exec.execute_save(['exiftool', '-RegionRectangle', str(img_metadata_file)])
|
||||
names_str = execute_save(['exiftool', '-RegionName', str(img_metadata_file)])
|
||||
r_types_str = execute_save(['exiftool', '-RegionType', str(img_metadata_file)])
|
||||
area_units_str = execute_save(['exiftool', '-RegionAreaUnit', str(img_metadata_file)])
|
||||
rectangles_str = execute_save(['exiftool', '-RegionRectangle', str(img_metadata_file)])
|
||||
|
||||
if len(names_str) == len(r_types_str) == len(area_units_str) == len(rectangles_str) == 0:
|
||||
# there are no tagged areas on this image
|
||||
@ -103,5 +52,5 @@ def get_exif_image_regions(image: Image) -> List[ExifImageRegion]:
|
||||
rectangles[i] += [None]
|
||||
rectangles[i][j] = float(rectangles_tmp[i * 4 + j])
|
||||
|
||||
return [ExifImageRegion(image=image, name=name, r_type=r_type, area_unit=area_unit, rectangle=rectangle) for
|
||||
return [ExifImageRegion(image_files=image, name=name, r_type=r_type, area_unit=area_unit, rectangle=rectangle) for
|
||||
name, r_type, area_unit, rectangle in zip(names, r_types, area_units, rectangles)]
|
||||
|
51
image_files.py
Normal file
51
image_files.py
Normal file
@ -0,0 +1,51 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ImageFiles:
|
||||
"""
|
||||
An image may be just a single file (e.g. "IMG_001.RAF") or
|
||||
multiple files ("IMG_002.JPG" (image-data) and
|
||||
"IMG_002.JPG.xmp" (additional metadata)).
|
||||
"""
|
||||
|
||||
files: list[Path]
|
||||
|
||||
def __init__(self, image_file: Path):
|
||||
if image_file.is_file():
|
||||
self.files = [image_file]
|
||||
image_file_name = image_file.name # file.name ==> file basename
|
||||
for sibling in image_file.parent.iterdir():
|
||||
sibling_stem = sibling.stem # file.stem ==> file basename without extension
|
||||
if sibling_stem == image_file_name:
|
||||
self.files += [sibling]
|
||||
if len(self.files) == 0:
|
||||
raise Exception
|
||||
else:
|
||||
raise Exception(f'Not a file: {image_file}')
|
||||
|
||||
def get_image_file(self):
|
||||
return self.files[0]
|
||||
|
||||
def get_xmp_file(self):
|
||||
"""
|
||||
:return: The sidecar xmp file, if it exists. Otherwise, `None` is returned.
|
||||
"""
|
||||
for file in self.files:
|
||||
file_extension = file.suffix[1:]
|
||||
if file_extension.lower() == 'xmp':
|
||||
return file
|
||||
return None
|
||||
|
||||
def get_metadata_file(self):
|
||||
"""
|
||||
If a sidecar xmp file exists, it is preferred over the image file itself.
|
||||
|
||||
:return: A file containing the image metadata.
|
||||
"""
|
||||
metadata = self.get_xmp_file()
|
||||
if metadata is None:
|
||||
metadata = self.get_image_file()
|
||||
return metadata
|
||||
|
||||
def __str__(self):
|
||||
return f'Image: {self.__dict__}'
|
175
image_manipulation.py
Normal file
175
image_manipulation.py
Normal file
@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from PIL import ImageDraw
|
||||
from PIL import ImageFilter
|
||||
from PIL import Image
|
||||
from PIL import ImageFont
|
||||
from PIL import ImageOps
|
||||
|
||||
import exif
|
||||
from area import NormalizedRectangle
|
||||
from subprocess_util import execute_save
|
||||
|
||||
|
||||
def open_image(src: Path | exif.ImageFiles, exif_rotation: bool = True) -> Image.Image:
|
||||
"""
|
||||
Open image and rotate according to EXIF metadata.
|
||||
|
||||
:param src:
|
||||
:param exif_rotation:
|
||||
:return:
|
||||
"""
|
||||
if isinstance(src, exif.ImageFiles):
|
||||
src = src.get_image_file()
|
||||
|
||||
# Open an image.
|
||||
image = Image.open(src)
|
||||
|
||||
if exif_rotation:
|
||||
# Rotate according to EXIF orientation.
|
||||
image = ImageOps.exif_transpose(image)
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def blur(image: Image.Image,
|
||||
areas: List[exif.ExifImageRegion | NormalizedRectangle]) -> Image.Image:
|
||||
"""
|
||||
:param image:
|
||||
:param areas: Areas to blur on the given image.
|
||||
:return: Reference to the modified image.
|
||||
"""
|
||||
if areas is None or len(areas) == 0:
|
||||
return image
|
||||
|
||||
areas_ = []
|
||||
for area in areas:
|
||||
if isinstance(area, NormalizedRectangle):
|
||||
areas_.append(area)
|
||||
if isinstance(area, exif.ExifImageRegion):
|
||||
areas_.append(NormalizedRectangle.of_exif_image_region(area))
|
||||
else:
|
||||
raise Exception()
|
||||
areas = areas_
|
||||
|
||||
# Create mask.
|
||||
mask = Image.new('L', image.size, 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
|
||||
# For each rectangle: Draw a white rectangle to the mask.
|
||||
for area in areas:
|
||||
# Calculate top left and lower right corners of rectangle.
|
||||
im_width, im_height = image.size
|
||||
x1 = im_width * area.x
|
||||
y1 = im_height * area.y
|
||||
x2 = x1 + im_width * area.width
|
||||
y2 = y1 + im_height * area.height
|
||||
|
||||
draw.rectangle(((x1, y1), (x2, y2)), fill=255)
|
||||
|
||||
# Save the mask.
|
||||
mask_path = Path('mask.png')
|
||||
mask.save(mask_path)
|
||||
|
||||
# Blur radius.
|
||||
# Between 50 and 250. Depending on how many pixels are inside the face-rectangle.
|
||||
# 50 for small faces on low-res images. 250 for a close-up on a high-resolution image.
|
||||
radius = 200
|
||||
# Blur image.
|
||||
blurred = image.filter(ImageFilter.GaussianBlur(radius))
|
||||
|
||||
# Paste blurred region and save result.
|
||||
image.paste(blurred, mask=mask)
|
||||
|
||||
mask_path.unlink(missing_ok=False)
|
||||
return image
|
||||
|
||||
|
||||
def resize(image: Image.Image, max_resolution: int = 2048) -> Image.Image:
|
||||
if max_resolution is None or max_resolution < 0:
|
||||
return image
|
||||
|
||||
actual_resolution: int = max(image.size[0], image.size[1])
|
||||
factor: float = max_resolution / actual_resolution
|
||||
return image.resize((round(factor * image.size[0]), round(factor * image.size[1])))
|
||||
|
||||
|
||||
def add_text(image: Image.Image, text: str = None) -> Image.Image:
|
||||
if text is None or len(text) == 0:
|
||||
return image
|
||||
|
||||
x = 80
|
||||
y = image.size[1] - 80
|
||||
|
||||
font = ImageFont.truetype("/usr/share/fonts/noto/NotoSans-Regular.ttf", 64)
|
||||
d = ImageDraw.Draw(image)
|
||||
|
||||
# anchor - Quick reference: https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#quick-reference
|
||||
anchor = 'ls'
|
||||
|
||||
# Draw border around text
|
||||
d.text((x - 1, y - 1), text, anchor=anchor, font=font, fill="black")
|
||||
d.text((x + 1, y - 1), text, anchor=anchor, font=font, fill="black")
|
||||
d.text((x - 1, y + 1), text, anchor=anchor, font=font, fill="black")
|
||||
d.text((x + 1, y + 1), text, anchor=anchor, font=font, fill="black")
|
||||
# Draw text itself
|
||||
d.text((x, y), text, anchor=anchor, font=font, fill="white")
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def save_image(image: Image.Image, dst: Path,
|
||||
src: Path | exif.ImageFiles = None,
|
||||
orientation: bool = True,
|
||||
gps: bool = True,
|
||||
date: bool = True
|
||||
) -> Path:
|
||||
"""
|
||||
Save image and copy some metadata from original image.
|
||||
|
||||
:param image:
|
||||
:param dst:
|
||||
:param src:
|
||||
:param orientation:
|
||||
:param gps:
|
||||
:param date:
|
||||
:return:
|
||||
"""
|
||||
# Save image.
|
||||
#
|
||||
# JPEG quality: https://jdhao.github.io/2019/07/20/pil_jpeg_image_quality/#other-options-for-quality
|
||||
# JPEG presets: https://github.com/python-pillow/Pillow/blob/main/src/PIL/JpegPresets.py#L67
|
||||
# e.g. web_low, web_medium, web_high, web_very_high, web_maximum
|
||||
image.save(dst, format='jpeg', quality='web_low')
|
||||
|
||||
if orientation or gps or date:
|
||||
copy_metadata(src, dst, orientation, gps, date)
|
||||
|
||||
return dst
|
||||
|
||||
|
||||
def copy_metadata(src: Path | exif.ImageFiles,
|
||||
dst: Path,
|
||||
orientation: bool = True,
|
||||
gps: bool = True,
|
||||
date: bool = True) -> None:
|
||||
if not (orientation or gps or date):
|
||||
return
|
||||
|
||||
if isinstance(src, exif.ImageFiles):
|
||||
src = src.get_metadata_file()
|
||||
|
||||
args = ['exiftool', '-overwrite_original', '-tagsfromfile', str(src)]
|
||||
if orientation:
|
||||
args += ['-orientation']
|
||||
if gps:
|
||||
args += ['-gps:all']
|
||||
if date:
|
||||
args += ['-alldates']
|
||||
args += [str(dst)]
|
||||
|
||||
print(f' Copying metadata ...')
|
||||
execute_save(args)
|
105
main.py
105
main.py
@ -1,55 +1,32 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Union
|
||||
|
||||
import blur
|
||||
import exec
|
||||
import exif
|
||||
import image_manipulation
|
||||
from exif import ExifImageRegion, get_exif_image_regions
|
||||
|
||||
# ======================================================================================================= #
|
||||
|
||||
# Adjust path to folder with images
|
||||
image_directory = Path('example')
|
||||
image_directory = Path('example') # Image directory.
|
||||
|
||||
# Enter name of persons to be blurred. Leave empty to blur any face.
|
||||
names = ['A', 'B', 'C']
|
||||
# names = []
|
||||
names = ['A', 'B', 'C'] # Names of Persons to be blurred.
|
||||
# names = [] # Blurr all faces.
|
||||
|
||||
delete_original: bool = True # deletes the original image after blurring image was created
|
||||
copy_metadata_gps: bool = True # copies gps location from original to blurred image
|
||||
copy_metadata_date: bool = True # copies date from original to blurred image
|
||||
resolution = 2048 # Resize image.
|
||||
# resolution = None # Keep original image size.
|
||||
text = "Example" # Add text to image.
|
||||
# text = None # Do not add text.
|
||||
|
||||
# Lower-case image extensions.
|
||||
copy_metadata_orientation: bool = False # Copies orientation metadata from original image.
|
||||
copy_metadata_gps: bool = True # Copies gps location metadata from original image.
|
||||
copy_metadata_date: bool = True # Copies date metadata from original image.
|
||||
|
||||
# List of lower-case image extensions.
|
||||
image_extensions = ['.jpg', '.jpeg', '.png']
|
||||
|
||||
r_types = ['Face']
|
||||
|
||||
# ======================================================================================================= #
|
||||
|
||||
|
||||
def blur_image(image: exif.Image) -> Union[Path, None]:
|
||||
"""
|
||||
If at least one tagged area of the image matches the criteria,
|
||||
a blurred image is created and its path is returned.
|
||||
"""
|
||||
exif_image_regions: List[ExifImageRegion] = get_exif_image_regions(image=image)
|
||||
|
||||
# Blur only some faces.
|
||||
normalized_rectangles = []
|
||||
for region in exif_image_regions:
|
||||
if len(r_types) > 0 and region.r_type not in r_types:
|
||||
continue
|
||||
if region.name in names or len(names) == 0:
|
||||
normalized_rectangles += [blur.NormalizedRectangle.of_exif_image_region(region)]
|
||||
|
||||
if len(normalized_rectangles) > 0:
|
||||
print(f' Blurring {len(normalized_rectangles)} areas ...')
|
||||
return blur.blur_rectangle1(image_src=image, normalized_rectangles=normalized_rectangles)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
# Convert all images in `image_directory`, including subdirectories.
|
||||
for _, _, files in os.walk(image_directory):
|
||||
@ -57,25 +34,53 @@ def main():
|
||||
file: Path = Path.joinpath(image_directory, relative_file_str)
|
||||
if file.suffix.lower() not in image_extensions:
|
||||
continue
|
||||
if file.stem.endswith(blur.stem_suffix()):
|
||||
if stem_suffix() in file.stem:
|
||||
print(f'Skipped the following image as it is already blurred:\n\t{file}')
|
||||
continue
|
||||
if blur.get_image_dst(file).exists():
|
||||
print(f'Skipped the following image as it\'s blurred output does already exist:\n\t{file}')
|
||||
continue
|
||||
dst = get_image_dst(file)
|
||||
if dst.exists():
|
||||
# Blur again as the source image might have different face tags now.
|
||||
dst.unlink()
|
||||
|
||||
print(f'{file}')
|
||||
blurred_img = blur_image(exif.Image(file))
|
||||
blur_image(exif.ImageFiles(file), dst)
|
||||
|
||||
if blurred_img is not None and copy_metadata_gps:
|
||||
print(f' Copying gps metadata to blurred file ...')
|
||||
exec.execute_save(['exiftool', '-tagsfromfile', str(file), '-gps:all', str(blurred_img)])
|
||||
if blurred_img is not None and copy_metadata_date:
|
||||
print(f' Copying date metadata to blurred file ...')
|
||||
exec.execute_save(['exiftool', '-tagsfromfile', str(file), '-alldates', str(blurred_img)])
|
||||
if blurred_img is not None and delete_original:
|
||||
print(f' Deleting original file ...')
|
||||
file.unlink()
|
||||
|
||||
def blur_image(image_files: exif.ImageFiles, dst: Path) -> Path:
|
||||
"""
|
||||
If at least one tagged area of the image matches the criteria,
|
||||
a blurred image is created and its path is returned.
|
||||
"""
|
||||
exif_image_regions: list[ExifImageRegion] = get_exif_image_regions(image=image_files)
|
||||
|
||||
# Blur only some faces.
|
||||
areas = []
|
||||
r_types = ['Face']
|
||||
for region in exif_image_regions:
|
||||
if len(r_types) > 0 and region.r_type not in r_types:
|
||||
continue
|
||||
if region.name in names or len(names) == 0:
|
||||
areas.append(region)
|
||||
|
||||
print(f' Blurring {len(areas)} areas ...')
|
||||
|
||||
im = image_manipulation.open_image(image_files)
|
||||
im = image_manipulation.blur(im, areas)
|
||||
im = image_manipulation.resize(im, resolution)
|
||||
im = image_manipulation.add_text(im, text)
|
||||
return image_manipulation.save_image(im, dst, image_files)
|
||||
|
||||
|
||||
def get_image_dst(image: Path) -> Path:
|
||||
return image.parent.joinpath(f'{image.stem}{stem_suffix()}{image.suffix}')
|
||||
|
||||
|
||||
def stem_suffix() -> str:
|
||||
"""
|
||||
Modified images will be saved with a different filename.
|
||||
This suffix will be added to their stem.
|
||||
"""
|
||||
return '[blurred]'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
Loading…
Reference in New Issue
Block a user