Advertisement
Guest User

Untitled

a guest
Feb 11th, 2016
50
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.12 KB | None | 0 0
  1. #!/usr/bin/env python
  2. """Watches the RC car for movement."""
  3. from collections import deque
  4. import argparse
  5. import datetime
  6. import math
  7. import os
  8. import socket
  9. import subprocess
  10. import sys
  11. import threading
  12. import time
  13.  
  14. from common import format_command
  15. from common import server_up
  16.  
  17. # pylint: disable=superfluous-parens
  18. # pylint: disable=global-statement
  19.  
  20.  
  21. CHANNELS_49_MHZ = (49.830, 49.845, 49.860, 49.875, 49.890)
  22. CHANNELS_27_MHZ = (26.995, 27.045, 27.095, 27.145, 27.195, 27.255)
  23.  
  24.  
  25. def normalize(img, bit_depth=None):
  26. """Linear normalization and conversion to grayscale of an image."""
  27. from PIL import ImageOps
  28. img = ImageOps.grayscale(img)
  29. img = ImageOps.autocontrast(img)
  30. if bit_depth is not None:
  31. img = ImageOps.posterize(img, bit_depth)
  32. return img
  33.  
  34.  
  35. def mean(values):
  36. """Calculate mean of the values."""
  37. return sum(values) / len(values)
  38.  
  39.  
  40. def standard_deviation(values, mean_=None):
  41. """Calculate standard deviation."""
  42. if mean_ is None:
  43. mean_ = mean(values)
  44. size = len(values)
  45. sum_ = 0.0
  46. for value in values:
  47. sum_ += math.sqrt((value - mean_) ** 2)
  48. return math.sqrt((1.0 / (size - 1)) * (sum_ / size))
  49.  
  50.  
  51. class PictureWrapper(threading.Thread):
  52. """Threaded wrapper to access images. Running image capture commands
  53. can take multiple seconds, so rather than try a command code and wait
  54. for an image, this will run in another thread and capture images
  55. continually. This might not give us the exact command code that
  56. produced a result, but that shoud be fine.
  57. """
  58. def __init__(self, command_tuple):
  59. super(PictureWrapper, self).__init__()
  60. self._command_tuple = command_tuple
  61. self._image = None
  62. self._run = True
  63. self._lock = threading.Lock()
  64.  
  65. def run(self):
  66. """Runs in a thread, continually captures images."""
  67. while self._run:
  68. start = time.time()
  69. new_image = self._get_picture_from_command()
  70. with self._lock:
  71. self._image = new_image
  72. # Limit the capture to once per second, in case it's really fast
  73. while time.time() < start + 1.0:
  74. time.sleep(0.1)
  75.  
  76. def stop(self):
  77. """Stop capturing images."""
  78. self._run = False
  79.  
  80. def get_picture(self):
  81. """Returns the most recent picture."""
  82. while self._image is None:
  83. time.sleep(0.1)
  84. with self._lock:
  85. return self._image.copy()
  86.  
  87. def _get_picture_from_command(self):
  88. """Saves a picture from stdout generated by running the command."""
  89. from PIL import Image
  90. import StringIO
  91.  
  92. # Use pipes to avoid writing to the disk
  93. pipe = subprocess.Popen(self._command_tuple, stdout=subprocess.PIPE)
  94.  
  95. file_buffer = StringIO.StringIO(pipe.stdout.read())
  96. pipe.stdout.close()
  97. image = Image.open(file_buffer)
  98. return image
  99.  
  100.  
  101. def percent_difference(image_1, image_2):
  102. """Returns the percent difference between two images."""
  103. assert image_1.mode == image_2.mode, 'Different kinds of images.'
  104. assert image_1.size == image_2.size, 'Different sizes.'
  105.  
  106. # TODO: Stop doing this in Python. It's incredibly slow, and on the
  107. # Rapberry Pi, I had to reduce the image size a lot to get it to process
  108. # with under a 1 second interval per command code. Maybe see
  109. # http://help.simplecv.org/question/2192/absolute-difference-between-images/
  110. pairs = zip(image_1.getdata(), image_2.getdata())
  111. if len(image_1.getbands()) == 1:
  112. # for gray-scale jpegs
  113. diff = sum(abs(p1 - p2) for p1, p2 in pairs)
  114. else:
  115. diff = sum(abs(c1 - c2) for p1, p2 in pairs for c1, c2 in zip(p1, p2))
  116.  
  117. ncomponents = image_1.size[0] * image_1.size[1] * 3
  118. return (diff / 255.0 * 100.0) / ncomponents
  119.  
  120.  
  121. def command_iterator(frequency):
  122. """Iterates through the frequencies and commands."""
  123. for useconds in range(100, 1201, 100):
  124. for sync_multiplier in range(2, 7):
  125. for sync_repeats in range(2, 7):
  126. for signal_repeats in range(5, 50):
  127. yield (
  128. frequency,
  129. useconds,
  130. sync_multiplier,
  131. sync_repeats,
  132. signal_repeats,
  133. )
  134.  
  135.  
  136. def search_for_command_codes(
  137. host,
  138. port,
  139. frequencies,
  140. get_picture_function=None,
  141. bit_depth=None
  142. ):
  143. """Iterates through commands and looks for changes in the webcam."""
  144. if get_picture_function is not None:
  145. diffs = deque()
  146. pictures = deque()
  147.  
  148. base = normalize(get_picture_function(), bit_depth=bit_depth)
  149. try:
  150. base.save('normalized-test.png')
  151. except Exception:
  152. pass
  153. time.sleep(1)
  154. print('Filling base photos for difference analysis')
  155. for _ in range(20):
  156. recent = normalize(get_picture_function(), bit_depth=bit_depth)
  157. diff = percent_difference(base, recent)
  158. time.sleep(1)
  159. diffs.append(diff)
  160. pictures.append(recent)
  161.  
  162. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  163.  
  164. print(
  165. 'Searching for command codes on {frequencies}'.format(
  166. frequencies=', '.join((str(f) for f in frequencies))
  167. )
  168. )
  169. command_tuple_description = ('freq', 'useconds', 'multiplier', 'sync_repeats', 'signal_repeats')
  170.  
  171. start = time.time()
  172. previous_image = None
  173. if get_picture_function is not None:
  174. previous_image = get_picture_function()
  175.  
  176. for frequency in frequencies:
  177. for command_tuple in command_iterator(frequency):
  178. # pylint: disable=star-args
  179. command = format_command(*command_tuple)
  180. if sys.version_info.major == 3:
  181. command = bytes(command, 'utf-8')
  182. sock.sendto(command, (host, port))
  183.  
  184. if get_picture_function is not None:
  185. # Normalizing and processing the image takes a long time, so
  186. # we'll do the normalization while we're waiting for the
  187. # command to broadcast for a full second
  188. recent = normalize(previous_image, bit_depth=bit_depth)
  189. # Let's compare the most recent photo to the oldest one,
  190. # in case a cloud passes over and the brightness changes
  191. diff = percent_difference(pictures[0], recent)
  192. std_dev = standard_deviation(diffs)
  193. mean_ = mean(diffs)
  194.  
  195. while time.time() < start + 1.0:
  196. time.sleep(0.1)
  197. start = time.time()
  198.  
  199. if get_picture_function is None:
  200. print(
  201. ' '.join((
  202. (desc + ':' + str(value)) for desc, value in zip(
  203. command_tuple_description,
  204. command_tuple
  205. )
  206. ))
  207. )
  208. else:
  209. previous_image = get_picture_function()
  210. # I should be doing a z-test or something here... eh
  211. if abs(diff - mean_) > (std_dev * 3.0) and diff > 2.0:
  212. print('Found substantially different photo, saving...')
  213. print(
  214. 'diff={diff}, mean={mean}, std dev={std_dev}'
  215. ' at {time}'.format(
  216. diff=diff,
  217. mean=mean_,
  218. std_dev=std_dev,
  219. time=str(datetime.datetime.now())
  220. )
  221. )
  222. file_name = '-'.join(
  223. (desc + '-' + str(value)) for desc, value in zip(
  224. command_tuple_description,
  225. command_tuple
  226. )
  227. )
  228. try:
  229. image = get_picture_function()
  230. image.save(file_name + '.png')
  231. except Exception as exc:
  232. print(file_name)
  233. print('Unable to save photo: ' + str(exc))
  234. time.sleep(2)
  235.  
  236. diffs.popleft()
  237. diffs.append(diff)
  238. pictures.popleft()
  239. pictures.append(recent)
  240.  
  241.  
  242. def make_parser():
  243. """Builds and returns an argument parser."""
  244. parser = argparse.ArgumentParser(
  245. description='Iterates through and broadcasts command codes and'
  246. ' monitors the webcam to watch for movement from the RC car.'
  247. )
  248.  
  249. parser.add_argument(
  250. '-p',
  251. '--port',
  252. dest='port',
  253. help='The port to send control commands to.',
  254. default=12345,
  255. type=int
  256. )
  257. parser.add_argument(
  258. '-s',
  259. '--server',
  260. dest='server',
  261. help='The server to send control commands to.',
  262. default='127.1'
  263. )
  264.  
  265. parser.add_argument(
  266. '-f',
  267. '--frequency',
  268. dest='frequency',
  269. help='The frequency to broadcast commands on.',
  270. default=49,
  271. type=float
  272. )
  273.  
  274. def bit_depth_checker(bit_depth):
  275. """Checks that the bit depth argument is valid."""
  276. try:
  277. bit_depth = int(bit_depth)
  278. except:
  279. raise argparse.ArgumentTypeError('Bit depth must be an int')
  280.  
  281. if not 1 <= bit_depth <= 8:
  282. raise argparse.ArgumentTypeError(
  283. 'Bit depth must be between 1 and 8 inclusive'
  284. )
  285.  
  286. return bit_depth
  287.  
  288. parser.add_argument(
  289. '-b',
  290. '--bit-depth',
  291. dest='bit_depth',
  292. help='The bit depth to reduce images to.',
  293. type=bit_depth_checker,
  294. default=1
  295. )
  296.  
  297. parser.add_argument(
  298. '--no-camera',
  299. dest='no_camera',
  300. help='Disable the camera and image recognition. You will need to watch'
  301. ' the RC car manually.',
  302. action='store_true',
  303. default=False
  304. )
  305.  
  306. parser.add_argument(
  307. '--raspberry-pi-camera',
  308. dest='raspi_camera',
  309. help='Force the use of the Raspberry Pi camera.',
  310. action='store_true',
  311. default=False
  312. )
  313. parser.add_argument(
  314. '--webcam',
  315. dest='webcam',
  316. help='Force the use of a webcam.',
  317. action='store_true',
  318. default=False
  319. )
  320.  
  321. return parser
  322.  
  323.  
  324. def main():
  325. """Parses command line arguments and runs the interactive controller."""
  326. parser = make_parser()
  327. args = parser.parse_args()
  328.  
  329. # Remove the default image to make sure that we're not processing images
  330. # from a previous run
  331. try:
  332. os.remove('photo.png')
  333. except OSError:
  334. pass
  335.  
  336. if not server_up(args.server, args.port, args.frequency):
  337. print(
  338. '''Server does not appear to be listening for messages, aborting.
  339. Did you run pi_pcm?'''
  340. )
  341. return
  342.  
  343. webcam = args.webcam
  344. raspi_camera = args.raspi_camera
  345. if not args.no_camera:
  346. try:
  347. import PIL as _
  348. except ImportError:
  349. sys.stderr.write(
  350. '''Using the camera to detect movement requires the Python PIL libraries. You
  351. can install them by running:
  352. apt-get install python-imaging
  353. Or, you can use the `--no-camera` option and just watch the RC car for
  354. movement. When it does, hit <Ctrl> + C and use the last printed values.
  355. '''
  356. )
  357. sys.exit(1)
  358.  
  359. if webcam and raspi_camera:
  360. sys.stderr.write(
  361. 'You can only specify one of --webcam and --rasberry-pi-camera.'
  362. )
  363. sys.exit(1)
  364. if not webcam and not raspi_camera:
  365. # Find which to use
  366. raspi_exists = subprocess.call(
  367. ('/usr/bin/which', 'raspistill'),
  368. stdout=open('/dev/null', 'w')
  369. )
  370. if raspi_exists == 0:
  371. raspi_camera = True
  372. else:
  373. raspi_camera = False
  374. webcam = not raspi_camera
  375.  
  376. # RC cars in the 27 and 49 MHz spectrum typically operate on one of a
  377. # several channels in that frequency, but most toy RC cars that I've
  378. # seen only list the major frequency on the car itself. If someone
  379. # enters a major frequency, search each channel.
  380. if args.frequency == 49:
  381. frequencies = CHANNELS_49_MHZ
  382. elif args.frequency == 27:
  383. frequencies = CHANNELS_27_MHZ
  384. else:
  385. frequencies = [args.frequency]
  386.  
  387. print('Sending commands to ' + args.server + ':' + str(args.port))
  388. picture_thread = None
  389. try:
  390. if args.no_camera:
  391. picture_function = None
  392. elif webcam:
  393. print('Using images from webcam')
  394. picture_thread = PictureWrapper(
  395. ('streamer', '-f', 'jpeg', '-s', '640x480', '-o', '/dev/stdout')
  396. )
  397. picture_thread.start()
  398. picture_function = picture_thread.get_picture
  399. elif raspi_camera:
  400. print('Using images from Raspberry Pi camera module')
  401. # Use a smaller size here, because computing the % difference
  402. # on the Pi is incredibly slow
  403. picture_thread = PictureWrapper(
  404. ('raspistill', '-w', '320', '-h', '240', '-t', '100', '-o', '-')
  405. )
  406. picture_thread.start()
  407. picture_function = picture_thread.get_picture
  408. else:
  409. print('Not using camera')
  410. picture_function = None
  411.  
  412. search_for_command_codes(
  413. args.server,
  414. args.port,
  415. frequencies,
  416. get_picture_function=picture_function,
  417. bit_depth=args.bit_depth,
  418. )
  419. # pylint: disable=broad-except
  420. except (KeyboardInterrupt, Exception) as exc:
  421. print('Caught exception, exiting')
  422. print(str(exc))
  423. if picture_thread is not None:
  424. picture_thread.stop()
  425. picture_thread.join()
  426.  
  427.  
  428. if __name__ == '__main__':
  429. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement