CPP_Birdtracker
A C++ object tracking program specifically for lunar bird tracking
fog_removal.py
Go to the documentation of this file.
1 # CPP_Birdtracker/fog_removal.py - Script to determine appropriate BLACKOUT_THRESH
2 # Copyright (C) <2020> <Wesley T. Honeycutt>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 
17 
18 
36 
37 # Default imports
38 import os
39 import subprocess
40 
41 # User imports
42 import numpy as np
43 import cv2
44 
45 """!
46 This is a user configurable option to match the color schema of your terminal for using the ascii
47 imaging options. If your terminal is dark text on a light background, set to False. If your
48 terminal is light text on a dark background, set to True. You can safely ignore this if you
49 are using the GUI version.
50 """
51 BLACK_BACK = True
52 
53 
54 TOGGLE_PRESSED = False
55 TOGGLE_ASC = False
56 
57 def nothing(args):
58  """
59  nothing
60  """
61  pass
62 
63 
65  """!
66  Create intro/directions slide for the thresholding Python script.
67 
68  @return image A 1080x1080 black 3 color image with red text (CV Mat)
69  """
70  image = np.zeros((1000, 1000, 3))
71  font = cv2.FONT_HERSHEY_SIMPLEX
72  text_color = (0, 0, 255)
73  text_lines = ["Use this program to determine the optimal threshold", \
74  "value for the input vid. Adjust the \"Threshold\"", \
75  "slider until the fog around the moon disappears and the", \
76  "edge of the moon is crisp, without losing parts of the", \
77  "edge. When you are happy with the results, use the value", \
78  "on the slider in this windowas the value for ", \
79  "BLACKOUT_THRESH in settings.cfg.", \
80  "", "", "", "", "", "", \
81  "To exit this script, press ESC", \
82  "", "", "", \
83  "To begin testing the image, or return to these directions,", \
84  "use the \"Directions\" slider"
85  ]
86  for i, j in enumerate(text_lines):
87  image = cv2.putText(image, j, (50, 50*(i+1)), font, 1, text_color, 2, cv2.LINE_AA)
88  return image
89 
90 def ascii_version(in_frame):
91  """!
92  This is a terminal-only version of the fog removal code. In this loop, the user is prompted
93  for a threshold value to test or to exit by entering "q". The in_frame param passes through to
94  the nested function.
95 
96  @param in_frame The input 3-channel OpenCV image
97  """
98  print("=======================================================================")
99  print("Use this program to determine the optimal threshold value for the input vid. ASCII mode is enabled for use in terminals without GUI access. Try to make any fog around the moon disappear to make the moon appear with a crisp edge. Don't overdo it and lose some of the edge though! The original image uses threshold 0, the max value of 255 will make all but the brightest spots disappear. When you are happy with the results, use the value you entered as the value for BLACKOUT_THRESH in settings.cfg.\n")
100  print("=======================================================================")
101  local_exit_toggle = True
102  in_frame = cv2.cvtColor(in_frame, cv2.COLOR_BGR2GRAY)
103  while (local_exit_toggle):
104  print("Enter a value between (0-255) or \"q\" to exit")
105  local_string = input("Entry:")
106  if (local_string == 'q') or (local_string == 'Q'):
107  local_exit_toggle = False
108  elif local_string.isdigit():
109  if int(local_string) < 256:
110  ascii_proc(in_frame, int(local_string))
111  else:
112  print("You entered an invalid integer value, please choose between 0-255")
113  else:
114  print("Unrecognized characters in entry. Try again")
115  return
116 
117 def ascii_proc(in_frame, thresh):
118  """!
119  This function does the heavy lifting for the ASCII version of this code. The input image
120  is cropped to ignore as much black background as possible, then resized to fit the user's
121  terminal (using output from `tput`). The image is thresholded, and the output from this
122  thresholding operaiton is converted to a string of ASCII characters. There are 97 characters
123  in the grayscale list. If the user has a black background, as determined by the global
124  variable BLACK_BACK, the order of ASCII characters are reversed. After printing our "image",
125  the user is informed if the image was resized and reminded of what value they entered.
126 
127  @param in_frame The input 1-channel OpenCV image
128  @param thresh An integer between 0-255 to represent the threshold of to test here
129  """
130  import imutils
131 
132  # Stores if the image was resized
133  rezzed = False
134 
135  # Empty output "image"
136  picstring = ""
137 
138  # Fetch the width of the user's terminal as an int
139  local_width = int(subprocess.check_output(["tput", "cols"]).decode("ascii").strip('\n'))
140 
141  # A list of characters in order of descending darkness for use in ASCII images. 97 items.
142  ascii_list = ["@", "M", "B", "H", "E", "N", "R", "#", "K", "W", "X", "D", "F", "P", "Q", "A", "S", "U", "Z", "b", "d", "e", "h", "x", "*", "8", "G", "m", "&", "0", "4", "L", "O", "V", "Y", "k", "p", "q", "5", "T", "a", "g", "n", "s", "6", "9", "o", "w", "z", "$", "C", "I", "u", "2", "3", "J", "c", "f", "r", "y", "%", "1", "v", "7", "l", "+", "i", "t", "[", "]", "", "{", "}", "?", "j", "|", "(", ")", "=", "~", "!", "-", "/", "<", ">", "\\", "\"", "^", "_", "'", ";", ",", ":", "`", ".", " ", " "]
143 
144  # For dark background terminals, use the reverse of the list
145  if BLACK_BACK:
146  ascii_list.reverse()
147 
148  # Divisor ratio to give weight to each ASCII value
149  ascii_ratio = 256/len(ascii_list)
150 
151  # Fetch largest contour and crop to that size
152  contours, hierarchy = cv2.findContours(in_frame, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
153  # Holds largest bounding box
154  box = (0,0,0,0) # biggest bounding box so far
155  # Holds largest bounding box area
156  box_area = 0
157  for cont in contours:
158  # Retrieves the boudning box properties x, y, width, and height
159  xxx, yyy, www, hhh = cv2.boundingRect(cont)
160  area = www * hhh
161  # Check if this is a bigger one
162  if area > box_area:
163  box = xxx, yyy, www, hhh
164  box_area = area
165  # Once we found the biggest contour, we re-grab the boundaries
166  xxx, yyy, www, hhh = box
167  # And do an OpenCV crop
168  roi=in_frame[yyy:yyy+hhh, xxx:xxx+www]
169 
170  # Resize the cropped image if the terminal is too small.
171  # We only worry about width since modern terminals have scrolling height.
172  if (roi.shape[0] > local_width):
173  roi = imutils.resize(roi, width=local_width)
174  rezzed = True
175  oldcols = roi.shape[0]
176 
177  # perform thresh
178  _, roi = cv2.threshold(roi, thresh, 255, cv2.THRESH_TOZERO)
179 
180  rows, cols = roi.shape
181  print("generating image...")
182  for i in range(0, rows):
183  for j in range(0, cols):
184  # Convert pixel value to scaled ASCII code.
185  picstring += ascii_list[int(roi[i, j]/ascii_ratio)]
186  picstring += '\n'
187  print(picstring)
188  if rezzed:
189  print("Your terminal is", local_width, " pixels wide, but the cropped raw image was", oldcols, "pixels wide. Your output has been resized.")
190  print("The above image threshold value was", thresh)
191  return
192 
193 def is_valid_file(parser, arg):
194  """!
195  Check if arg is a valid file that already exists on the file system.
196 
197  @param parser the argparse object
198  @param arg the string of the filename we want to test
199 
200  @return arg
201  """
202  arg = os.path.abspath(arg)
203  if not os.path.exists(arg):
204  parser.error("The file %s does not exist!" % arg)
205  raise FileNotFoundError("The file you told the script to run on does not exist")
206  else:
207  return arg
208 
209 def check_positive(value):
210  """!
211  Check that the input integer for the starting frame number is valid
212  """
213  ivalue = int(value)
214  if ivalue <= 0:
215  raise RuntimeError("%s is an invalid positive int value" % value)
216  return ivalue
217 
218 
220  """!
221  Get parser object for this script
222 
223  @return Parser object
224  """
225  from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
226  parser = ArgumentParser(description=__doc__, formatter_class=ArgumentDefaultsHelpFormatter)
227  parser.add_argument("-f", "--file", dest="filename", required=True,\
228  type=lambda x: is_valid_file(parser, x), help="video input FILE", metavar="FILE")
229  parser.add_argument("-n", "--nth_frame", dest="max_frame", required=False,\
230  type=check_positive, help="Use the nth frame of the video to test fog", \
231  nargs=None, default=100, metavar='N')
232  parser.add_argument("-a", "--ascii", dest="ascii_toggle", required=False, action="store_true",\
233  help="Toggle for an ascii (terminal) only version of this script", default=False)
234  return parser
235 
236 def main():
237  """!
238  Perform primary functions of this script.
239  Command line arguments are first parsed to get the file name on which we want to operate. Then,
240  we ensure that this is a valid file. The video file is opened and the first frame is read.
241  We then create our output window of size 1080x1080 and trackbars for the Directions toggle and
242  Image Threshold. If everything was successful, the window opens with the Diretions image. When
243  the user tells the Directions to toggle closed, the first frame from the video is shown after
244  conversion to grayscale and thresholded to zero. The image threshold is determined by the value
245  of the threshold trackbar. The user exits with the ESC key. On exit, the final value of the
246  threshold is printed to the terminal.
247  """
248  # Prepare input file based on the terminal command args
249  args = get_parser().parse_args()
250  print("Operating on file:", args.filename)
251  # Get the first frame of the video from the terminal command
252  cap = cv2.VideoCapture(args.filename)
253  ret, frame = cap.read()
254  cnt = 1
255  cnt_max = args.max_frame
256  TOGGLE_ASC = args.ascii_toggle
257  while cnt < cnt_max:
258  ret, frame = cap.read()
259  cnt = cnt + 1
260  if TOGGLE_ASC:
261  ascii_version(frame)
262  else:
263  # Create our window and relevant trackbars
264  cv2.namedWindow("image", cv2.WINDOW_NORMAL)
265  cv2.resizeWindow("image", 1080, 1080)
266  cv2.createTrackbar("Directions", "image", 0, 1, nothing)
267  cv2.createTrackbar("Threshold", "image", 0, 255, nothing)
268  # Misc initial values
269  outimg = create_directions()
270  # If we got a video frame, execute the main loop.
271  if ret:
272  # Misc initial values
273  pretest = 0
274  thresh = 0
275  # Convert input image to gray
276  frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
277  while(1):
278  # Get the value of your directions toggle
279  local_button = cv2.getTrackbarPos("Directions", "image")
280  if (local_button == 0) and (pretest == 1):
281  outimg = create_directions()
282  # Set the value for the directions toggle
283  if local_button > 0:
284  TOGGLE_PRESSED = True
285  else:
286  TOGGLE_PRESSED = False
287  # Process the image with the threshold value
288  if (TOGGLE_PRESSED):
289  thresh = cv2.getTrackbarPos("Threshold", "image")
290  _, outimg = cv2.threshold(frame, thresh, 255, cv2.THRESH_TOZERO)
291  # Show the image, either directions or the thresholded image
292  cv2.imshow("image", outimg)
293  # ESC to kill this script
294  kill = cv2.waitKey(1) & 0xFF
295  if kill == 27:
296  print("Your final threshold value was:", thresh)
297  break
298  # Set the pretest value for the next loop
299  pretest = local_button
300 
301  else:
302  # Error loading video
303  raise RuntimeError("ERROR: could not get frame from video")
304  cv2.destroyAllWindows()
305  return
306 
307 if __name__ == "__main__":
308  main()
def check_positive(value)
Check that the input integer for the starting frame number is valid.
Definition: fog_removal.py:209
def ascii_proc(in_frame, thresh)
This function does the heavy lifting for the ASCII version of this code.
Definition: fog_removal.py:117
def ascii_version(in_frame)
This is a terminal-only version of the fog removal code.
Definition: fog_removal.py:90
def main()
Perform primary functions of this script.
Definition: fog_removal.py:236
def get_parser()
Get parser object for this script.
Definition: fog_removal.py:219
def is_valid_file(parser, arg)
Check if arg is a valid file that already exists on the file system.
Definition: fog_removal.py:193
def create_directions()
Create intro/directions slide for the thresholding Python script.
Definition: fog_removal.py:64