Next -23- 添加相册系列 -3- 获取图像信息、保存为json文件并上传图像

本文最后更新于:2021年12月11日 上午

相册需要在本地建立文件夹,为了维护相册,需要自动生成对应图像的json文件,并自动压缩上传,本文分享我的python程序,提供一种解决方案供大家使用参考。

准备工作

目标

  • 自动读取文件夹中的所有图像
  • 压缩到固定尺寸
  • 转存到指定的临时文件夹
  • 自动上传至七牛云
  • 自动提取图像信息并保存到json
  • 整合所有相册信息到json

工程代码

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import platform


class Parameters:

Image_Root_Path = r'/media/vvd/新加卷/pic' # root of source raw images
Hexo_Root_Path =r'/home/vvd/VVD_Work/Hexo/VVD_Hexo' # root of Hexo dir
Hexo_Sub_Dir_to_Photos = r'source/photos' # my source dir for album
Uploading_Temp_Image_Path = r'/home/vvd/VVD_Work/Hexo/Album_Temp' # root of temp image dir
Album_Ddescription_File_Name = 'readme.json' # name of json file in each image dir
Log_File_Name = 'Album_Log.log' # name of log file
Album_Total_Json = 'album.json' # json info of album
Album_Md_File_Name = 'index.md' # name of markdown file of hexo
Qshell_Config_File_Name = 'upload.conf' # qiniu qshell config file name

Logging_Object = None # object of logging class
Image_Info_Get_Object = None # instance of PhotoExifInfo

Image_Extension_List =['jpg','png','jpeg','bmp'] # list of common image extensions
Image_Resize_Width = 2000 # image width after resize
Whether_Overwrite_Old_Temp_File = False # whether overwrite old file while the program run again
Image_Url_Prefix =r'https://cdn.jsdelivr.net/gh/zywvvd/HexoFiles/vvd_hexo/' # prefix of image url
Force_Upload = False # upload images no matter what happens

Current_System = platform.system() # current operating system
if Current_System == 'Linux':
Split_Char ='/' # split char of current operating system
elif Current_System == 'Windows':
Split_Char ='\\'

图像信息提取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
60
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# getinfo.py

class PhotoExifInfo():

def __init__(self):

if os.path.exists('vvdkey.py'):
from vvdkey import baidu_key # 使用你自己的百度key
self.baidu_map_ak = baidu_key
else:
## set your baidu map key here
self.baidu_map_ak = ""

self.interested_keys = {
# camera Model
'Image Model':'Image_Model',#'相机型号',
# Aperture 1/value
'EXIF FNumber':'EXIF_FNumber',#'光圈',
# Focal Length
'EXIF FocalLength':'EXIF_FocalLength',#'焦距',
# Exposure Mode
'EXIF ExposureMode':'EXIF_ExposureMode',#'曝光模式',
# ExposureTime in seconds
'EXIF ExposureTime':'EXIF_ExposureTime',#'曝光时间',
# ISO
'EXIF ISOSpeedRatings':'EXIF_ISOSpeedRatings',#'ISO',
# date of photo
'Image DateTime':'Image_DateTime'#'拍摄时间',
}


def get_image_info(self,image_path):
"""
获取照片信息
"""
image_info_dict={}

with open(image_path, 'rb') as fp:
tags = exifread.process_file(fp)

for j in tags:
print(f"{j} :{tags[j]}")

for item in tuple(self.interested_keys):

try:
info = tags[item].printable
if item == 'EXIF FNumber':
if '/' in info:
A,B = info.split('/')
info = 'f'+format(int(A)/int(B),'.1f')
else:
info = 'f'+info
if item == 'EXIF FocalLength':
info = info+'mm'
if item == 'EXIF ExposureTime':
info = info+'s'
image_info_dict[self.interested_keys[item]] = info

except:
print(f'{image_path} has no attribute of {item}')
continue

try:
localcation = self._get_city_info(tags)
if localcation != "":
image_info_dict['positon'] = localcation

except:
print(f'{image_path} has no GPS info')

return image_info_dict


def _get_lng_lat(self, tags):
"""
经纬度转换
"""
try:

# 纬度
LatRef = tags["GPS GPSLatitudeRef"].printable
Lat = tags["GPS GPSLatitude"].printable[1:-1].replace(" ", "").replace("/", ",").split(",")
Lat = float(Lat[0]) + float(Lat[1]) / 60 + float(Lat[2]) / 3600
if LatRef != "N":
Lat = Lat * (-1)

# 经度
LonRef = tags["GPS GPSLongitudeRef"].printable
Lon = tags["GPS GPSLongitude"].printable[1:-1].replace(" ", "").replace("/", ",").split(",")
Lon = float(Lon[0]) + float(Lon[1]) / 60 + float(Lon[2]) / 3600
if LonRef != "E":
Lon = Lon * (-1)
return Lat,Lon
except:
print('Unable to get')



def _get_city_info(self, tags):

result = self._get_lng_lat(tags)
if result:
Lat, Lon = result
url = "https://api.map.baidu.com/reverse_geocoding/v3/?ak="+self.baidu_map_ak+"&output=json&coordtype=wgs84ll&location=" + str(Lat) + ',' + str(Lon)
#url = "https://api.map.baidu.com/reverse_geocoding/v3/?ak="+self.baidu_map_ak+"&output=json&coordtype=wgs84ll&location=31.225696563611,121.49884033194"
response = requests.get(url).json()
status = response['status']
if status == 0:
address = response['result']['formatted_address']
if address != "":
return(address)
else:
print('baidu_map error')
return ""

主要功能函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
60
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
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
280
281
282
283
284
285
286
287
288
289
290
291
292
import config
import json
import os
import cv2
import shutil
import logging
import time
from getinfo import PhotoExifInfo


def log_init():
"""
initialize logging
save the logging object in `config.Parameters.Logging_Object`

after this operation,
we could save logs with sample orders such as `logging.debug('test debug')` `logging.info('test info')`
logging level : debug < info < warning <error < critica
"""
log_file_path = os.path.join(config.Parameters.Uploading_Temp_Image_Path, config.Parameters.Log_File_Name)
if os.path.exists(log_file_path):
# open log file as mode of append
open_type = 'a'
else:
# open log file as mode of write
open_type = 'w'

logging.basicConfig(

# 日志级别,logging.DEBUG,logging.ERROR
level = logging.INFO,

# 日志格式: 时间、代码所在文件名、代码行号、日志级别名字、日志信息
#format = '%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
format = '%(asctime)s : %(message)s',

# 打印日志的时间
datefmt = '%Y-%m-%d %H:%M:%S',

# 日志文件存放的目录(目录必须存在)及日志文件名
filename = log_file_path,

# 打开日志文件的方式
filemode = open_type
)

config.Parameters.Logging_Object = logging


def log(message):
"""
print message to console and than save it to log
"""
print(message)
if config.Parameters.Logging_Object:
config.Parameters.Logging_Object.info(message)
else:
raise TypeError('there is sth wrong with the object of logging in config ')


def dir_check(dir_path):
"""
check if `dir_path` is a real directory path
"""
if not os.path.isdir(dir_path):
try:
os.makedirs(dir_path)
except Exception as err:
print(f'failed to make dir {dir_path}, error {err}')
finally:
log(f'{dir_path} does not exist and we have built it for you ')


def environment_check():
"""
check album environment, try to fix the matters if sth goes wrong
"""

## check important dirs
assert os.path.isdir(config.Parameters.Image_Root_Path) ,f"image root dir {config.Parameters.Image_Root_Path} does not exist"
assert os.path.isdir(config.Parameters.Hexo_Root_Path) ,f"hexo root dir {config.Parameters.Hexo_Root_Path} does not exist"

dir_check(config.Parameters.Uploading_Temp_Image_Path)

## check image width, I think it shold not < 200
config.Parameters.Image_Resize_Width = max(int(config.Parameters.Image_Resize_Width), 200)

## initialize logging
log_init()

## image_info_get object
config.Parameters.Image_Info_Get_Object = PhotoExifInfo()


def init_json_create(json_path):
"""
init a json file to `json_path`
"""
assert os.path.basename(json_path).split('.')[-1]=='json',f'{json_path} should be a path of json file '
assert os.path.isdir(os.path.split(json_path)[0]), 'the folder does not exist'

now = time.localtime()
info_dict={}

## time
my_time={
'year':now.tm_year,\
'month':now.tm_mon,\
'day':now.tm_mday
}
info_dict['time'] = my_time

## type
info_dict['type'] = 'album type'

## model
info_dict['model'] = ''

## position
position = {
'city':'beijing',\
'street':'Tiananmen'
}
info_dict['position'] = position

## title
info_dict['title'] = 'Album title'

## balabala
info_dict['balabala'] = 'description of album'

with open(json_path, mode='w') as fp:
json.dump(info_dict, fp, indent=3)
log(f'make initial json file {json_path}')


def image_compression_and_save(src_dir, tar_dir):
"""
compress and move images from src to tar
"""
sub_image_dirs = os.listdir(src_dir)

for sub_image_dir in sub_image_dirs:
src_sub_image_path = os.path.join(src_dir, sub_image_dir)
tar_sub_image_path = os.path.join(tar_dir, sub_image_dir)

## check tar dir
dir_check(tar_sub_image_path)

file_list = os.listdir(src_sub_image_path)
for item in file_list:
extension = item.split('.')[-1]

if extension.lower() in config.Parameters.Image_Extension_List:
## is image file
image_path = os.path.join(src_sub_image_path, item)

tar_image_path = os.path.join(tar_sub_image_path, item)

if not os.path.exists(tar_image_path) or config.Parameters.Whether_Overwrite_Old_Temp_File:
## new file or overwrite

image = cv2.imread(image_path)
image_shape = image.shape

if image_shape[1] >= config.Parameters.Image_Resize_Width:
## need compression
new_width = int(config.Parameters.Image_Resize_Width)
new_height = int(config.Parameters.Image_Resize_Width/image_shape[1]*image_shape[0])

new_image = cv2.resize(image,(new_width, new_height) )
cv2.imwrite(tar_image_path, new_image)
log(f'image {os.path.join(sub_image_dir,item)} has been resized to {(new_width, new_height)} and moved to temp dir')

else:
## small image, copy directly
shutil.copy(image_path, tar_image_path)
log(f'image {os.path.join(sub_image_dir,item)} has been moved to temp dir')


def deal_with_sub_json(src_dir, tar_dir):
"""
deal with sub json files in each sub dir
"""
sub_image_dirs = os.listdir(src_dir)

for sub_image_dir in sub_image_dirs:

src_sub_image_path = os.path.join(src_dir, sub_image_dir)
tar_sub_image_path = os.path.join(tar_dir, sub_image_dir)

## check tar dir
dir_check(tar_sub_image_path)

src_info_json_file_path = os.path.join(src_sub_image_path, config.Parameters.Album_Ddescription_File_Name)

if not os.path.exists(src_info_json_file_path):
init_json_create(src_info_json_file_path)
else:
print(f'{src_info_json_file_path} found')

with open(src_info_json_file_path) as fp:
album_info = json.load(fp)

file_list = os.listdir(src_sub_image_path)

image_info_list = []

for item in file_list:
extension = item.split('.')[-1]

if extension.lower() in config.Parameters.Image_Extension_List:
## is image file
image_path = os.path.join(src_sub_image_path, item)

assert not config.Parameters.Image_Info_Get_Object is None, "Image_Info_Get_Object should be a instance "
image_info_dict = config.Parameters.Image_Info_Get_Object.get_image_info(image_path)

image_info_dict['url'] = config.Parameters.Image_Url_Prefix+sub_image_dir+'/'+item

image_info_list.append(image_info_dict)

album_info['image_info'] = image_info_list
album_info['directory'] = sub_image_dir

tar_info_json_file_path = os.path.join(tar_sub_image_path, config.Parameters.Album_Ddescription_File_Name)

with open(tar_info_json_file_path,'w',encoding='utf-8') as fp:
json.dump(album_info, fp, indent=3, ensure_ascii=False)
log(f'json file of image dir {sub_image_dir} created')


def json_integrate(temp_root):
'''
integrate json files to a single file
'''
sub_image_dirs = os.listdir(temp_root)

album_dict = {}

album_json_list = []

for sub_image_dir in sub_image_dirs:
if os.path.isdir(os.path.join(temp_root,sub_image_dir)):
json_file_path = os.path.join(temp_root,sub_image_dir, config.Parameters.Album_Ddescription_File_Name)
assert os.path.exists(json_file_path),f'file {json_file_path} not flund'

with open(json_file_path) as fp:
json_dict = json.load(fp)

album_json_list.append(json_dict)

album_dict['album'] = album_json_list

integrate_json_path = os.path.join(temp_root, config.Parameters.Album_Total_Json)
with open(integrate_json_path, mode='w', encoding='utf-8') as fp:
json.dump(album_dict, fp, ensure_ascii=False, indent=3)
log(f'total json {integrate_json_path} saved')

return album_dict

def make_dir_json_md_of_album(temp_root, hexo_photos_path, album_dict):
'''
save md files to hexo source/photos dir
'''
assert os.path.isdir(hexo_photos_path) ,'hexo photos path {hexo_photos_path} does not exist'

src_album_json_path = os.path.join(temp_root, config.Parameters.Album_Total_Json)
assert os.path.exists(src_album_json_path), f'album json file {src_album_json_path} not found'
tar_album_json_path = os.path.join(hexo_photos_path, config.Parameters.Album_Total_Json)

shutil.copy(src_album_json_path, tar_album_json_path)
log(f"json file {config.Parameters.Album_Total_Json} moved to hexo dir")
for sub_image_dict in album_dict['album']:
src_json_path = os.path.join(temp_root, sub_image_dict['directory'], config.Parameters.Album_Ddescription_File_Name)
assert os.path.exists(src_json_path), f'sub json file {src_json_path} not found'

tar_json_path = os.path.join(hexo_photos_path, sub_image_dict['directory'], config.Parameters.Album_Ddescription_File_Name)
image_dir = os.path.join(hexo_photos_path, sub_image_dict['directory'])
dir_check(image_dir)

shutil.copyfile(src_json_path, tar_json_path)
log(f"json file {sub_image_dict['directory'] +'_'+ config.Parameters.Album_Ddescription_File_Name} moved to hexo dir")

with open(os.path.join(image_dir,config.Parameters.Album_Md_File_Name),'w', encoding='utf-8') as fp:
fp.writelines('---'+'\n')
fp.writelines('title: '+sub_image_dict['title']+'\n')
fp.writelines('date: '+str(sub_image_dict['time']['year'])+'-'+str(sub_image_dict['time']['month'])+'-'+str(sub_image_dict['time']['day'])+'\n')
fp.writelines('type: photography'+'\n')
fp.writelines('album: '+sub_image_dict['directory']+'\n')
fp.writelines('---'+'\n')
log(f'create md file of {os.path.join(image_dir,config.Parameters.Album_Md_File_Name)}')

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import os
import config

from funs import environment_check
from funs import deal_with_sub_json
from funs import json_integrate
from funs import image_compression_and_save
from funs import make_dir_json_md_of_album



if __name__ =='__main__':

src_image_root = config.Parameters.Image_Root_Path
temp_image_root = config.Parameters.Uploading_Temp_Image_Path
hexo_photos_path = os.path.join(config.Parameters.Hexo_Root_Path,config.Parameters.Hexo_Sub_Dir_to_Photos)

## check config environment
environment_check()

## compress and save images
image_compression_and_save(src_image_root, temp_image_root)

## get info of images and save json of each dir
deal_with_sub_json(src_image_root, temp_image_root)

## integrate jsons
album_dict = json_integrate(temp_image_root)

## make folders and markdowns in dir of photos
make_dir_json_md_of_album(temp_image_root, hexo_photos_path, album_dict)

## upload images with qshell
if config.Parameters.Force_Upload:
os.system("rm -rf ~/.qshell/qupload")

cmd = "qshell qupload "+os.path.join(config.Parameters.Uploading_Temp_Image_Path,config.Parameters.Qshell_Config_File_Name)
os.system(cmd)

主要函数做了注释,希望可以给大家启发,如果想使用我这套代码可以到 hexo_album_process 下载

运行图示

  • 初始相册文件夹pic

  • 相册内部

  • 每个文件夹需要有readme.json记录文件夹信息,程序会自动提取,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"time":
{
"year":2015,
"month":11,
"day":28
},
"type":"风光",
"model":"",
"position":
{
"city":"合肥",
"street":"ustc"
},
"title":"USTC",
"balabala":
"关于母校。"
}
1
2
3
4
5
Image_Root_Path = r'/media/vvd/新加卷/pic'                                   												# root of source raw images
Hexo_Root_Path =r'/home/vvd/VVD_Work/Hexo/VVD_Hexo' # root of Hexo dir
Hexo_Sub_Dir_to_Photos = r'source/photos' # my source dir for album
Uploading_Temp_Image_Path = r'/home/vvd/VVD_Work/Hexo/Album_Temp' # root of temp image dir
Image_Url_Prefix =r'https://cdn.jsdelivr.net/gh/zywvvd/HexoFiles/vvd_hexo/' # prefix of image url

  • 临时文件夹产生压缩后的图像文件夹与json文件

  • 程序会在hexo->source-> photos文件夹内写入相册文件夹层级结构与对应json文件

  • 文件夹内的json加入了图像的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"time": {
"year": 2015,
"month": 11,
"day": 28
},
"type": "风光",
"model": "",
"position": {
"city": "合肥",
"street": "ustc"
},
"title": "USTC",
"balabala": "关于母校。",
"image_info": [
{
"Image_Model": "NIKON D610",
"EXIF_FNumber": "f11",
"EXIF_FocalLength": "35mm",
"EXIF_ExposureMode": "Auto Exposure",
"EXIF_ExposureTime": "6s",
"EXIF_ISOSpeedRatings": "100",
"Image_DateTime": "2015:11:28 00:07:05",
"url": "https://cdn.jsdelivr.net/gh/zywvvd/HexoFiles/vvd_hexo/2015_11_28_hefei_ustc_fengjing/1.jpg"
},
{
"Image_Model": "NIKON D610",
"EXIF_FNumber": "f4",
"EXIF_FocalLength": "34mm",
"EXIF_ExposureMode": "Manual Exposure",
"EXIF_ExposureTime": "25s",
"Image_DateTime": "2015:11:28 22:13:29",
"url": "https://cdn.jsdelivr.net/gh/zywvvd/HexoFiles/vvd_hexo/2015_11_28_hefei_ustc_fengjing/2.jpg"
}
],
"directory": "2015_11_28_hefei_ustc_fengjing"
}
  • 图像会自动上传到七牛云

小结

文中没有太先进的技术,主要提供了为静态页面自动维护相册问题的解决思路,并分享了json生成与图像上传的工程,有更好的实现方法欢迎交流。

参考链接

本文启发自 https://asdfv1929.github.io/2018/05/26/next-add-photos/ 感谢前辈分享