├── .gitignore ├── LICENSE ├── README.md └── rectify.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/tags 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Davide Belloli 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteor-M2 image rectifier 2 | 3 | This is a free, open-source image processor that corrects the deformation 4 | visible on images from the Meteor-M2 weather satellite. Although specifically 5 | designed for this satellite, it can easily be adapted to correct for the Earth's 6 | curvature at any given height. 7 | 8 | ## Requirements 9 | 10 | This script depends on `pillow` and `numpy`. You can install them by issuing the following 11 | commands: 12 | 13 | ``` 14 | pip install pillow 15 | pip install numpy 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /rectify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from multiprocessing import Pool, cpu_count 4 | import sys 5 | import re 6 | import numpy 7 | from math import * 8 | from PIL import Image 9 | 10 | 11 | 12 | EARTH_RADIUS = 6371.0 13 | SAT_HEIGHT = 822.5 14 | SAT_ORBIT_RADIUS = EARTH_RADIUS + SAT_HEIGHT 15 | SWATH_KM = 2800.0 16 | THETA_C = SWATH_KM / EARTH_RADIUS 17 | 18 | # Note: theta_s is the satellite viewing angle, theta_c is the angle between the projection of the satellite on the 19 | # Earth's surface and the point the satellite is looking at, measured at the center of the Earth 20 | 21 | # Compute the satellite angle of view given the center angle 22 | def theta_s(theta_c): 23 | return atan(EARTH_RADIUS * sin(theta_c)/(SAT_HEIGHT+EARTH_RADIUS*(1-cos(theta_c)))) 24 | 25 | # Compute the inverse of the function above 26 | def theta_c(theta_s): 27 | delta_sqrt = sqrt(EARTH_RADIUS**2 + tan(theta_s)**2*(EARTH_RADIUS**2-SAT_ORBIT_RADIUS**2)) 28 | return acos((tan(theta_s)**2*SAT_ORBIT_RADIUS+delta_sqrt)/(EARTH_RADIUS*(tan(theta_s)**2+1))) 29 | 30 | # The nightmare fuel that is the correction factor function. 31 | # It is the reciprocal of d/d(theta_c) of theta_s(theta_c) a.k.a. 32 | # the derivative of the inverse of theta_s(theta_c) 33 | def correction_factor(theta_c): 34 | norm_factor = EARTH_RADIUS/SAT_HEIGHT 35 | tan_derivative_recip = (1+(EARTH_RADIUS*sin(theta_c)/(SAT_HEIGHT+EARTH_RADIUS*(1-cos(theta_c))))**2) 36 | arg_derivative_recip = (SAT_HEIGHT+EARTH_RADIUS*(1-cos(theta_c)))**2/(EARTH_RADIUS*cos(theta_c)*(SAT_HEIGHT+EARTH_RADIUS*(1-cos(theta_c)))-EARTH_RADIUS**2*sin(theta_c)**2) 37 | 38 | return norm_factor * tan_derivative_recip * arg_derivative_recip 39 | 40 | # Radians position given the absolute x pixel position, assuming that the sensor samples the Earth 41 | # surface with a constant angular step 42 | def theta_center(img_size, x): 43 | ts = theta_s(THETA_C/2.0) * (abs(x-img_size/2.0) / (img_size/2.0)) 44 | return theta_c(ts) 45 | 46 | # Worker thread 47 | def wthread(rectified_width, corr, endrow, startrow): 48 | # Make temporary working img to push pixels onto 49 | working_img = Image.new(img.mode, (rectified_width, img.size[1])) 50 | rectified_pixels = working_img.load() 51 | 52 | for row in range(startrow, endrow): 53 | # First pass: stretch from the center towards the right side of the image 54 | start_px = orig_pixels[img.size[0]/2, row] 55 | cur_col = int(rectified_width/2) 56 | target_col = cur_col 57 | 58 | for col in range(int(img.size[0]/2), img.size[0]): 59 | target_col += corr[col] 60 | end_px = orig_pixels[col, row] 61 | delta = int(target_col) - cur_col 62 | 63 | # Linearly interpolate 64 | for i in range(delta): 65 | interp_r = int((start_px[0]*(delta-i) + end_px[0]*i) / delta) 66 | interp_g = int((start_px[1]*(delta-i) + end_px[1]*i) / delta) 67 | interp_b = int((start_px[2]*(delta-i) + end_px[2]*i) / delta) 68 | 69 | rectified_pixels[cur_col, row] = (interp_r, interp_g, interp_b) 70 | cur_col += 1 71 | 72 | start_px = end_px 73 | 74 | # First pass: stretch from the center towards the left side of the image 75 | start_px = orig_pixels[img.size[0]/2, row] 76 | cur_col = int(rectified_width/2) 77 | target_col = cur_col 78 | 79 | for col in range(int(img.size[0]/2)-1, -1, -1): 80 | target_col -= corr[col] 81 | end_px = orig_pixels[col, row] 82 | delta = cur_col - int(target_col) 83 | 84 | # Linearly interpolate 85 | for i in range(delta): 86 | interp_r = int((start_px[0]*(delta-i) + end_px[0]*i) / delta) 87 | interp_g = int((start_px[1]*(delta-i) + end_px[1]*i) / delta) 88 | interp_b = int((start_px[2]*(delta-i) + end_px[2]*i) / delta) 89 | 90 | rectified_pixels[cur_col, row] = (interp_r, interp_g, interp_b) 91 | cur_col -= 1 92 | 93 | start_px = end_px 94 | 95 | # Crop the portion we worked on 96 | slice = working_img.crop(box=(0, startrow, rectified_width, endrow)) 97 | # Convert to a numpy array so STUPID !#$&ING PICKLE WILL WORK 98 | out = numpy.array(slice) 99 | # Make dict of important values, return that. 100 | return { "offs" : startrow, "offe" : endrow, "pixels" : out } 101 | 102 | if __name__ == "__main__": 103 | if len(sys.argv) < 2: 104 | print("Usage: {} ".format(sys.argv[0])) 105 | sys.exit(1) 106 | 107 | out_fname = re.sub("\..*$", "-rectified", sys.argv[1]) 108 | 109 | img = Image.open(sys.argv[1]) 110 | print("Opened {}x{} image".format(img.size[0], img.size[1])) 111 | 112 | # Precompute the correction factors 113 | corr = [] 114 | for i in range(img.size[0]): 115 | corr.append(correction_factor(theta_center(img.size[0], i))) 116 | 117 | # Estimate the width of the rectified image 118 | rectified_width = ceil(sum(corr)) 119 | 120 | # Make new image 121 | rectified_img = Image.new(img.mode, (rectified_width, img.size[1])) 122 | 123 | # Get the pixel 2d arrays from the source image 124 | orig_pixels = img.load() 125 | 126 | # Callback function to modify the new image 127 | def modimage(data): 128 | if data: 129 | # Write slice to the new image in the right place 130 | rectified_img.paste(Image.fromarray(data["pixels"]), box=(0, data["offs"])) 131 | 132 | # Number of workers to be spawned - Probably best to not overdo this... 133 | numworkers = cpu_count() 134 | # Estimate the number of rows per worker 135 | wrows = ceil(img.size[1]/numworkers) 136 | # Initialize some starting data 137 | startrow = 0 138 | endrow = wrows 139 | # Make out process pool 140 | p = Pool(processes=numworkers) 141 | 142 | #Let's have a pool party! Only wnum workers are invited, though. 143 | for wnum in range(numworkers): 144 | # Make the workers with appropriate arguments, pass callback method to actually write data. 145 | p.apply_async(wthread, (rectified_width, corr, endrow, startrow), callback=modimage) 146 | # Aparrently ++ doesn't work? 147 | wnum = wnum+1 148 | # Beginning of next worker is the end of this one 149 | startrow = wrows*wnum 150 | # End of the worker is the specified number of rows past the beginning 151 | endrow = startrow + wrows 152 | # Show how many processes we're making! 153 | print("Spawning process ", wnum) 154 | # Pool's closed, boys 155 | p.close() 156 | # It's a dead pool now 157 | p.join() 158 | 159 | 160 | print("Writing rectified image to {}".format(out_fname + ".png")) 161 | rectified_img.save(out_fname + ".png", "PNG") 162 | --------------------------------------------------------------------------------