文章目录
功能介绍
电脑启动时,自动执行该脚本,动态监听某个目录,对该目录下(包含子目录)未添加过边框的图片,添加边框(可设置边框宽度和颜色)
功能实现
图片处理框架选择
目前Python处理图片的两个比较流行的库:pillow和OpenCV,本文采用两种方式实现,顺便比较一下两个库处理图片的性能差异
1.使用pillow实现
import os
from PIL import Image
class ImageBorderHandler:
def __init__(self, source_folder: str, output_folder: str):
self.source_folder = source_folder
self.output_folder = output_folder
if not os.path.exists(output_folder):
os.mkdir(output_folder)
def _get_source_file_abs_path(self, file_name: str):
return os.path.join(self.source_folder, file_name)
def _get_output_file_abs_path(self, file_name: str):
return os.path.join(self.output_folder, file_name)
def _prepare_output_abs_path(self, file_name: str):
"""
检查输出路径,如果父级目录不存在,自动创建
:param file_name: 待输出文件,可能含有父级目录
"""
full_path = os.path.join(self.output_folder, file_name)
if not os.path.exists(full_path):
dir_item = self.output_folder
for path_item in file_name.split(os.path.sep)[: -1]:
dir_item += f'{os.path.sep}{path_item}'
if not os.path.exists(dir_item):
os.mkdir(dir_item)
def border_image(self, image_file_name: str, border_size=3, border_color=(212, 212, 212)):
"""
为图片添加边框
:param image_file_name: 待处理图片文件名
:param border_size: 边框宽度,默认值3px
:param border_color: 边框颜色,默认值为浅灰色
:return: None
"""
if not os.path.exists(self._get_source_file_abs_path(image_file_name)):
return
self._pillow_border_image(image_file_name, border_size=border_size, border_color=border_color)
def _pillow_border_image(self, image_file_name: str, border_size: int, border_color):
origin_image = Image.open(self._get_source_file_abs_path(image_file_name))
(w, h) = origin_image.size
# 宽度为原始图片尺寸 + 两个border的尺寸
border_image = Image.new('RGB', (w + 2 * border_size, h + 2 * border_size), border_color)
border_image.paste(origin_image, (border_size, border_size))
self._prepare_output_abs_path(image_file_name)
border_image.save(self._get_output_file_abs_path(image_file_name))
2.使用OpenCV实现
import os
import cv2
class ImageBorderHandler:
def __init__(self, source_folder: str, output_folder: str):
self.source_folder = source_folder
self.output_folder = output_folder
if not os.path.exists(output_folder):
os.mkdir(output_folder)
def _get_source_file_abs_path(self, file_name: str):
return os.path.join(self.source_folder, file_name)
def _get_output_file_abs_path(self, file_name: str):
return os.path.join(self.output_folder, file_name)
def _prepare_output_abs_path(self, file_name: str):
"""
检查输出路径,如果父级目录不存在,自动创建
:param file_name: 待输出文件,可能含有父级目录
"""
full_path = os.path.join(self.output_folder, file_name)
if not os.path.exists(full_path):
dir_item = self.output_folder
for path_item in file_name.split(os.path.sep)[: -1]:
dir_item += f'{os.path.sep}{path_item}'
if not os.path.exists(dir_item):
os.mkdir(dir_item)
def border_image(self, image_file_name: str, border_size=3, border_color=(212, 212, 212)):
"""
为图片添加边框
:param image_file_name: 待处理图片文件名
:param border_size: 边框宽度,默认值3px
:param border_color: 边框颜色,默认值为浅灰色
:return: None
"""
if not os.path.exists(self._get_source_file_abs_path(image_file_name)):
return
self._open_cv_border_image(image_file_name, border_size=border_size, border_color=border_color)
def _pillow_border_image(self, image_file_name: str, border_size: int, border_color):
origin_image = Image.open(self._get_source_file_abs_path(image_file_name))
(w, h) = origin_image.size
# 宽度为原始图片尺寸 + 两个border的尺寸
border_image = Image.new('RGB', (w + 2 * border_size, h + 2 * border_size), border_color)
border_image.paste(origin_image, (border_size, border_size))
self._prepare_output_abs_path(image_file_name)
border_image.save(self._get_output_file_abs_path(image_file_name))
def _open_cv_border_image(self, image_file_name: str, border_size: int, border_color: tuple):
origin_image = cv2.imread(self._get_source_file_abs_path(image_file_name))
bordered_image = cv2.copyMakeBorder(origin_image,
border_size, border_size, border_size, border_size,
cv2.BORDER_CONSTANT, value=border_color)
self._prepare_output_abs_path(image_file_name)
cv2.imwrite(self._get_output_file_abs_path(image_file_name), bordered_image)
监听指定目录
自动在目标目录下创建一个隐藏文件processed_cache.txt(以.开头),存放已经添加过边框的图片文件名。该示例中监听的目录为:/Users/jason315/Desktop/test-image,将添加边框后的图片保存到/Users/jason315/Desktop/image-bordered目录下
关键代码如下
import os
import cv2
import time
from PIL import Image
class ImageBorderHandler:
def __init__(self, source_folder: str, output_folder: str):
self.processed_cache_file = '.processed_cache.txt'
self.source_folder = source_folder
self.output_folder = output_folder
# 文件初始化
if not os.path.exists(output_folder):
os.mkdir(output_folder)
if not os.path.exists(self._get_processed_cache_abs_path()):
with open(self._get_processed_cache_abs_path(), 'w') as f:
f.flush()
# processed_cache初始化
self.processed_cache = set()
with open(self._get_processed_cache_abs_path(), 'r') as f:
for line in f.readlines():
self.processed_cache.add(line.replace('\n', ''))
@staticmethod
def _log_with_time(log_str):
"""
打印日志, 格式[yyyy-MM-dd HH:mm:ss] 日志内容
"""
print(f'[{time.strftime("%Y-%m-%d %H:%M:%S")}] {log_str}')
@staticmethod
def _is_expected_image_file(file_name: str):
suffix = file_name[file_name.rindex('.') + 1:]
return suffix.lower() in ['png', 'jpeg', 'jpg']
def _inner_process_file(self, the_file: str, processed_file_list: list[str]):
file_name = the_file[the_file.rindex(os.path.sep) + 1:]
if os.path.isdir(the_file):
# 递归处理文件夹
for file_item in os.listdir(the_file):
self._inner_process_file(f'{the_file}/{file_item}', processed_file_list)
return
elif file_name.startswith('.') or not self._is_expected_image_file(file_name):
return
# 处理还未添加边框的图片
path_under_source_folder = the_file.split(self.source_folder)[1]
if path_under_source_folder.startswith(os.path.sep):
path_under_source_folder = path_under_source_folder[1:]
if path_under_source_folder not in self.processed_cache:
self.border_image(path_under_source_folder)
self.processed_cache.add(path_under_source_folder)
processed_file_list.append(f'{path_under_source_folder}\n')
def _inner_process(self):
processed_file_list = []
self._inner_process_file(self.source_folder, processed_file_list)
if len(processed_file_list) > 0:
with open(self._get_processed_cache_abs_path(), 'a') as f:
f.writelines(processed_file_list)
f.flush()
self._log_with_time(f'add borders for {len(processed_file_list)} images')
def start_process(self):
"""
开始监听目标文件夹,每隔5秒处理1次
:return:
"""
self._log_with_time('the ImageBorderHandler starts working ...')
while(True):
self._inner_process()
time.sleep(5)
if __name__ == '__main__':
image_border_handler = ImageBorderHandler('/Users/jason315/Desktop/test-image', '/Users/jason315/Desktop/image-bordered')
image_border_handler.start_process()
启动后添加一些图片到test-image目录下,观察输出日志
配置开机自启动
新建文件image-border-auto-start.sh,编写脚本,调用Python启动main方法
cd /Users/jason315/Practice/pythonspace/jason-tool/com/jasonidea/tools
python ./image_border_handler.py
本文以MacOS设置开机自启动为例,Windows系统请参考这篇博客
1.修改自启动脚本运行权限
chmod 777 image-border-auto-start.sh
2.右键点击文件,修改打开方式
修改文件启动方式为Terminal(终端)
3.设置 > 用户与群组 > 登录项
添加自启动脚本
完整代码
image_border_handler.py
import os
import cv2
import time
from PIL import Image
class ImageBorderHandler:
def __init__(self, source_folder: str, output_folder: str):
self.processed_cache_file = '.processed_cache.txt'
self.source_folder = source_folder
self.output_folder = output_folder
# 文件初始化
if not os.path.exists(output_folder):
os.mkdir(output_folder)
if not os.path.exists(self._get_processed_cache_abs_path()):
with open(self._get_processed_cache_abs_path(), 'w') as f:
f.flush()
# processed_cache初始化
self.processed_cache = set()
with open(self._get_processed_cache_abs_path(), 'r') as f:
for line in f.readlines():
self.processed_cache.add(line.replace('\n', ''))
def _get_processed_cache_abs_path(self):
return os.path.join(self.source_folder, self.processed_cache_file)
def _get_source_file_abs_path(self, file_name: str):
return os.path.join(self.source_folder, file_name)
def _get_output_file_abs_path(self, file_name: str):
return os.path.join(self.output_folder, file_name)
def _prepare_output_abs_path(self, file_name: str):
"""
检查输出路径,如果父级目录不存在,自动创建
:param file_name: 待输出文件,可能含有父级目录
"""
full_path = os.path.join(self.output_folder, file_name)
if not os.path.exists(full_path):
dir_item = self.output_folder
for path_item in file_name.split(os.path.sep)[: -1]:
dir_item += f'{os.path.sep}{path_item}'
if not os.path.exists(dir_item):
os.mkdir(dir_item)
@staticmethod
def _log_with_time(log_str):
"""
打印日志, 格式[yyyy-MM-dd HH:mm:ss] 日志内容
"""
print(f'[{time.strftime("%Y-%m-%d %H:%M:%S")}] {log_str}')
def border_image(self, image_file_name: str, border_size=3, border_color=(212, 212, 212)):
"""
为图片添加边框
:param image_file_name: 待处理图片文件名
:param border_size: 边框宽度,默认值3px
:param border_color: 边框颜色,默认值为浅灰色
:return: None
"""
if not os.path.exists(self._get_source_file_abs_path(image_file_name)):
return
self._open_cv_border_image(image_file_name, border_size=border_size, border_color=border_color)
def _pillow_border_image(self, image_file_name: str, border_size: int, border_color):
origin_image = Image.open(self._get_source_file_abs_path(image_file_name))
(w, h) = origin_image.size
# 宽度为原始图片尺寸 + 两个border的尺寸
border_image = Image.new('RGB', (w + 2 * border_size, h + 2 * border_size), border_color)
border_image.paste(origin_image, (border_size, border_size))
self._prepare_output_abs_path(image_file_name)
border_image.save(self._get_output_file_abs_path(image_file_name))
def _open_cv_border_image(self, image_file_name: str, border_size: int, border_color: tuple):
origin_image = cv2.imread(self._get_source_file_abs_path(image_file_name))
bordered_image = cv2.copyMakeBorder(origin_image,
border_size, border_size, border_size, border_size,
cv2.BORDER_CONSTANT, value=border_color)
self._prepare_output_abs_path(image_file_name)
cv2.imwrite(self._get_output_file_abs_path(image_file_name), bordered_image)
@staticmethod
def _is_expected_image_file(file_name: str):
suffix = file_name[file_name.rindex('.') + 1:]
return suffix.lower() in ['png', 'jpeg', 'jpg']
def _inner_process_file(self, the_file: str, processed_file_list: list[str]):
file_name = the_file[the_file.rindex(os.path.sep) + 1:]
if os.path.isdir(the_file):
# 递归处理文件夹
for file_item in os.listdir(the_file):
self._inner_process_file(f'{the_file}/{file_item}', processed_file_list)
return
elif file_name.startswith('.') or not self._is_expected_image_file(file_name):
return
# 处理还未添加边框的图片
path_under_source_folder = the_file.split(self.source_folder)[1]
if path_under_source_folder.startswith(os.path.sep):
path_under_source_folder = path_under_source_folder[1:]
if path_under_source_folder not in self.processed_cache:
self.border_image(path_under_source_folder)
self.processed_cache.add(path_under_source_folder)
processed_file_list.append(f'{path_under_source_folder}\n')
def _inner_process(self):
processed_file_list = []
self._inner_process_file(self.source_folder, processed_file_list)
if len(processed_file_list) > 0:
with open(self._get_processed_cache_abs_path(), 'a') as f:
f.writelines(processed_file_list)
f.flush()
self._log_with_time(f'add borders for {len(processed_file_list)} images')
def start_process(self):
"""
开始监听目标文件夹,每隔5秒处理1次
:return:
"""
self._log_with_time('the ImageBorderHandler starts working ...')
while(True):
self._inner_process()
time.sleep(5)
if __name__ == '__main__':
image_border_handler = ImageBorderHandler('/Users/jason315/Desktop/test-image', '/Users/jason315/Desktop/image-bordered')
image_border_handler.start_process()