cockpit 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. #!/usr/bin/env python
  2. import os
  3. import re
  4. import json
  5. import glob
  6. import shlex
  7. import collections
  8. import datetime
  9. import logging
  10. import random
  11. import argparse
  12. import subprocess
  13. import curses
  14. def read_info_file(path):
  15. result = {}
  16. with open(path) as f:
  17. for line in f:
  18. key, value = line.split('=')
  19. result[key] = value.strip()
  20. return result
  21. def read_edf_id19_header(filename):
  22. result = {}
  23. with open(filename) as f:
  24. data = f.read(1024)
  25. pattern = re.compile(r'(.*) = (.*) ;')
  26. for line in data.split('\n'):
  27. m = pattern.match(line)
  28. if m:
  29. result[m.group(1).strip()] = m.group(2).strip()
  30. return result
  31. def extract_motor_positions(id19_header):
  32. names = id19_header['motor_mne'].split(' ')
  33. values = id19_header['motor_pos'].split(' ')
  34. return {k: float(v) for (k, v) in zip(names, values)}
  35. def have_flats(path):
  36. large = os.path.join(path, 'fc')
  37. small = os.path.join(path, 'fc-small')
  38. return (os.path.exists(large) and
  39. os.path.exists(small) and
  40. glob.glob(os.path.join(large, '*.tif')) and
  41. glob.glob(os.path.join(small, '*.tif')))
  42. class Configuration(object):
  43. def __init__(self, source, destination):
  44. self.source = os.path.abspath(source)
  45. self.destination = os.path.abspath(destination)
  46. class LineList(object):
  47. def __init__(self, window):
  48. self.window = window
  49. self.height, self.width = window.getmaxyx()
  50. self.current = 0
  51. self.lines = []
  52. def redraw(self):
  53. for y in range(len(self.lines)):
  54. line, attr = self.lines[y]
  55. self.window.addnstr(y, 0, line, self.width, attr)
  56. self.window.clrtoeol()
  57. self.window.refresh()
  58. def add_line(self, s, attr=0):
  59. self.lines.append((s, attr))
  60. if len(self.lines) > self.height - 1:
  61. self.lines = self.lines[1:]
  62. self.redraw()
  63. def update_last(self, line=None, attr=None):
  64. if not self.lines:
  65. return
  66. old_line, old_attr = self.lines[-1]
  67. self.lines[-1] = (line or old_line, attr or old_attr)
  68. self.redraw()
  69. class Colors(object):
  70. NORMAL = 1
  71. SUCCESS = 2
  72. WARN = 3
  73. ERROR = 4
  74. STATUS_BAR = 5
  75. HIGHLIGHT = 6
  76. def __init__(self):
  77. curses.init_pair(Colors.NORMAL, curses.COLOR_WHITE, curses.COLOR_BLACK)
  78. curses.init_pair(Colors.SUCCESS, curses.COLOR_GREEN, curses.COLOR_BLACK)
  79. curses.init_pair(Colors.WARN, curses.COLOR_YELLOW, curses.COLOR_BLACK)
  80. curses.init_pair(Colors.ERROR, curses.COLOR_RED, curses.COLOR_BLACK)
  81. curses.init_pair(Colors.STATUS_BAR, curses.COLOR_BLACK, curses.COLOR_YELLOW)
  82. curses.init_pair(Colors.HIGHLIGHT, curses.COLOR_WHITE, curses.COLOR_BLACK)
  83. self.attrs = {
  84. Colors.NORMAL: curses.A_NORMAL,
  85. Colors.SUCCESS: curses.A_NORMAL,
  86. Colors.WARN: curses.A_BOLD,
  87. Colors.ERROR: curses.A_BOLD,
  88. Colors.STATUS_BAR: curses.A_NORMAL,
  89. Colors.HIGHLIGHT: curses.A_BOLD,
  90. }
  91. def get(self, code):
  92. return curses.color_pair(code) | self.attrs[code]
  93. class StatusBar(object):
  94. def __init__(self, window, colors):
  95. self.window = window
  96. self.c = colors
  97. def update(self, s):
  98. self.window.clear()
  99. self.window.bkgd(' ', self.c.get(Colors.STATUS_BAR))
  100. self.window.addstr(s, self.c.get(Colors.STATUS_BAR))
  101. class LogList(LineList):
  102. def __init__(self, window, colors):
  103. self.line_list = LineList(window)
  104. self.c = colors
  105. self.log_file = open('cockpit.log', 'a')
  106. def _log_time(self, s, attr):
  107. timestamp = datetime.datetime.now().strftime('%H:%M:%S')
  108. log = '[{}] {}'.format(timestamp, s)
  109. self.line_list.add_line(log, attr)
  110. self.log_file.write(log)
  111. if not log.endswith('\n'):
  112. self.log_file.write('\n')
  113. self.log_file.flush()
  114. os.fsync(self.log_file.fileno())
  115. def info(self, s):
  116. self._log_time(s, self.c.get(Colors.NORMAL))
  117. def highlight(self, s):
  118. self._log_time(s, self.c.get(Colors.HIGHLIGHT))
  119. def success(self, s):
  120. self._log_time(s, self.c.get(Colors.SUCCESS))
  121. def warn(self, s):
  122. self._log_time(s, self.c.get(Colors.WARN))
  123. def error(self, s):
  124. self._log_time(s, self.c.get(Colors.ERROR))
  125. class CommandList(LineList):
  126. def __init__(self, window, colors):
  127. self.line_list = LineList(window)
  128. self.normal = colors.get(Colors.NORMAL)
  129. self.highlight = colors.get(Colors.HIGHLIGHT)
  130. self.current = ''
  131. def add_character(self, c):
  132. self.current += c
  133. self.line_list.update_last(line='> {}'.format(self.current))
  134. def backspace(self):
  135. if self.current:
  136. self.current = self.current[:len(self.current) - 1]
  137. self.add_character('')
  138. def set_actions(self, actions):
  139. self.current = ''
  140. for a in actions.values():
  141. self.line_list.add_line('[{}] {}'.format(a.key, a.note), self.highlight)
  142. class Action(object):
  143. def __init__(self, key, note, func, next_state):
  144. self.key = key
  145. self.note = note
  146. self.run = func
  147. self.next_state = next_state
  148. def __repr__(self):
  149. return '<Action:key={}>'.format(self.key)
  150. class StateMachine(object):
  151. START = 0
  152. QUIT = 1
  153. SYNC = 2
  154. CLEAN = 3
  155. FLATCORRECT = 4
  156. OPTIMIZE = 5
  157. QUICK_OPTIMIZE = 6
  158. RECONSTRUCT = 7
  159. def __init__(self):
  160. self.current = StateMachine.START
  161. self.transitions = {
  162. StateMachine.START: collections.OrderedDict(),
  163. StateMachine.CLEAN: collections.OrderedDict(),
  164. StateMachine.FLATCORRECT: collections.OrderedDict(),
  165. StateMachine.QUIT: collections.OrderedDict(),
  166. StateMachine.SYNC: collections.OrderedDict(),
  167. StateMachine.OPTIMIZE: collections.OrderedDict(),
  168. StateMachine.QUICK_OPTIMIZE: collections.OrderedDict(),
  169. StateMachine.RECONSTRUCT: collections.OrderedDict(),
  170. }
  171. def add_action(self, from_state, action):
  172. self.transitions[from_state][action.key] = action
  173. def transition(self, action):
  174. self.current = self.transitions[self.current][action.key].next_state
  175. @property
  176. def actions(self):
  177. return self.transitions[self.current]
  178. def is_valid_key(self, key):
  179. return key in (k for k in self.transitions[self.current].keys())
  180. class Parameters(object):
  181. def __init__(self, axis, angle):
  182. self.axis = axis
  183. self.angle = angle
  184. class Dataset(object):
  185. def __init__(self, path):
  186. self.path = path
  187. self.prefix = os.path.basename(path)
  188. def __repr__(self):
  189. return '<Dataset(prefix={})>'.format(self.prefix)
  190. class Application(object):
  191. def __init__(self, config):
  192. self.config = config
  193. self.running = True
  194. self.datasets = []
  195. self.known = set()
  196. self.current = None
  197. self.index = -1
  198. def scan(self):
  199. new = []
  200. for p in os.listdir(self.config.destination):
  201. path = os.path.join(self.config.destination, p)
  202. if os.path.isdir(path) and p not in self.known:
  203. new.append(Dataset(path))
  204. self.known.add(p)
  205. if new:
  206. for d in new:
  207. self.log.highlight("Found new dataset {}".format(d.prefix))
  208. self.datasets.append(d)
  209. def run_command(self, cmd):
  210. try:
  211. self.log.info("Executing `{}`".format(cmd))
  212. p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  213. for line in iter(p.stdout.readline, ''):
  214. self.log.info(line)
  215. for line in iter(p.stderr.readline, ''):
  216. self.log.error(line)
  217. while p.poll() is None:
  218. pass
  219. if p.returncode == 0:
  220. self.log.success("done")
  221. return True
  222. self.log.error("Command returned {}".format(p.returncode))
  223. return False
  224. except Exception as e:
  225. self.log.error("{}".format(e))
  226. return False
  227. @property
  228. def params_name(self):
  229. return '{}params.json'.format(self.current.prefix)
  230. def read_info(self):
  231. info_file = os.path.join(self.current.path, '{}.info'.format(self.current.prefix))
  232. return read_info_file(info_file)
  233. def read_optimal_params(self, scale=1.0):
  234. with open(self.params_name) as f:
  235. opt = json.load(f)
  236. return Parameters(opt['x-center']['value'], opt['lamino-angle']['value'] * scale)
  237. def optimize(self, resize):
  238. slices_per_device = 100
  239. half_range = 1.0
  240. info = self.read_info()
  241. axis = (float(info['Col_end']) + 1) / 2.0
  242. axis_step = 0.25
  243. fname = os.path.join(self.current.path, '{}0000.edf'.format(self.current.prefix))
  244. header = read_edf_id19_header(fname)
  245. motor_pos = extract_motor_positions(header)
  246. inclination_angle = motor_pos['rytot']
  247. theta = 90.0 - inclination_angle
  248. angle_step = 0.025
  249. angle_start = theta - half_range
  250. angle_stop = theta + half_range
  251. x_region = 960
  252. y_region = 960
  253. fc_path = '{path}/fc'.format(path=self.current.path)
  254. if resize:
  255. x_region /= 2
  256. y_region /= 2
  257. axis /= 2
  258. axis_step /= 2
  259. fc_path = '{path}/fc-small'.format(path=self.current.path)
  260. axis_start = axis - slices_per_device * axis_step
  261. axis_stop = axis + slices_per_device * axis_step
  262. self.log.info(" Using theta = {}, inclination angle = {}".format(theta, inclination_angle))
  263. self.log.info(" Scanning lamino angle within [{}:{}:{}]".format(angle_start, angle_stop, angle_step))
  264. self.log.info(" Scanning x axis within [{}:{}:{}]".format(axis_start, axis_stop, axis_step))
  265. opt_params = ('--num-iterations 2'
  266. ' --axis-range={ax_start},{ax_stop},{ax_step}'
  267. ' --lamino-angle-range={an_start},{an_stop},{an_step}'
  268. ' --metric kurtosis --z-metric kurtosis'
  269. ' --tmp-output {path}/tmp'
  270. .format(ax_start=axis_start, ax_stop=axis_stop, ax_step=axis_step,
  271. an_start=angle_start, an_stop=angle_stop, an_step=angle_step,
  272. path=self.current.path))
  273. params = ('--x-region="-{x_region},{x_region},1"'
  274. ' --y-region="-{y_region},{y_region},1"'
  275. ' --overall-angle -360'
  276. ' --pixel-size {pixel_size}e-6'
  277. ' --roll-angle 0'
  278. ' --slices-per-device 100'
  279. .format(pixel_size=info['PixelSize'], x_region=x_region, y_region=y_region))
  280. cmd = ('optimize-parameters --verbose'
  281. ' {fc_path}'
  282. ' {opt_params}'
  283. ' --reco-params "{params}"'
  284. ' --params-filename {params_name}'
  285. .format(opt_params=opt_params, params=params, params_name=self.params_name, fc_path=fc_path))
  286. return self.run_command(cmd)
  287. def update_statusbar(self):
  288. self.log.info("Current dataset: {}".format(self.current.prefix))
  289. if self.current:
  290. self.status_bar.update("Current: {} [{}/{}]".format(self.current.prefix, self.index + 1, len(self.datasets)))
  291. def on_reconstruct(self):
  292. self.log.highlight("Reconstructing ...")
  293. with open(os.path.join(self.prefix, 'params.json')) as f:
  294. info = self.read_info()
  295. opt = json.load(f)
  296. lamino_angle = opt['lamino-angle']['value']
  297. axis = opt['x-center']['value']
  298. x_region = 960
  299. y_region = 960
  300. cmd = ('tofu lamino --verbose'
  301. ' --projections {prefix}/fc'
  302. ' --x-region="-{x_region},{x_region},1"'
  303. ' --y-region="-{y_region},{y_region},1"'
  304. ' --lamino-angle {lamino_angle}'
  305. ' --axis {x_axis},{y_axis}'
  306. ' --overall-angle -360'
  307. ' --pixel-size {pixel_size}e-6'
  308. ' --roll-angle 0'
  309. ' --slices-per-device 300'
  310. ' --output {prefix}/slices/slice'
  311. .format(pixel_size=info['PixelSize'],
  312. x_region=x_region, y_region=y_region,
  313. lamino_angle=lamino_angle,
  314. x_axis=axis, y_axis=float(info['Dim_2']) / 2,
  315. prefix=self.prefix))
  316. return self.run_command(cmd)
  317. def on_quit(self):
  318. self.running = False
  319. return True
  320. def on_sync(self):
  321. self.log.highlight("Syncing data ...")
  322. cmd = 'bash sync.sh {} {}'.format(self.config.source, self.config.destination)
  323. return self.run_command(cmd)
  324. def on_clean(self):
  325. self.log.highlight("Cleaning {}".format(self.config.destination))
  326. return True
  327. def on_flat_correct(self):
  328. self.log.highlight("Flat field correction ...")
  329. try:
  330. info = self.read_info()
  331. path = self.current.path
  332. data = dict(path=path, prefix=self.current.prefix, num=info['TOMO_N'], step=1)
  333. cmd = ('tofu flatcorrect --verbose'
  334. ' --reduction-mode median'
  335. ' --projections "{path}/{prefix}*.edf"'
  336. ' --darks {path}/darkend0000.edf'
  337. ' --flats {path}/ref*_0000.edf'
  338. ' --flats2 {path}/ref*_{num}.edf'
  339. ' --number {num}'
  340. ' --step {step}'
  341. ' --absorptivity'
  342. ' --fix-nan-and-inf'.format(**data))
  343. large_cmd = cmd + ' --output {path}/fc/fc-%04i.tif'.format(path=path)
  344. small_cmd = cmd + ' --resize 2 --output {path}/fc-small/fc-%04i.tif'.format(path=path)
  345. if not self.run_command(small_cmd):
  346. self.log.error("Could not create small flat field corrected projections")
  347. return result
  348. return self.run_command(large_cmd)
  349. except Exception as e:
  350. self.log.error("Error: {}".format(e))
  351. return False
  352. def on_quick_optimize(self):
  353. self.log.highlight("Quick optimization ...")
  354. try:
  355. if self.optimize(True):
  356. with open(self.params_name) as f:
  357. opt = json.load(f)
  358. opt['x-center']['value'] *= 2.0
  359. with open(self.params_name, 'w') as f:
  360. json.dump(opt, f)
  361. params = self.read_optimal_params()
  362. self.log.highlight(" Optimal axis: {}".format(params.angle))
  363. self.log.highlight(" Optimal center: {}".format(params.axis))
  364. return True
  365. except Exception as e:
  366. self.log.error("Error: {}".format(e))
  367. return False
  368. def on_optimize(self):
  369. self.log.highlight("Optimizing ...")
  370. try:
  371. if self.optimize(False):
  372. params = self.read_optimal_params()
  373. self.log.highlight(" Optimal axis: {}".format(params.angle))
  374. self.log.highlight(" Optimal center: {}".format(params.axis))
  375. return True
  376. except Exception as e:
  377. self.log.error("Could not optimize: {}".format(e))
  378. return False
  379. def on_next_dataset(self):
  380. self.index = (self.index + 1) % len(self.datasets)
  381. self.current = self.datasets[self.index]
  382. self.update_statusbar()
  383. def on_previous_dataset(self):
  384. self.index = (self.index - 1) % len(self.datasets)
  385. self.current = self.datasets[self.index]
  386. self.update_statusbar()
  387. def do_nothing(self):
  388. return True
  389. def _run(self, screen):
  390. curses.curs_set(False)
  391. height, width = screen.getmaxyx()
  392. colors = Colors()
  393. top_pane = screen.subwin(1, width, 0, 0)
  394. bottom_pane = screen.subwin(height - 1, width, 1, 0)
  395. left_pane = bottom_pane.subwin(height - 1, width / 4, 1, 0)
  396. right_pane = bottom_pane.subwin(height - 1, 3 * width / 4, 1, width / 4)
  397. self.status_bar = StatusBar(top_pane, colors)
  398. self.status_bar.update('Cockpit')
  399. cmd_window = CommandList(left_pane, colors)
  400. machine = StateMachine()
  401. quit = Action('e', 'Exit', self.on_quit, machine.QUIT)
  402. sync = Action('s', 'Sync', self.on_sync, machine.SYNC)
  403. clean = Action('c', 'Clean', self.on_clean, machine.CLEAN)
  404. quick_optimize = Action('q', 'Quick optimization', self.on_quick_optimize, machine.QUICK_OPTIMIZE)
  405. optimize = Action('o', 'Optimization', self.on_optimize, machine.OPTIMIZE)
  406. reconstruct = Action('r', 'Reconstruct', self.on_reconstruct, machine.RECONSTRUCT)
  407. flatcorrect = Action('f', 'Flat correct', self.on_flat_correct, machine.FLATCORRECT)
  408. next_dataset = Action('n', 'Next dataset', self.on_next_dataset, machine.START)
  409. previous_dataset = Action('p', 'Previous dataset', self.on_previous_dataset, machine.START)
  410. machine.add_action(machine.START, sync)
  411. machine.add_action(machine.START, quick_optimize)
  412. machine.add_action(machine.START, optimize)
  413. machine.add_action(machine.START, next_dataset)
  414. machine.add_action(machine.START, previous_dataset)
  415. machine.add_action(machine.START, quit)
  416. machine.add_action(machine.SYNC, quit)
  417. machine.add_action(machine.QUICK_OPTIMIZE, reconstruct)
  418. machine.add_action(machine.QUICK_OPTIMIZE, optimize)
  419. machine.add_action(machine.QUICK_OPTIMIZE, next_dataset)
  420. machine.add_action(machine.QUICK_OPTIMIZE, previous_dataset)
  421. machine.add_action(machine.QUICK_OPTIMIZE, quit)
  422. machine.add_action(machine.OPTIMIZE, quick_optimize)
  423. machine.add_action(machine.OPTIMIZE, reconstruct)
  424. machine.add_action(machine.OPTIMIZE, next_dataset)
  425. machine.add_action(machine.OPTIMIZE, previous_dataset)
  426. machine.add_action(machine.OPTIMIZE, quit)
  427. machine.add_action(machine.CLEAN, sync)
  428. machine.add_action(machine.CLEAN, quit)
  429. machine.add_action(machine.FLATCORRECT, quick_optimize)
  430. machine.add_action(machine.FLATCORRECT, optimize)
  431. machine.add_action(machine.FLATCORRECT, next_dataset)
  432. machine.add_action(machine.FLATCORRECT, previous_dataset)
  433. machine.add_action(machine.FLATCORRECT, quit)
  434. self.log = LogList(right_pane, colors)
  435. self.log.info('Source dir set to {}'.format(self.config.source))
  436. self.log.info('Destination dir set to {}'.format(self.config.destination))
  437. self.scan()
  438. if self.datasets:
  439. self.index = 0
  440. self.current = self.datasets[0]
  441. if not have_flats(self.current.path):
  442. machine.add_action(machine.START, flatcorrect)
  443. machine.add_action(machine.SYNC, flatcorrect)
  444. if os.path.exists(self.params_name):
  445. machine.add_action(machine.START, reconstruct)
  446. self.update_statusbar()
  447. cmd_window.set_actions(machine.actions)
  448. while self.running:
  449. ci = screen.getch()
  450. cc = chr(ci) if ci < 256 else ''
  451. if machine.is_valid_key(cc):
  452. cmd_window.add_character(cc)
  453. action = machine.actions[cc]
  454. if action.run():
  455. machine.transition(action)
  456. cmd_window.set_actions(machine.actions)
  457. elif ci == curses.KEY_BACKSPACE:
  458. cmd_window.backspace()
  459. self.scan()
  460. screen.refresh()
  461. def run(self):
  462. curses.wrapper(self._run)
  463. if __name__ == '__main__':
  464. parser = argparse.ArgumentParser()
  465. parser.add_argument('--source', help='Source directory', default='.', metavar='DIR')
  466. parser.add_argument('--destination', help='Destination directory', default='.', metavar='DIR')
  467. args = parser.parse_args()
  468. config = Configuration(args.source, args.destination)
  469. app = Application(config)
  470. app.run()