All scripts working, initial commit
This commit is contained in:
commit
238921eda3
5 changed files with 1149 additions and 0 deletions
59
ffmpeg_video.py
Normal file
59
ffmpeg_video.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import subprocess
|
||||
import os
|
||||
import shutil
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from sortedcontainers import SortedDict
|
||||
import tqdm
|
||||
|
||||
path_to_images = r"..\composite\goes16"
|
||||
temp_path = r"..\vid"
|
||||
output_file = r"..\goes16.mp4"
|
||||
starttime = time.mktime(datetime.datetime(2024, 1, 1).timetuple())
|
||||
stoptime = time.mktime(datetime.datetime(2099, 1, 1).timetuple())
|
||||
min_file_size = 200000 # Detect and remove corrupted files by filtering by file size
|
||||
max_file_size = 500000
|
||||
|
||||
os.makedirs(temp_path, exist_ok = True)
|
||||
|
||||
files_by_time = SortedDict()
|
||||
for root, dirs, files in tqdm.tqdm(os.walk(path_to_images), desc="Finding and filtering files."):
|
||||
for f in files:
|
||||
if f.endswith('.jpg'):
|
||||
fpath = os.path.join(root, f)
|
||||
fsize = os.path.getsize(fpath)
|
||||
ftime = int(f.replace('-', '.').split('.')[-2])
|
||||
if (fsize > min_file_size) and (fsize < max_file_size) and (ftime > starttime) and (ftime < stoptime):
|
||||
files_by_time[ftime] = fpath
|
||||
|
||||
difftimes = np.diff(files_by_time.keys())
|
||||
unique, counts = np.unique(difftimes, return_counts=True)
|
||||
interval = unique[0]
|
||||
assert np.sum((unique % interval) > 0) == 0 # Ensure all our timestamps align perfectly with our interval
|
||||
|
||||
prevtime = files_by_time.peekitem(0)[0] - interval
|
||||
index = 0
|
||||
newfpath = ""
|
||||
for t, f in tqdm.tqdm(files_by_time.items(), desc="Copying files to transcode dir", total = len(files_by_time)):
|
||||
framejump = (t - prevtime) // interval
|
||||
if framejump < 60:
|
||||
for i in range(framejump - 1): # Repeat old frame to fill gaps
|
||||
shutil.copy(newfpath, f"{os.path.join(temp_path, f"{index+1:06d}.jpg")}")
|
||||
index += 1
|
||||
else: # Skip past intervals that are too large to fill reasonably
|
||||
print(f"Detected a frame gap of: {framejump}!")
|
||||
newfpath = f"{os.path.join(temp_path, f"{index+1:06d}.jpg")}"
|
||||
shutil.copy(f, newfpath)
|
||||
prevtime = t
|
||||
index += 1
|
||||
|
||||
command_line = f'ffmpeg -framerate 60 -pattern_type sequence -i "{os.path.join(temp_path, r"%06d.jpg")}" -c:v libx264 -crf 18 -preset veryfast {output_file}'
|
||||
print(command_line)
|
||||
|
||||
pipe = subprocess.Popen(command_line, shell=True, stdout=subprocess.PIPE).stdout
|
||||
output = pipe.read().decode()
|
||||
pipe.close()
|
||||
|
||||
shutil.rmtree(temp_path)
|
||||
269
merger.py
Normal file
269
merger.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import os
|
||||
import time
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from multiprocessing import Queue, Process
|
||||
|
||||
import tqdm
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
def bin_ndarray(ndarray, new_shape, operation='mean'):
|
||||
"""
|
||||
Bins an ndarray in all axes based on the target shape, by summing or
|
||||
averaging.
|
||||
|
||||
Number of output dimensions must match number of input dimensions and
|
||||
new axes must divide old ones.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> m = np.arange(0,100,1).reshape((10,10))
|
||||
>>> n = bin_ndarray(m, new_shape=(5,5), operation='sum')
|
||||
>>> print(n)
|
||||
|
||||
[[ 22 30 38 46 54]
|
||||
[102 110 118 126 134]
|
||||
[182 190 198 206 214]
|
||||
[262 270 278 286 294]
|
||||
[342 350 358 366 374]]
|
||||
|
||||
"""
|
||||
operation = operation.lower()
|
||||
if not operation in ['sum', 'mean']:
|
||||
raise ValueError("Operation not supported.")
|
||||
if ndarray.ndim != len(new_shape):
|
||||
raise ValueError("Shape mismatch: {} -> {}".format(ndarray.shape,
|
||||
new_shape))
|
||||
compression_pairs = [(d, c//d) for d,c in zip(new_shape,
|
||||
ndarray.shape)]
|
||||
flattened = [l for p in compression_pairs for l in p]
|
||||
ndarray = ndarray.reshape(flattened)
|
||||
for i in range(len(new_shape)):
|
||||
op = getattr(ndarray, operation)
|
||||
ndarray = op(-1*(i+1))
|
||||
return ndarray
|
||||
|
||||
def gamma_correct(fun):
|
||||
def wrapper(*args, **kwargs):
|
||||
args = list(args)
|
||||
args[0] = np.power(args[0], 2.2)
|
||||
args[1] = np.power(args[1], 2.2)
|
||||
args = tuple(args)
|
||||
result = fun(*args, **kwargs)
|
||||
return np.power(result, 1/2.2)
|
||||
return wrapper
|
||||
|
||||
def clip_color(fun):
|
||||
def wrapper(*args, **kwargs):
|
||||
return np.clip(fun(*args, **kwargs), 0.0, 1.0)
|
||||
return wrapper
|
||||
|
||||
# linear_srgb_matrix = np.array([[0.4124, 0.3576, 0.1805],
|
||||
# [0.2126, 0.7152, 0.0722],
|
||||
# [0.0193, 0.1192, 0.9505]])
|
||||
|
||||
# linear_srgb_matrix_inv = np.array([[ 3.2406, -1.5372, -0.4986],
|
||||
# [-0.9689, 1.8758, 0.0415],
|
||||
# [ 0.0557, -0.2040, 1.0570]])
|
||||
|
||||
# def linear_color_correction(fun):
|
||||
# def wrapper(*args, **kwargs):
|
||||
# args = list(args)
|
||||
# inds = args[0] <= 0.04045
|
||||
# ninds = args[0] > 0.04045
|
||||
# for i in range(2):
|
||||
# args[i][inds] = args[i][inds] / 12.92
|
||||
# args[i][ninds] = np.power((args[i][ninds] + 0.055) / 1.055, 2.4)
|
||||
# for x in range(args[i].shape[0]):
|
||||
# for y in range(args[i].shape[1]):
|
||||
# args[i][x,y,:] = np.matmul(linear_srgb_matrix, args[i][x,y,:])
|
||||
# args = tuple(args)
|
||||
# result = fun(*args, **kwargs)
|
||||
|
||||
# for x in range(result.shape[0]):
|
||||
# for y in range(result.shape[1]):
|
||||
# result[x,y,:] = np.matmul(linear_srgb_matrix_inv, result[x,y,:])
|
||||
# inds = result <= 0.0031308
|
||||
# ninds = result > 0.0031308
|
||||
# result[inds] = result[inds] * 12.92
|
||||
# result[ninds] = np.power(result[ninds], 1.0/2.4) * 1.055 - 0.055
|
||||
# return result
|
||||
# return wrapper
|
||||
|
||||
|
||||
@gamma_correct
|
||||
def composite_alpha_over(F, B, alpha_F, alpha_B = 1):
|
||||
return (F*alpha_F + B*alpha_B*(1-alpha_F)) / (alpha_F + alpha_B*(1-alpha_F))
|
||||
|
||||
def composite_alpha_blend(F, B, alpha):
|
||||
return F*alpha + B*(1-alpha)
|
||||
|
||||
def linear_burn(F, B):
|
||||
burn = F + B - 1
|
||||
burn[burn < 0.0] = 0.0
|
||||
return burn
|
||||
|
||||
def difference(F, B):
|
||||
return np.abs(F - B)
|
||||
|
||||
@clip_color
|
||||
def linear_light(F, B):
|
||||
result = np.zeros_like(F)
|
||||
inds = F <= 0.5
|
||||
ninds = F > 0.5
|
||||
result[inds] = B[inds] + 2.0 * F[inds] - 1
|
||||
result[ninds] = 2.0 * (F[ninds] - 0.5) + B[ninds]
|
||||
return result
|
||||
|
||||
@clip_color
|
||||
def hard_light(F, B):
|
||||
result = np.zeros_like(F)
|
||||
inds = B < 0.5
|
||||
ninds = B >= 0.5
|
||||
result[inds] = 2 * F[inds] * B[inds]
|
||||
result[ninds] = 1 - (2*(1 - F[ninds])*(1 - B[ninds]))
|
||||
return result
|
||||
|
||||
@clip_color
|
||||
def color_dodge(F, B):
|
||||
return B / (1.000001 - F)
|
||||
|
||||
@clip_color
|
||||
def exclusion(F, B):
|
||||
return F + B - 2*F*B
|
||||
|
||||
@clip_color
|
||||
def saturation(img, R, G, B):
|
||||
img[:,:,0] *= R
|
||||
img[:,:,1] *= G
|
||||
img[:,:,2] *= B
|
||||
return img
|
||||
|
||||
@clip_color
|
||||
def contrast(img, c, b):
|
||||
return (img - 0.5) * c + 0.5 + b*c
|
||||
|
||||
def generate_composite(work_queue, result_queue):
|
||||
while True:
|
||||
try:
|
||||
job = work_queue.get()
|
||||
if job is None:
|
||||
break
|
||||
files_this_timestamp, timestamp, processed_images_dir = job
|
||||
filename = f"Composite-{int(timestamp)}.jpg"
|
||||
filepath = os.path.join(processed_images_dir, filename)
|
||||
if os.path.isfile(filepath):
|
||||
result_queue.put(("Exists", timestamp))
|
||||
continue
|
||||
|
||||
# image_names = ["094Å", "131Å", "171Å", "195Å", "284Å", "304Å"]
|
||||
data = []
|
||||
for i in range(6):
|
||||
img = Image.open(files_this_timestamp[i])
|
||||
trimmed_img_data = np.array(img)[40:-40,40:-40,:3] # Trim off edges of image to remove text
|
||||
normalized_img_data = trimmed_img_data / 255.0 # Normalize to float 0.0-1.0 instead of uint8
|
||||
srgb_img_data = np.power(normalized_img_data, 2.2) # Gamma correct to sRGB color space
|
||||
data.append(normalized_img_data)
|
||||
|
||||
# Assemble composite image
|
||||
composite_image_data = data[4] # Start with (284Å)
|
||||
# Do a linear burn with 304Å at 95% alpha
|
||||
composite_image_data = composite_alpha_over(linear_burn(data[5], composite_image_data), composite_image_data, 0.95)
|
||||
# Do a difference operation with 195Å at 95% alpha
|
||||
composite_image_data = composite_alpha_over(exclusion(data[3], composite_image_data), composite_image_data, 0.90)
|
||||
# Do a linear_light layer op with 171Å
|
||||
composite_image_data = linear_light(data[2], composite_image_data)
|
||||
# Do a hard_light layer op with 131Å
|
||||
composite_image_data = composite_alpha_over(hard_light(data[1], composite_image_data), composite_image_data, 0.20)
|
||||
# Do a color_dodge layer op with 094Å
|
||||
composite_image_data = composite_alpha_over(color_dodge(data[0], composite_image_data), composite_image_data, 0.25)
|
||||
# Do an exclusion layer op with 094Å
|
||||
composite_image_data = composite_alpha_over(exclusion(data[0], composite_image_data), composite_image_data, 0.80)
|
||||
# Tweak the colors a little
|
||||
composite_image_data = saturation(composite_image_data, 1.0, 0.95, 1.15)
|
||||
# Boost contrast
|
||||
composite_image_data = contrast(composite_image_data, 1.5, 0.15)
|
||||
|
||||
# Now shrink the component images and assemble them alongside the composite.
|
||||
new_dim = composite_image_data.shape[0] // 3
|
||||
# Enlarge the composite to fit the new images
|
||||
composite_image_data = np.pad(composite_image_data, ((0,0),(new_dim, new_dim),(0,0)))
|
||||
for i in range(6):
|
||||
resized = bin_ndarray(data[i], (new_dim, new_dim, 3))
|
||||
if i < 3:
|
||||
composite_image_data[i*new_dim:(i+1)*new_dim, :new_dim, :] = resized
|
||||
else:
|
||||
composite_image_data[(i-3)*new_dim:(i-2)*new_dim, -new_dim:, :] = resized
|
||||
|
||||
img = Image.fromarray((255 * composite_image_data).astype('uint8'))
|
||||
timestring = datetime.datetime.fromtimestamp(timestamp, tz = datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
|
||||
ImageDraw.Draw(img).text((655, 15), f"NOAA GOES Sattelite SUVI Composite - {timestring} UTC",(255,255,255), font_size = 24)
|
||||
img.save(filepath, quality = 90)
|
||||
result_queue.put(("Created", timestamp))
|
||||
|
||||
# plt.figure("Composite")
|
||||
# plt.imshow(composite_image_data)
|
||||
# plt.show()
|
||||
# plt.close('all')
|
||||
except Exception as e:
|
||||
result_queue.put((e, timestamp))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
stored_images_dir = r"..\comp"
|
||||
processed_images_dir = r"..\composite"
|
||||
nworkers = 8
|
||||
os.makedirs(processed_images_dir, exist_ok=True)
|
||||
|
||||
files_sorted_by_timestamp = defaultdict(list)
|
||||
for root, dirs, files in os.walk(stored_images_dir):
|
||||
for f in files:
|
||||
if f.endswith(".png"):
|
||||
file_parts = f.split("_")
|
||||
measurement = file_parts[1]
|
||||
sattelite = file_parts[2]
|
||||
measure_end_time = datetime.datetime.strptime(file_parts[4][1:16], "%Y%m%dT%H%M%S")
|
||||
measure_end_time.replace(tzinfo=datetime.timezone.utc)
|
||||
measure_end_time = time.mktime(measure_end_time.timetuple())
|
||||
files_sorted_by_timestamp[measure_end_time].append(os.path.join(root,f))
|
||||
|
||||
work_queue = Queue(maxsize = 3)
|
||||
result_queue = Queue()
|
||||
workers = []
|
||||
for i in range(nworkers):
|
||||
p = Process(target = generate_composite, args = (work_queue, result_queue), daemon=True)
|
||||
p.start()
|
||||
workers.append(p)
|
||||
|
||||
for timestamp in tqdm.tqdm(files_sorted_by_timestamp, desc="Creating Composite Solar Images"):
|
||||
files_this_timestamp = files_sorted_by_timestamp[timestamp]
|
||||
files_this_timestamp = sorted(files_this_timestamp)
|
||||
if not len(files_this_timestamp) == 7:
|
||||
print(f"Detected a file gap at: {timestamp}")
|
||||
continue
|
||||
|
||||
work_queue.put((files_this_timestamp, timestamp, processed_images_dir))
|
||||
|
||||
ncreated = 0
|
||||
nexists = 0
|
||||
for _ in range(len(files_sorted_by_timestamp)):
|
||||
result = result_queue.get(5.0)
|
||||
if result[0] == "Exists":
|
||||
nexists += 1
|
||||
elif result[0] == "Created":
|
||||
ncreated += 1
|
||||
else:
|
||||
print(f"A worker encountered an exception on job {result[1]}: {result[0]}")
|
||||
|
||||
for _ in range(nworkers):
|
||||
try:
|
||||
work_queue.put(None, timeout=1.0)
|
||||
except:
|
||||
break
|
||||
|
||||
for w in workers:
|
||||
w.join(5.0)
|
||||
|
||||
print("Done")
|
||||
502
merger_FITS.py
Normal file
502
merger_FITS.py
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
import os
|
||||
import time
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from multiprocessing import Queue, Process
|
||||
import re
|
||||
import warnings
|
||||
import queue
|
||||
|
||||
import tqdm
|
||||
from PIL import Image, ImageDraw
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
from astropy.io import fits
|
||||
import palettable
|
||||
|
||||
def lowpriority():
|
||||
""" Set the priority of the process to below-normal."""
|
||||
|
||||
import sys
|
||||
try:
|
||||
sys.getwindowsversion()
|
||||
except AttributeError:
|
||||
isWindows = False
|
||||
else:
|
||||
isWindows = True
|
||||
|
||||
if isWindows:
|
||||
# Based on:
|
||||
# "Recipe 496767: Set Process Priority In Windows" on ActiveState
|
||||
# http://code.activestate.com/recipes/496767/
|
||||
import win32api,win32process,win32con
|
||||
|
||||
pid = win32api.GetCurrentProcessId()
|
||||
handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid)
|
||||
win32process.SetPriorityClass(handle, win32process.BELOW_NORMAL_PRIORITY_CLASS)
|
||||
else:
|
||||
import os
|
||||
|
||||
os.nice(1)
|
||||
|
||||
def bin_ndarray(ndarray, new_shape, operation='mean'):
|
||||
"""
|
||||
Bins an ndarray in all axes based on the target shape, by summing or
|
||||
averaging.
|
||||
|
||||
Number of output dimensions must match number of input dimensions and
|
||||
new axes must divide old ones.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> m = np.arange(0,100,1).reshape((10,10))
|
||||
>>> n = bin_ndarray(m, new_shape=(5,5), operation='sum')
|
||||
>>> print(n)
|
||||
|
||||
[[ 22 30 38 46 54]
|
||||
[102 110 118 126 134]
|
||||
[182 190 198 206 214]
|
||||
[262 270 278 286 294]
|
||||
[342 350 358 366 374]]
|
||||
|
||||
"""
|
||||
operation = operation.lower()
|
||||
if not operation in ['sum', 'mean']:
|
||||
raise ValueError("Operation not supported.")
|
||||
if ndarray.ndim != len(new_shape):
|
||||
raise ValueError("Shape mismatch: {} -> {}".format(ndarray.shape,
|
||||
new_shape))
|
||||
compression_pairs = [(d, c//d) for d,c in zip(new_shape,
|
||||
ndarray.shape)]
|
||||
flattened = [l for p in compression_pairs for l in p]
|
||||
ndarray = ndarray.reshape(flattened)
|
||||
for i in range(len(new_shape)):
|
||||
op = getattr(ndarray, operation)
|
||||
ndarray = op(-1*(i+1))
|
||||
return ndarray
|
||||
|
||||
def gamma_correct(fun):
|
||||
def wrapper(*args, **kwargs):
|
||||
args = list(args)
|
||||
args[0] = np.power(args[0], 2.2)
|
||||
args[1] = np.power(args[1], 2.2)
|
||||
args = tuple(args)
|
||||
result = fun(*args, **kwargs)
|
||||
return np.power(result, 1/2.2)
|
||||
return wrapper
|
||||
|
||||
def clip_color(fun):
|
||||
def wrapper(*args, **kwargs):
|
||||
return np.clip(fun(*args, **kwargs), 0.0, 1.0)
|
||||
return wrapper
|
||||
|
||||
# linear_srgb_matrix = np.array([[0.4124, 0.3576, 0.1805],
|
||||
# [0.2126, 0.7152, 0.0722],
|
||||
# [0.0193, 0.1192, 0.9505]])
|
||||
|
||||
# linear_srgb_matrix_inv = np.array([[ 3.2406, -1.5372, -0.4986],
|
||||
# [-0.9689, 1.8758, 0.0415],
|
||||
# [ 0.0557, -0.2040, 1.0570]])
|
||||
|
||||
# def linear_color_correction(fun):
|
||||
# def wrapper(*args, **kwargs):
|
||||
# args = list(args)
|
||||
# inds = args[0] <= 0.04045
|
||||
# ninds = args[0] > 0.04045
|
||||
# for i in range(2):
|
||||
# args[i][inds] = args[i][inds] / 12.92
|
||||
# args[i][ninds] = np.power((args[i][ninds] + 0.055) / 1.055, 2.4)
|
||||
# for x in range(args[i].shape[0]):
|
||||
# for y in range(args[i].shape[1]):
|
||||
# args[i][x,y,:] = np.matmul(linear_srgb_matrix, args[i][x,y,:])
|
||||
# args = tuple(args)
|
||||
# result = fun(*args, **kwargs)
|
||||
|
||||
# for x in range(result.shape[0]):
|
||||
# for y in range(result.shape[1]):
|
||||
# result[x,y,:] = np.matmul(linear_srgb_matrix_inv, result[x,y,:])
|
||||
# inds = result <= 0.0031308
|
||||
# ninds = result > 0.0031308
|
||||
# result[inds] = result[inds] * 12.92
|
||||
# result[ninds] = np.power(result[ninds], 1.0/2.4) * 1.055 - 0.055
|
||||
# return result
|
||||
# return wrapper
|
||||
|
||||
|
||||
@gamma_correct
|
||||
def composite_alpha_over(F, B, alpha_F, alpha_B = 1):
|
||||
return (F*alpha_F + B*alpha_B*(1-alpha_F)) / (alpha_F + alpha_B*(1-alpha_F))
|
||||
|
||||
def composite_alpha_blend(F, B, alpha):
|
||||
return F*alpha + B*(1-alpha)
|
||||
|
||||
def linear_burn(F, B):
|
||||
burn = F + B - 1
|
||||
burn[burn < 0.0] = 0.0
|
||||
return burn
|
||||
|
||||
def difference(F, B):
|
||||
d = np.abs(F - B)
|
||||
if d.shape[2] == 4: # Preserve alpha of base image
|
||||
d[:,:,3] = B[:,:,3]
|
||||
return d
|
||||
|
||||
@clip_color
|
||||
def linear_light(F, B):
|
||||
result = np.zeros_like(F)
|
||||
inds = F <= 0.5
|
||||
ninds = F > 0.5
|
||||
result[inds] = B[inds] + 2.0 * F[inds] - 1
|
||||
result[ninds] = 2.0 * (F[ninds] - 0.5) + B[ninds]
|
||||
return result
|
||||
|
||||
@clip_color
|
||||
def hard_light(F, B):
|
||||
result = np.zeros_like(F)
|
||||
inds = B < 0.5
|
||||
ninds = B >= 0.5
|
||||
result[inds] = 2 * F[inds] * B[inds]
|
||||
result[ninds] = 1 - (2*(1 - F[ninds])*(1 - B[ninds]))
|
||||
return result
|
||||
|
||||
@clip_color
|
||||
def color_dodge(F, B):
|
||||
return B / (1.000001 - F)
|
||||
|
||||
@clip_color
|
||||
def exclusion(F, B):
|
||||
d = F + B - 2*F*B
|
||||
if d.shape[2] == 4: # Preserve alpha of base image
|
||||
d[:,:,3] = B[:,:,3]
|
||||
return d
|
||||
|
||||
@clip_color
|
||||
def saturation(img, R, G, B):
|
||||
img[:,:,0] *= R
|
||||
img[:,:,1] *= G
|
||||
img[:,:,2] *= B
|
||||
return img
|
||||
|
||||
@clip_color
|
||||
def contrast(img, c, b):
|
||||
return (img - 0.5) * c + 0.5 + b*c
|
||||
|
||||
def rgb_to_hsl(img):
|
||||
r = img[:,:,0]
|
||||
g = img[:,:,1]
|
||||
b = img[:,:,2]
|
||||
cmax = np.copy(r)
|
||||
cmax[g > cmax] = g[g > cmax]
|
||||
cmax[b > cmax] = b[b > cmax]
|
||||
cmin = np.copy(r)
|
||||
cmin[g < cmin] = g[g < cmin]
|
||||
cmin[b < cmin] = b[b < cmin]
|
||||
delta = cmax - cmin
|
||||
|
||||
|
||||
# Calc hue
|
||||
hue = np.zeros_like(r)
|
||||
inds = cmax == r
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore')
|
||||
hue[inds] = 60 * np.mod((g[inds]-b[inds])/delta[inds], 6)
|
||||
inds = cmax == g
|
||||
hue[inds] = 60 * ((b[inds]-r[inds])/delta[inds] + 2)
|
||||
inds = cmax == b
|
||||
hue[inds] = 60 * ((r[inds]-g[inds])/delta[inds] + 4)
|
||||
hue[np.isnan(hue)] = 0
|
||||
hue[hue < 0] = hue[hue < 0] + 360 # Make negative hue values positive behind 360
|
||||
|
||||
# Calc lightness / luminance
|
||||
luminance = (cmax + cmin) / 2.0
|
||||
|
||||
# Calc saturation
|
||||
saturation = np.zeros_like(r)
|
||||
inds = delta != 0
|
||||
saturation[inds] = delta[inds] / (1.0 - np.abs(2 * luminance[inds] - 1.0))
|
||||
|
||||
# Multiply luminance and saturation by 100 to scale them to the appropriate range (0-100)
|
||||
saturation *= 100.0
|
||||
luminance *= 100.0
|
||||
|
||||
|
||||
if img.shape[2] == 4: # Preserve alpha of original image
|
||||
return np.stack([hue, saturation, luminance, img[:,:,3]], -1)
|
||||
else:
|
||||
return np.stack([hue, saturation, luminance], -1)
|
||||
|
||||
def hsl_to_rgb(img):
|
||||
hue = img[:,:,0]
|
||||
saturation = img[:,:,1]
|
||||
luminance = img[:,:,2]
|
||||
saturation /= 100.0
|
||||
luminance /= 100.0
|
||||
c = (1.0 - np.abs(2.0 * luminance - 1.0)) * saturation
|
||||
x = c * (1.0 - np.abs((hue/60.0) % 2.0 - 1.0))
|
||||
m = luminance - c / 2.0
|
||||
r = np.zeros_like(hue)
|
||||
g = np.zeros_like(hue)
|
||||
b = np.zeros_like(hue)
|
||||
|
||||
inds1 = np.logical_and(0.0 <= hue, hue < 60.0)
|
||||
r[inds1] = c[inds1]
|
||||
g[inds1] = x[inds1]
|
||||
inds2 = np.logical_and(60.0 <= hue, hue < 120.0)
|
||||
r[inds2] = x[inds2]
|
||||
g[inds2] = c[inds2]
|
||||
inds3 = np.logical_and(120.0 <= hue, hue < 180.0)
|
||||
g[inds3] = c[inds3]
|
||||
b[inds3] = x[inds3]
|
||||
inds4 = np.logical_and(180.0 <= hue, hue < 240.0)
|
||||
g[inds4] = x[inds4]
|
||||
b[inds4] = c[inds4]
|
||||
inds5 = np.logical_and(240.0 <= hue, hue < 300.0)
|
||||
r[inds5] = x[inds5]
|
||||
b[inds5] = c[inds5]
|
||||
inds6 = np.logical_and(240.0 <= hue, hue < 300.0)
|
||||
r[inds6] = c[inds6]
|
||||
b[inds6] = x[inds6]
|
||||
|
||||
r += m
|
||||
g += m
|
||||
b += m
|
||||
|
||||
if img.shape[2] == 4: # Preserve alpha of original image
|
||||
return np.stack([r, g, b, img[:,:,3]], -1)
|
||||
else:
|
||||
return np.stack([r, g, b], -1)
|
||||
|
||||
|
||||
def generate_composite(work_queue, result_queue):
|
||||
image_names = ["094A", "131A", "171A", "195A", "284A", "304A"]
|
||||
cmaps =[palettable.cmocean.sequential.Ice_5.mpl_colormap,
|
||||
palettable.cmocean.sequential.Ice_20.mpl_colormap,
|
||||
palettable.cmocean.sequential.Turbid_5_r.mpl_colormap,
|
||||
palettable.cmocean.sequential.Turbid_20_r.mpl_colormap,
|
||||
plt.cm.get_cmap('gist_heat'),
|
||||
plt.cm.get_cmap('afmhot')]
|
||||
# Modify colormaps to start at perfect black (when they otherwise start at very dark colors)
|
||||
for cmi in range(0,4):
|
||||
for c in ['red','green','blue']:
|
||||
for i in range(3):
|
||||
cmaps[cmi]._segmentdata[c][0][i] = 0.0
|
||||
# These values are used to map floating point radiance values to colors using the above color maps.
|
||||
vmins = [0.050, 0.05, 00.100, 00.10, 00.100, 00.1]
|
||||
vmaxs = [8.000, 8.00, 20.000, 30.00, 40.000, 90.0]
|
||||
gammas = [0.375, 0.40, 00.425, 00.45, 00.475, 00.5]
|
||||
while True:
|
||||
try:
|
||||
job = work_queue.get()
|
||||
if job is None:
|
||||
result_queue.cancel_join_thread()
|
||||
return
|
||||
files_this_timestamp, timestamp, processed_images_dir = job
|
||||
filename = f"Composite-{int(timestamp)}.jpg"
|
||||
filepath = os.path.join(processed_images_dir, filename)
|
||||
if os.path.isfile(filepath):
|
||||
result_queue.put(("Exists", timestamp))
|
||||
continue
|
||||
|
||||
base_imgs = []
|
||||
for i in range(6):
|
||||
raw_data = np.flip(fits.getdata(files_this_timestamp[i]), 0)
|
||||
raw_data[raw_data < 0.0] = 0.0 # Remove non-zero data because it doesn't makes sense (supposed to be std Radiance)
|
||||
base_imgs.append(cmaps[i](np.clip((raw_data - vmins[i]) / vmaxs[i], 0, 1.0)**gammas[i]))
|
||||
# plt.figure(image_names[i])
|
||||
# plt.imshow(base_imgs[-1])
|
||||
|
||||
|
||||
# plt.figure("Initial Blend")
|
||||
composite_image_data = composite_alpha_over(base_imgs[4], base_imgs[5], 0.2)
|
||||
composite_image_data = composite_image_data**0.5
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
# plt.figure("Linear Burn with 304")
|
||||
composite_image_data = composite_alpha_over(linear_burn(base_imgs[5], composite_image_data), composite_image_data, 0.60)
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
# plt.figure("Light Ops with mid bands")
|
||||
mix_img = composite_alpha_over(exclusion(base_imgs[3], composite_image_data), composite_image_data, 0.95)
|
||||
mix_img = composite_alpha_over(linear_light(base_imgs[2], mix_img), mix_img, 1.0)
|
||||
mix_img = mix_img**0.75
|
||||
composite_image_data = composite_alpha_over(mix_img, composite_image_data, 0.5)
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
# plt.figure("Mix in mid 131")
|
||||
composite_image_data = composite_alpha_over(base_imgs[2], composite_image_data, 0.25) # Mix in a small amount of 131Å for the nice streamers
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
# plt.figure("Mid diff")
|
||||
mix_img = difference(base_imgs[3], base_imgs[2]) # 171Å - 131Å
|
||||
# plt.imshow(mix_img)
|
||||
|
||||
# plt.figure("HSL ops with mid")
|
||||
comp_hsl = rgb_to_hsl(composite_image_data)
|
||||
mix_img_hsl = rgb_to_hsl(mix_img)
|
||||
del mix_img
|
||||
base_img2_hsl = rgb_to_hsl(base_imgs[2])
|
||||
base_img3_hsl = rgb_to_hsl(base_imgs[3])
|
||||
comp_hsl = np.copy(comp_hsl)
|
||||
comp_hsl[:,:,0] += 0.025*mix_img_hsl[:,:,0] # Rotate hue based on mix_1_hsl
|
||||
del mix_img_hsl
|
||||
comp_hsl[:,:,0][comp_hsl[:,:,0] > 360.0] -= 360.0
|
||||
comp_hsl[:,:,1] -= 0.1*base_img3_hsl[:,:,1] # Reduce saturation based on base_img3
|
||||
comp_hsl[:,:,1] = np.clip(comp_hsl[:,:,1], 0.0, 100.0)
|
||||
comp_hsl[:,:,2] += 0.5*base_img2_hsl[:,:,2] # Boost luminance based on base_img2
|
||||
comp_hsl[:,:,1] = np.clip(comp_hsl[:,:,2], 0.0, 100.0)
|
||||
composite_image_data = hsl_to_rgb(comp_hsl)
|
||||
del comp_hsl
|
||||
composite_image_data = saturation(composite_image_data, 1.0, 0.8, 0.1) # Remove some blue and green
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
# plt.figure("Hue shifted 131")
|
||||
mix_img_hsl = rgb_to_hsl(base_imgs[1])
|
||||
mix_img_hsl[:,:,0] -= 50
|
||||
mix_img_hsl[:,:,0][mix_img_hsl[:,:,0] < 0.0] += 360.0
|
||||
mix_img = hsl_to_rgb(mix_img_hsl)
|
||||
del mix_img_hsl
|
||||
# plt.imshow(mix_img)
|
||||
|
||||
# plt.figure("Hard Light with hue shifted 131")
|
||||
composite_image_data = composite_alpha_over(hard_light(mix_img, composite_image_data), composite_image_data, 0.2)
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
# plt.figure("Adjusted 094")
|
||||
mix_img = saturation(base_imgs[0], 1.15, 1.2, 1.05)
|
||||
mix_img = contrast(mix_img, 1.5, 0.0)
|
||||
# plt.imshow(mix_img)
|
||||
|
||||
# plt.figure("Color Dodge with adjusted 094")
|
||||
composite_image_data = composite_alpha_over(color_dodge(mix_img, composite_image_data), composite_image_data, 0.5)
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
# plt.figure("Exclusion with adjusted 094")
|
||||
composite_image_data = composite_alpha_over(exclusion(mix_img, composite_image_data), composite_image_data, 0.65)
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
del mix_img
|
||||
|
||||
# plt.figure("Final Image")
|
||||
composite_image_data = saturation(composite_image_data, 1.0, 1.1, 1.2)
|
||||
composite_image_data = contrast(composite_image_data, 1.20, 0.00)
|
||||
# plt.imshow(composite_image_data)
|
||||
|
||||
# We have our final image
|
||||
# plt.show()
|
||||
|
||||
# Trim edges of final image so it fits nicely in 1920
|
||||
composite_image_data = composite_image_data[64:-64,64:-64,:3]
|
||||
|
||||
# Now shrink the component images and assemble them alongside the composite.
|
||||
new_dim = composite_image_data.shape[0] // 3
|
||||
# Enlarge the composite to fit the new images
|
||||
composite_image_data = np.pad(composite_image_data, ((0,0),(new_dim, new_dim),(0,0)))
|
||||
for i in range(6):
|
||||
img = base_imgs[i][64:-64,64:-64,:3] # Trim edges of image data to fit nicely
|
||||
img = bin_ndarray(img, (new_dim, new_dim, 3)) # Shrink down to 1/3 for assembly
|
||||
img = contrast(img, 1.25, 0.0)
|
||||
xdimoff = i%2 * (composite_image_data.shape[1] - new_dim)
|
||||
ydimoff = i//2*new_dim
|
||||
composite_image_data[ydimoff:ydimoff+new_dim, xdimoff:xdimoff+new_dim, :] = img
|
||||
|
||||
img = Image.fromarray((255 * composite_image_data).astype('uint8'))
|
||||
timestring = datetime.datetime.fromtimestamp(timestamp, tz = datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
|
||||
ImageDraw.Draw(img).text((655, 15), f"NOAA GOES Sattelite SUVI Composite - {timestring} UTC",(255,255,255), font_size = 24)
|
||||
for i in range(6): # Draw component angstrom labels
|
||||
if i%2 == 0:
|
||||
xdimtxtoff = 5
|
||||
else:
|
||||
xdimtxtoff = composite_image_data.shape[1] - 44
|
||||
ydimtxtoff = i//2*new_dim + new_dim / 2.0 - 8
|
||||
ImageDraw.Draw(img).text((xdimtxtoff, ydimtxtoff), image_names[i], font_size = 16)
|
||||
img.save(filepath, quality = 90)
|
||||
result_queue.put(("Created", timestamp))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
except Exception as e:
|
||||
result_queue.put((e, timestamp))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
stored_fits_dirs = [r"..\Data\goes16\l2\data", r"..\Data\goes18\l2\data"]
|
||||
processed_images_dirs = [r"..\composite\goes16", r"..\composite\goes18"]
|
||||
|
||||
regex_filename = r"dr_suvi-l2-ci\d{3}_g(16|18)_s\S*\.fits"
|
||||
nworkers = 14
|
||||
|
||||
# Testing
|
||||
# stored_fits_dirs = [r"C:\NOAA_SOLAR_IMAGES\fits_test_2024"]
|
||||
# processed_images_dirs = [r"C:\NOAA_SOLAR_IMAGES\composite"]
|
||||
|
||||
work_queue = Queue(maxsize = nworkers)
|
||||
result_queue = Queue()
|
||||
workers = []
|
||||
for i in range(nworkers):
|
||||
p = Process(target = generate_composite, args = (work_queue, result_queue), daemon=True)
|
||||
p.start()
|
||||
workers.append(p)
|
||||
|
||||
lowpriority()
|
||||
ncreated = 0
|
||||
nexists = 0
|
||||
nfailed = 0
|
||||
try:
|
||||
for stored_fits_dir, processed_images_dir in zip(stored_fits_dirs, processed_images_dirs):
|
||||
os.makedirs(processed_images_dir, exist_ok=True)
|
||||
filename_tester = re.compile(regex_filename)
|
||||
|
||||
files_sorted_by_timestamp = defaultdict(list)
|
||||
found_files = 0
|
||||
print(f"Searching for FITS files in: {stored_fits_dir}")
|
||||
for root, dirs, files in tqdm.tqdm(os.walk(stored_fits_dir), desc="Searching"):
|
||||
for f in files:
|
||||
if filename_tester.match(f):
|
||||
file_parts = f.split("_")
|
||||
measurement = file_parts[1]
|
||||
sattelite = file_parts[2]
|
||||
measure_end_time = datetime.datetime.strptime(file_parts[4][1:16], "%Y%m%dT%H%M%S")
|
||||
measure_end_time.replace(tzinfo=datetime.timezone.utc)
|
||||
measure_end_time = time.mktime(measure_end_time.timetuple())
|
||||
files_sorted_by_timestamp[measure_end_time].append(os.path.join(root,f))
|
||||
found_files += 1
|
||||
|
||||
print(f"Found {found_files} FITS files. Starting conversion.")
|
||||
|
||||
for timestamp in tqdm.tqdm(files_sorted_by_timestamp, desc="Creating Composite Solar Images"):
|
||||
# Collect completed jobs and record completion status
|
||||
while True:
|
||||
try:
|
||||
result = result_queue.get_nowait()
|
||||
if result[0] == "Exists":
|
||||
nexists += 1
|
||||
elif result[0] == "Created":
|
||||
ncreated += 1
|
||||
else:
|
||||
print(f"A worker encountered an exception on job {result[1]}: {result[0]}")
|
||||
nfailed += 1
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
# Submit new jobs
|
||||
files_this_timestamp = files_sorted_by_timestamp[timestamp]
|
||||
files_this_timestamp = sorted(files_this_timestamp)
|
||||
if not len(files_this_timestamp) == 6:
|
||||
print(f"Invalid or incomplete sensor records for: {timestamp}")
|
||||
continue
|
||||
|
||||
work_queue.put((files_this_timestamp, timestamp, processed_images_dir))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("Finishing current jobs and exiting")
|
||||
|
||||
for _ in range(nworkers):
|
||||
try:
|
||||
work_queue.put(None, timeout=10.0)
|
||||
except:
|
||||
break
|
||||
|
||||
for w in workers:
|
||||
w.join(10.0)
|
||||
|
||||
print(f"Created {ncreated} | Already had {nexists} | Failed {nfailed}")
|
||||
148
puller.py
Normal file
148
puller.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import os
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import datetime
|
||||
import json
|
||||
from threading import Thread
|
||||
import queue
|
||||
|
||||
import tqdm
|
||||
|
||||
def recursive_find_links(url):
|
||||
links_regex_pattern = r'(?<=<a href=")([^ ?]*)(?=">)' # Find href links that do not contain question marks or whitespace
|
||||
times_regex_pattern = r'(?<=<\/a>)(\s*\d{4}-\d{2}-\d{2} \d{2}:\d{2})(?= )' # Find timestamps in UTC in the format YYYY-MM-DD HH:mm
|
||||
with urllib.request.urlopen(url) as response:
|
||||
html_content = response.read().decode('utf-8')
|
||||
|
||||
links = re.findall(links_regex_pattern, html_content)
|
||||
times = re.findall(times_regex_pattern, html_content)
|
||||
for i in range(len(times)):
|
||||
dt = datetime.datetime.strptime(times[i].strip(), "%Y-%m-%d %H:%M")
|
||||
dt.replace(tzinfo=datetime.timezone.utc)
|
||||
times[i] = time.mktime(dt.timetuple())
|
||||
|
||||
if len(links) - 1 == len(times):
|
||||
links = links[1:]
|
||||
elif len(links) == len(times):
|
||||
pass
|
||||
else:
|
||||
raise(ValueError)
|
||||
|
||||
for _link, _time in zip(links, times):
|
||||
if _link.endswith("latest.png"):
|
||||
continue
|
||||
elif _link.endswith("/"):
|
||||
yield from recursive_find_links(urllib.parse.urljoin(url,_link))
|
||||
else:
|
||||
yield urllib.parse.urljoin(url,_link), _time
|
||||
|
||||
|
||||
# Fetch image from url and store it to path, retrying on failure
|
||||
def image_fetch_worker(work_queue, result_queue, attempt_count = 1):
|
||||
while True:
|
||||
job = work_queue.get()
|
||||
if job == None:
|
||||
return
|
||||
url, path, t = job
|
||||
|
||||
attempts = 0
|
||||
while attempts < attempt_count:
|
||||
try:
|
||||
req = urllib.request.Request(url, data=None)
|
||||
image_data = urllib.request.urlopen(req).read()
|
||||
os.makedirs(os.path.split(path)[0], exist_ok=True)
|
||||
open(path, 'wb').write(image_data)
|
||||
result_queue.put((True, url, t))
|
||||
break
|
||||
except Exception as e:
|
||||
if hasattr(e, "code") and e.code == 404: # This is expected if the file has been removed from the site (at least for swpc.noaa.gov)
|
||||
attempts += attempt_count
|
||||
elif attempts == 0:
|
||||
print(f"\nA problem occurred on image: {url} | {e}")
|
||||
time.sleep(1 + random.random())
|
||||
attempts += 1
|
||||
if (attempt_count > 1) and (attempts == attempt_count):
|
||||
print(f'\nAfter {attempt_count} retries, could not fetch: {url}')
|
||||
result_queue.put((False, url, t))
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
directory_url = r"https://services.swpc.noaa.gov/images/animations/suvi/"
|
||||
stored_images_dir = r"..\suvi"
|
||||
file_database_path = r"..\file_database.json"
|
||||
fetch_interval = 30*60
|
||||
nworkers = 8
|
||||
|
||||
file_info_cache = {}
|
||||
try:
|
||||
print(f"Attempting to load file records from cache: {file_database_path}")
|
||||
with open(file_database_path, 'r') as f:
|
||||
file_info_cache = json.loads(f.read())
|
||||
print(f"File records loaded from cache: {len(file_info_cache)} records found.")
|
||||
except Exception as e:
|
||||
print(f"Load failed, starting with empty cache")
|
||||
file_info_cache = {}
|
||||
|
||||
work_queue = queue.Queue(maxsize=nworkers)
|
||||
result_queue = queue.Queue()
|
||||
workers = []
|
||||
for _ in range(nworkers):
|
||||
t = Thread(target=image_fetch_worker, args=(work_queue, result_queue), daemon=True)
|
||||
t.start()
|
||||
workers.append(t)
|
||||
try:
|
||||
while True:
|
||||
fetched_image_count = 0
|
||||
already_had_image_count = 0
|
||||
failed_image_count = 0
|
||||
urllen = len(directory_url)
|
||||
for l, t in tqdm.tqdm(recursive_find_links(directory_url), desc="Downloading files"):
|
||||
# Collect complete work and record it
|
||||
while True:
|
||||
try:
|
||||
r_success, r_url, r_t = result_queue.get_nowait()
|
||||
if r_success:
|
||||
fetched_image_count += 1
|
||||
file_info_cache[r_url] = r_t
|
||||
else:
|
||||
failed_image_count += 1
|
||||
except queue.Empty:
|
||||
break
|
||||
# If we dont have the file or the file at the link is newer than the one we previously fetched
|
||||
if (not (l in file_info_cache)) or t > file_info_cache[l]:
|
||||
file_portion_of_link = l[urllen:]
|
||||
filepath = os.path.join(stored_images_dir, file_portion_of_link.replace("/", os.sep))
|
||||
if os.path.exists(filepath):
|
||||
if l in file_info_cache: # If we have record of this file, it must be out of date, rename it and download the new version.
|
||||
os.rename(filepath, filepath + f"_{int(file_info_cache[l])}")
|
||||
else: # If we have no record of this file, update the file info cache and don't redownload
|
||||
file_info_cache[l] = t
|
||||
already_had_image_count += 1
|
||||
continue
|
||||
work_queue.put((l, filepath, t))
|
||||
else:
|
||||
already_had_image_count += 1
|
||||
with open(file_database_path, 'w') as f:
|
||||
f.write(json.dumps(file_info_cache))
|
||||
print(f"Downloaded {fetched_image_count} | Already had {already_had_image_count} | Failed {failed_image_count}")
|
||||
time.sleep(fetch_interval)
|
||||
except KeyboardInterrupt:
|
||||
print("Saving file database and shutting down.")
|
||||
|
||||
for _ in range(nworkers):
|
||||
try:
|
||||
work_queue.put(None, timeout=1.0)
|
||||
except:
|
||||
break
|
||||
|
||||
for w in workers:
|
||||
w.join(5.0)
|
||||
|
||||
with open(file_database_path, 'w') as f:
|
||||
f.write(json.dumps(file_info_cache))
|
||||
|
||||
print(f"Downloaded {fetched_image_count} | Already had {already_had_image_count} | Failed {failed_image_count}")
|
||||
171
puller_fits.py
Normal file
171
puller_fits.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import os
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import datetime
|
||||
import json
|
||||
from threading import Thread
|
||||
import queue
|
||||
|
||||
import tqdm
|
||||
|
||||
directory_url = r"https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/"
|
||||
stored_images_dir = r"..\Data"
|
||||
# directory_url = r"https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes16/l2/data/suvi-l2-ci094/"
|
||||
# stored_images_dir = r"Z:\NOAA GOES Data\Data\goes16\l2\suvi-l2-ci094"
|
||||
ignore_folder_names = ["l1b", "goes17", "2017", "2018", "2019", "2020", "2021", "2022"]
|
||||
file_database_path = r"..\file_database.json"
|
||||
fetch_interval = 60*60
|
||||
nworkers = 16
|
||||
randomize_order = True
|
||||
|
||||
def recursive_find_links(url, attempt_count = 3, randomize_order = True):
|
||||
links_regex_pattern = r'(?<=<a href=")([^ ?:]*)(?=">.*\d{4}-\d{2}-\d{2} \d{2}:\d{2})' # Find href links that do not contain question marks or whitespace
|
||||
times_regex_pattern = r'(?<=<td align="right">)(\d{4}-\d{2}-\d{2} \d{2}:\d{2})(?= )' # Find timestamps in UTC in the format YYYY-MM-DD HH:mm
|
||||
with urllib.request.urlopen(url) as response:
|
||||
attempts = 0
|
||||
html_content = None
|
||||
while attempts < attempt_count:
|
||||
try:
|
||||
html_content = response.read().decode('utf-8')
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Exception while fetching links: {e}")
|
||||
time.sleep(1 + random.random())
|
||||
attempts += 1
|
||||
if (attempt_count > 1) and (attempts == attempt_count):
|
||||
print(f'\nAfter {attempt_count} retries, could not fetch: {url}')
|
||||
return
|
||||
|
||||
links = re.findall(links_regex_pattern, html_content)
|
||||
times = re.findall(times_regex_pattern, html_content)
|
||||
for i in range(len(times)):
|
||||
dt = datetime.datetime.strptime(times[i].strip(), "%Y-%m-%d %H:%M")
|
||||
dt.replace(tzinfo=datetime.timezone.utc)
|
||||
times[i] = time.mktime(dt.timetuple())
|
||||
|
||||
if len(links) - 1 == len(times):
|
||||
links = links[1:]
|
||||
elif len(links) == len(times):
|
||||
pass
|
||||
else:
|
||||
raise(ValueError)
|
||||
|
||||
results = list(zip(links, times))
|
||||
if randomize_order:
|
||||
random.shuffle(results)
|
||||
for _link, _time in results:
|
||||
if _link.endswith("/"):
|
||||
if _link.split(r"/")[-2] in ignore_folder_names:
|
||||
continue
|
||||
else:
|
||||
yield from recursive_find_links(urllib.parse.urljoin(url,_link), randomize_order)
|
||||
else:
|
||||
yield urllib.parse.urljoin(url,_link), _time
|
||||
|
||||
|
||||
# Fetch image from url and store it to path, retrying on failure
|
||||
def image_fetch_worker(work_queue, result_queue, attempt_count = 1):
|
||||
while True:
|
||||
job = work_queue.get()
|
||||
if job == None:
|
||||
return
|
||||
url, path, t = job
|
||||
|
||||
attempts = 0
|
||||
while attempts < attempt_count:
|
||||
try:
|
||||
req = urllib.request.Request(url, data=None)
|
||||
image_data = urllib.request.urlopen(req).read()
|
||||
os.makedirs(os.path.split(path)[0], exist_ok=True)
|
||||
open(path, 'wb').write(image_data)
|
||||
result_queue.put((True, url, t))
|
||||
break
|
||||
except Exception as e:
|
||||
if hasattr(e, "code") and e.code == 404: # This is expected if the file has been removed from the site (at least for swpc.noaa.gov)
|
||||
attempts += attempt_count
|
||||
elif attempts == 0:
|
||||
print(f"\nA problem occurred on image: {url} | {e}")
|
||||
time.sleep(1 + random.random())
|
||||
attempts += 1
|
||||
if (attempt_count > 1) and (attempts == attempt_count):
|
||||
print(f'\nAfter {attempt_count} retries, could not fetch: {url}')
|
||||
result_queue.put((False, url, t))
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
file_info_cache = {}
|
||||
try:
|
||||
print(f"Attempting to load file records from cache: {file_database_path}")
|
||||
with open(file_database_path, 'r') as f:
|
||||
file_info_cache = json.loads(f.read())
|
||||
print(f"File records loaded from cache: {len(file_info_cache)} records found.")
|
||||
except Exception as e:
|
||||
print(f"Load failed, starting with empty cache")
|
||||
file_info_cache = {}
|
||||
|
||||
work_queue = queue.Queue(maxsize=nworkers)
|
||||
result_queue = queue.Queue()
|
||||
workers = []
|
||||
for _ in range(nworkers):
|
||||
t = Thread(target=image_fetch_worker, args=(work_queue, result_queue), daemon=True)
|
||||
t.start()
|
||||
workers.append(t)
|
||||
try:
|
||||
while True:
|
||||
fetched_image_count = 0
|
||||
already_had_image_count = 0
|
||||
failed_image_count = 0
|
||||
urllen = len(directory_url)
|
||||
for l, t in tqdm.tqdm(recursive_find_links(directory_url, randomize_order), desc="Downloading files"):
|
||||
# Collect completed jobs and record completion status
|
||||
while True:
|
||||
try:
|
||||
r_success, r_url, r_t = result_queue.get_nowait()
|
||||
if r_success:
|
||||
fetched_image_count += 1
|
||||
file_info_cache[r_url] = r_t
|
||||
else:
|
||||
failed_image_count += 1
|
||||
except queue.Empty:
|
||||
break
|
||||
# If we dont have the file or the file at the link is newer than the one we previously fetched
|
||||
if (not (l in file_info_cache)) or t > file_info_cache[l]:
|
||||
file_portion_of_link = l[urllen:]
|
||||
filepath = os.path.join(stored_images_dir, file_portion_of_link.replace("/", os.sep))
|
||||
if os.path.exists(filepath):
|
||||
if l in file_info_cache: # If we have record of this file, it must be out of date, rename it and download the new version.
|
||||
new_name = filepath + f"_{int(file_info_cache[l])}"
|
||||
if os.path.exists(new_name):
|
||||
os.remove(new_name)
|
||||
os.rename(filepath, new_name)
|
||||
else: # If we have no record of this file, update the file info cache and don't redownload
|
||||
file_info_cache[l] = t
|
||||
already_had_image_count += 1
|
||||
continue
|
||||
work_queue.put((l, filepath, t))
|
||||
else:
|
||||
already_had_image_count += 1
|
||||
with open(file_database_path, 'w') as f:
|
||||
f.write(json.dumps(file_info_cache))
|
||||
print(f"Downloaded {fetched_image_count} | Already had {already_had_image_count} | Failed {failed_image_count}")
|
||||
time.sleep(fetch_interval)
|
||||
except KeyboardInterrupt:
|
||||
print("Saving file database and shutting down.")
|
||||
|
||||
for _ in range(nworkers):
|
||||
try:
|
||||
work_queue.put(None, timeout=5.0)
|
||||
except:
|
||||
break
|
||||
|
||||
for w in workers:
|
||||
w.join(5.0)
|
||||
|
||||
with open(file_database_path, 'w') as f:
|
||||
f.write(json.dumps(file_info_cache))
|
||||
|
||||
print(f"Downloaded {fetched_image_count} | Already had {already_had_image_count} | Failed {failed_image_count}")
|
||||
Loading…
Add table
Reference in a new issue