選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

rectify.py 6.8 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. #!/usr/bin/python3
  2. from multiprocessing import Pool, cpu_count
  3. import sys
  4. import re
  5. import numpy
  6. from math import atan,sin,cos,sqrt,tan,acos,ceil
  7. from PIL import Image
  8. EARTH_RADIUS = 6371.0
  9. SAT_HEIGHT = 830.0
  10. SAT_ORBIT_RADIUS = EARTH_RADIUS + SAT_HEIGHT
  11. SWATH_KM = 2800.0
  12. THETA_C = SWATH_KM / EARTH_RADIUS
  13. # Note: theta_s is the satellite viewing angle, theta_c is the angle between the projection of the satellite on the
  14. # Earth's surface and the point the satellite is looking at, measured at the center of the Earth
  15. # Compute the satellite angle of view given the center angle
  16. def theta_s(theta_c):
  17. return atan(EARTH_RADIUS * sin(theta_c)/(SAT_HEIGHT+EARTH_RADIUS*(1-cos(theta_c))))
  18. # Compute the inverse of the function above
  19. def theta_c(theta_s):
  20. delta_sqrt = sqrt(EARTH_RADIUS**2 + tan(theta_s)**2 *
  21. (EARTH_RADIUS**2-SAT_ORBIT_RADIUS**2))
  22. return acos((tan(theta_s)**2*SAT_ORBIT_RADIUS+delta_sqrt)/(EARTH_RADIUS*(tan(theta_s)**2+1)))
  23. # The nightmare fuel that is the correction factor function.
  24. # It is the reciprocal of d/d(theta_c) of theta_s(theta_c) a.k.a.
  25. # the derivative of the inverse of theta_s(theta_c)
  26. def correction_factor(theta_c):
  27. norm_factor = EARTH_RADIUS/SAT_HEIGHT
  28. tan_derivative_recip = (
  29. 1+(EARTH_RADIUS*sin(theta_c)/(SAT_HEIGHT+EARTH_RADIUS*(1-cos(theta_c))))**2)
  30. arg_derivative_recip = (SAT_HEIGHT+EARTH_RADIUS*(1-cos(theta_c)))**2/(EARTH_RADIUS*cos(
  31. theta_c)*(SAT_HEIGHT+EARTH_RADIUS*(1-cos(theta_c)))-EARTH_RADIUS**2*sin(theta_c)**2)
  32. return norm_factor * tan_derivative_recip * arg_derivative_recip
  33. # Radians position given the absolute x pixel position, assuming that the sensor samples the Earth
  34. # surface with a constant angular step
  35. def theta_center(img_size, x):
  36. ts = theta_s(THETA_C/2.0) * (abs(x-img_size/2.0) / (img_size/2.0))
  37. return theta_c(ts)
  38. # Worker thread
  39. def wthread(rectified_width, corr, endrow, startrow):
  40. # Make temporary working img to push pixels onto
  41. working_img = Image.new(img.mode, (rectified_width, img.size[1]))
  42. rectified_pixels = working_img.load()
  43. for row in range(startrow, endrow):
  44. # First pass: stretch from the center towards the right side of the image
  45. start_px = orig_pixels[img.size[0]/2, row]
  46. cur_col = int(rectified_width/2)
  47. target_col = cur_col
  48. for col in range(int(img.size[0]/2), img.size[0]):
  49. target_col += corr[col]
  50. end_px = orig_pixels[col, row]
  51. delta = int(target_col) - cur_col
  52. # Linearly interpolate
  53. for i in range(delta):
  54. # For night passes of Meteor the image is just gray level and
  55. # start_px and end_px being an int instead of a tuple
  56. if type(start_px) != int:
  57. interp_r = int((start_px[0]*(delta-i) + end_px[0]*i) / delta)
  58. interp_g = int((start_px[1]*(delta-i) + end_px[1]*i) / delta)
  59. interp_b = int((start_px[2]*(delta-i) + end_px[2]*i) / delta)
  60. rectified_pixels[cur_col,row] = (interp_r, interp_g, interp_b)
  61. else:
  62. interp = int((start_px*(delta-i) + end_px*i) / delta)
  63. rectified_pixels[cur_col,row] = interp
  64. cur_col += 1
  65. start_px = end_px
  66. # First pass: stretch from the center towards the left side of the image
  67. start_px = orig_pixels[img.size[0]/2, row]
  68. cur_col = int(rectified_width/2)
  69. target_col = cur_col
  70. for col in range(int(img.size[0]/2)-1, -1, -1):
  71. target_col -= corr[col]
  72. end_px = orig_pixels[col, row]
  73. delta = cur_col - int(target_col)
  74. # Linearly interpolate
  75. for i in range(delta):
  76. # For night passes of Meteor the image is just gray level and
  77. # start_px and end_px being an int instead of a tuple
  78. if type(start_px) != int:
  79. interp_r = int((start_px[0]*(delta-i) + end_px[0]*i) / delta)
  80. interp_g = int((start_px[1]*(delta-i) + end_px[1]*i) / delta)
  81. interp_b = int((start_px[2]*(delta-i) + end_px[2]*i) / delta)
  82. rectified_pixels[cur_col,row] = (interp_r, interp_g, interp_b)
  83. else:
  84. interp = int((start_px*(delta-i) + end_px*i) / delta)
  85. rectified_pixels[cur_col,row] = interp
  86. cur_col -= 1
  87. start_px = end_px
  88. # Crop the portion we worked on
  89. slice = working_img.crop(box=(0, startrow, rectified_width, endrow))
  90. # Convert to a numpy array so STUPID !#$&ING PICKLE WILL WORK
  91. out = numpy.array(slice)
  92. # Make dict of important values, return that.
  93. return {"offs": startrow, "offe": endrow, "pixels": out}
  94. if __name__ == "__main__":
  95. if len(sys.argv) < 2:
  96. print("Usage: {} <input file>".format(sys.argv[0]))
  97. sys.exit(1)
  98. out_fname = re.sub("\..*$", "-rectified", sys.argv[1])
  99. img = Image.open(sys.argv[1])
  100. print("Opened {}x{} image".format(img.size[0], img.size[1]))
  101. # Precompute the correction factors
  102. corr = []
  103. for i in range(img.size[0]):
  104. corr.append(correction_factor(theta_center(img.size[0], i)))
  105. # Estimate the width of the rectified image
  106. rectified_width = ceil(sum(corr))
  107. # Make new image
  108. rectified_img = Image.new(img.mode, (rectified_width, img.size[1]))
  109. # Get the pixel 2d arrays from the source image
  110. orig_pixels = img.load()
  111. # Callback function to modify the new image
  112. def modimage(data):
  113. if data:
  114. # Write slice to the new image in the right place
  115. rectified_img.paste(Image.fromarray(
  116. data["pixels"]), box=(0, data["offs"]))
  117. # Number of workers to be spawned - Probably best to not overdo this...
  118. numworkers = cpu_count()
  119. # Estimate the number of rows per worker
  120. wrows = ceil(img.size[1]/numworkers)
  121. # Initialize some starting data
  122. startrow = 0
  123. endrow = wrows
  124. # Make out process pool
  125. p = Pool(processes=numworkers)
  126. # Let's have a pool party! Only wnum workers are invited, though.
  127. for wnum in range(numworkers):
  128. # Make the workers with appropriate arguments, pass callback method to actually write data.
  129. p.apply_async(wthread, (rectified_width, corr,
  130. endrow, startrow), callback=modimage)
  131. # Aparrently ++ doesn't work?
  132. wnum = wnum+1
  133. # Beginning of next worker is the end of this one
  134. startrow = wrows*wnum
  135. # End of the worker is the specified number of rows past the beginning
  136. endrow = startrow + wrows
  137. # Show how many processes we're making!
  138. print("Spawning process ", wnum)
  139. # Pool's closed, boys
  140. p.close()
  141. # It's a dead pool now
  142. p.join()
  143. rectified_img.save(out_fname + ".png", "PNG")