app.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. from flask import Flask, flash, session, request, Response, send_from_directory, render_template, jsonify
  4. from flask_cors import cross_origin
  5. import signal
  6. import sys
  7. import numpy as np
  8. import os
  9. import json
  10. import time
  11. import cv2
  12. import threading
  13. import base64
  14. from frame import frame_process, frame_grid
  15. #######################################
  16. # Config and Data Object
  17. ###################################################################
  18. storageFolder = "storage"
  19. settingsFilename = storageFolder + "/settings.json"
  20. confData = [
  21. {
  22. # Settings sfom JSON
  23. 'id': 0,
  24. 'size': (800, 600),
  25. 'undistort': {
  26. 'type': 0,
  27. 'calibration': {
  28. 'matrix': np.array([[448.27817297404454, 0, 291.8520566174806], [0, 584.2504759789658, 239.55416051020535], [0, 0, 1]]),
  29. 'distortion': np.array([-0.04882221532751064, -0.67818019974141, -0.0037673942250171966, 0.027266585061974342, 1.2104220826993612]),
  30. },
  31. 'distortion_f': 0.0,
  32. },
  33. 'image_transform': {
  34. 'rotation': 0,
  35. 'scale_x': 1.0,
  36. 'scale_y': 1.0
  37. },
  38. 'cvSettings': {'name': 'Top', 'contrast': 1, 'brightness': 0, 'blur': (1, 1), 'adaptiveThreshold_blockSize': 11, 'adaptiveThreshold_C': 2},
  39. # Run variables
  40. 'vid': None,
  41. 'outputFrame': None,
  42. 'lock': threading.Lock(),
  43. 'distortion_maps': {'map_x': None, 'map_y': None},
  44. # Changes via Flask app
  45. 'viewParams': {'raw':0, 'cv':0, 'ppmm':10, 'grid':0, 'marker':0},
  46. },
  47. {
  48. # Settings sfom JSON
  49. 'id': 1,
  50. 'size': (640, 427),
  51. 'undistort': {
  52. 'type': 0,
  53. 'calibration': {
  54. 'matrix': np.array([[448.27817297404454, 0, 291.8520566174806], [0, 584.2504759789658, 239.55416051020535], [0, 0, 1]]),
  55. 'distortion': np.array([-0.04882221532751064, -0.67818019974141, -0.0037673942250171966, 0.027266585061974342, 1.2104220826993612]),
  56. },
  57. 'distortion_f': 0.0,
  58. },
  59. 'image_transform': {
  60. 'rotation': 0,
  61. 'scale_x': 1.0,
  62. 'scale_y': 1.0
  63. },
  64. 'cvSettings': {'name': 'Bottom', 'contrast': 1, 'brightness': 0, 'blur': (1, 1), 'adaptiveThreshold_blockSize': 799, 'adaptiveThreshold_C': 27},
  65. # Run variables
  66. 'vid': None,
  67. 'outputFrame': None,
  68. 'lock': threading.Lock(),
  69. 'distortion_maps': {'map_x': None, 'map_y': None},
  70. # Changes via Flask app
  71. 'viewParams': {'raw':0, 'cv':0, 'ppmm':10, 'grid':0, 'marker':0},
  72. }
  73. ]
  74. emptyFrame = cv2.imread('noimage.jpg')
  75. ###################################################################
  76. # Functions
  77. ###################################################################
  78. # Create Image correction maps by distortion factor
  79. def create_distortion_maps(width, height, distortion_type='barrel', strength=0.5):
  80. #height, width = image_size
  81. center_x, center_y = width / 2, height / 2
  82. def map_coordinates(x, y):
  83. rel_x = (x - center_x) / center_x
  84. rel_y = (y - center_y) / center_y
  85. radius = np.sqrt(rel_x**2 + rel_y**2)
  86. if distortion_type == 'barrel':
  87. factor = 1 + strength * radius**2
  88. elif distortion_type == 'pincushion':
  89. factor = 1 / (1 + strength * radius**2)
  90. else:
  91. factor = 1
  92. new_x = center_x + factor * rel_x * center_x
  93. new_y = center_y + factor * rel_y * center_y
  94. return new_x, new_y
  95. map_x = np.zeros((height, width), np.float32)
  96. map_y = np.zeros((height, width), np.float32)
  97. for y in range(height):
  98. for x in range(width):
  99. new_x, new_y = map_coordinates(x, y)
  100. map_x[y, x] = new_x
  101. map_y[y, x] = new_y
  102. return map_x, map_y
  103. ###################################################################
  104. # Capture Video and set capture options
  105. def videoCapture(num):
  106. global confData
  107. if confData[num]['vid'] != None:
  108. if confData[num]['vid'].isOpened():
  109. confData[num]['vid'].release()
  110. confData[num]['vid'] = cv2.VideoCapture(confData[num]['id'])
  111. confData[num]['vid'].set(cv2.CAP_PROP_FRAME_WIDTH, confData[num]['size'][0])
  112. confData[num]['vid'].set(cv2.CAP_PROP_FRAME_HEIGHT, confData[num]['size'][1])
  113. confData[num]['vid'].set(cv2.CAP_PROP_FPS, 30)
  114. ###################################################################
  115. # Load cams settings from LSON file
  116. def load_cam_settings():
  117. global confData
  118. global settingsFilename
  119. with open(settingsFilename) as infile:
  120. result = json.load(infile)
  121. for num in range(len(confData)):
  122. prevId = confData[num]['id']
  123. prevSize = confData[num]['size']
  124. confData[num]['id'] = result['cams'][num]['id']
  125. confData[num]['undistort'] = result['cams'][num]['undistort']
  126. confData[num]['undistort']['calibration']['matrix'] = np.array(result['cams'][num]['undistort']['calibration']['matrix'])
  127. confData[num]['undistort']['calibration']['distortion'] = np.array(result['cams'][num]['undistort']['calibration']['distortion'])
  128. #confData[num]['undistort_type'] = result['cams'][num]['undistort_type']
  129. #confData[num]['calibration']['matrix'] = np.array(result['cams'][num]['calibration']['matrix'])
  130. #confData[num]['calibration']['distortion'] = np.array(result['cams'][num]['calibration']['distortion'])
  131. #confData[num]['distortion_f'] = result['cams'][num]['distortion_f']
  132. #confData[num]['rotation'] = result['cams'][num]['rotation']
  133. confData[num]['image_transform'] = result['cams'][num]['image_transform']
  134. confData[num]['size'] = (result['cams'][num]['size']['width'], result['cams'][num]['size']['height'])
  135. #if confData[num]['undistort_type'] == 0:
  136. map_x, map_y = create_distortion_maps(confData[num]['size'][0], confData[num]['size'][1], 'barrel', confData[num]['undistort']['distortion_f'])
  137. confData[num]['distortion_maps']['map_x'] = map_x
  138. confData[num]['distortion_maps']['map_y'] = map_y
  139. #else:
  140. newcameramtx, roi = cv2.getOptimalNewCameraMatrix(confData[num]['undistort']['calibration']['matrix'], confData[num]['undistort']['calibration']['distortion'], confData[num]['size'], 1, confData[num]['size'])
  141. #x, y, w, h = roi
  142. confData[num]['undistort']['calibration']['newcameramtx'] = newcameramtx
  143. confData[num]['undistort']['calibration']['roi'] = roi
  144. confData[num]['cvSettings'] = result['cv'][num]
  145. if confData[num]['id'] != prevId or confData[num]['size'][0] != prevSize[0] or confData[num]['size'][1] != prevSize[1]:
  146. videoCapture(num)
  147. return
  148. ###################################################################
  149. load_cam_settings()
  150. def get_empty_frame(num):
  151. frame = emptyFrame
  152. frame = cv2.resize(frame, (confData[num]['size']))
  153. frame = frame_grid(frame, confData[num]['viewParams'])
  154. return frame
  155. def frame_undistort(frame, num):
  156. global confData
  157. # Resize image
  158. frame = cv2.resize(frame, confData[num]['size'])
  159. if confData[num]['undistort']['type'] == 0:
  160. if confData[num]['undistort']['distortion_f'] != 0:
  161. frame = cv2.remap(frame, confData[num]['distortion_maps']['map_x'], confData[num]['distortion_maps']['map_y'], cv2.INTER_LINEAR)
  162. else:
  163. frame = cv2.undistort(frame, confData[num]['undistort']['calibration']['matrix'], confData[num]['undistort']['calibration']['distortion'], None, confData[num]['undistort']['calibration']['newcameramtx'])
  164. x, y, w, h = confData[num]['undistort']['calibration']['roi']
  165. frame = frame[y: y+h, x: x+w]
  166. return frame
  167. def frame_transform(frame, num):
  168. rotation = confData[num]['image_transform']['rotation']
  169. scale_x = confData[num]['image_transform']['scale_x']
  170. scale_y = confData[num]['image_transform']['scale_y']
  171. if rotation != 0:
  172. rotate = cv2.ROTATE_90_CLOCKWISE
  173. if rotation == 90:
  174. rotate = cv2.ROTATE_90_CLOCKWISE
  175. if rotation == 180:
  176. rotate = cv2.ROTATE_180
  177. if rotation == -90:
  178. rotate = cv2.ROTATE_90_COUNTERCLOCKWISE
  179. frame = cv2.rotate(frame, rotate)
  180. if (scale_x != 0) or (scale_y != 0):
  181. frame = cv2.resize(frame, None, fx=scale_x, fy=scale_y)
  182. return frame
  183. def image_processing(num, cvParams):
  184. global confData
  185. objects = {}
  186. # try to start VideoCapture if its not opened yet
  187. if not confData[num]['vid'].isOpened():
  188. videoCapture(num)
  189. # if opened then try to read next frame
  190. if not confData[num]['vid'].isOpened():
  191. #frame = emptyFrame
  192. frame = get_empty_frame(num)
  193. else:
  194. # read the next frame from the video stream
  195. ret, frame = confData[num]['vid'].read()
  196. if ret:
  197. # Image Correction
  198. frame = frame_undistort(frame, num)
  199. # Frame Transform
  200. frame = frame_transform(frame, num)
  201. # Frame Process
  202. frame, objects = frame_process(num, frame, confData[num]['viewParams'], cvParams, confData[num]['cvSettings'])
  203. # Draw grid
  204. frame = frame_grid(frame, {'grid':1, 'marker':0})
  205. else:
  206. confData[num]['vid'].release()
  207. #frame = emptyFrame
  208. frame = get_empty_frame(num)
  209. (flag, encodedImage) = cv2.imencode(".jpg", frame)
  210. jpg_as_text = 'data:image/jpeg;base64,' + base64.b64encode(encodedImage).decode('utf-8')
  211. result = {'objects': objects, 'img': jpg_as_text}
  212. return result
  213. def video_processing(num):
  214. global confData
  215. # loop over frames from the video stream
  216. while True:
  217. # try to start VideoCapture if its not opened yet
  218. if not confData[num]['vid'].isOpened():
  219. videoCapture(num)
  220. # if opened then try to read next frame
  221. if not confData[num]['vid'].isOpened():
  222. frame = get_empty_frame(num)
  223. #frame = emptyFrame
  224. #frame = cv2.resize(frame, (confData[num]['size']))
  225. #frame = frame_grid(frame, confData[num]['viewParams'])
  226. time.sleep(1.0)
  227. else:
  228. # read the next frame from the video stream
  229. ret, frame = confData[num]['vid'].read()
  230. if ret:
  231. # Image Correction
  232. if confData[num]['viewParams']['raw'] != 1:
  233. frame = frame_undistort(frame, num)
  234. # Frame Transform
  235. frame = frame_transform(frame, num)
  236. # Frame Process
  237. if confData[num]['viewParams']['cv'] != 0:
  238. frame, objects = frame_process(num, frame, confData[num]['viewParams'], confData[num]['cvSettings'])
  239. else:
  240. time.sleep(0.03)
  241. # Draw grid
  242. frame = frame_grid(frame, confData[num]['viewParams'])
  243. else:
  244. confData[num]['vid'].release()
  245. #frame = emptyFrame
  246. frame = get_empty_frame(num)
  247. #time.sleep(0.2)
  248. # acquire the lock, set the output frame, and release the
  249. # lock
  250. with confData[num]['lock']:
  251. confData[num]['outputFrame'] = frame.copy()
  252. def generate_video(num):
  253. global confData
  254. # loop over frames from the output stream
  255. while True:
  256. # wait until the lock is acquired
  257. with confData[num]['lock']:
  258. # check if the output frame is available, otherwise skip
  259. # the iteration of the loop
  260. if confData[num]['outputFrame'] is None:
  261. continue
  262. # encode the frame in JPEG format
  263. (flag, encodedImage) = cv2.imencode(".jpg", confData[num]['outputFrame'])
  264. # ensure the frame was successfully encoded
  265. if not flag:
  266. continue
  267. # yield the output frame in the byte format
  268. yield(b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + bytearray(encodedImage) + b'\r\n')
  269. def cam_calibration(num):
  270. global confData
  271. # try to start VideoCapture if its not opened yet
  272. if not confData[num]['vid'].isOpened():
  273. videoCapture(num)
  274. # if opened then try to read next frame
  275. if not confData[num]['vid'].isOpened():
  276. result = {'result': 'error', 'msg': 'cannot find camera'}
  277. else:
  278. # read the next frame from the video stream
  279. ret, frame = confData[num]['vid'].read()
  280. if ret:
  281. square_size = 1.0
  282. pattern_size = (9, 6)
  283. pattern_points = np.zeros((np.prod(pattern_size), 3), np.float32)
  284. pattern_points[:, :2] = np.indices(pattern_size).T.reshape(-1, 2)
  285. pattern_points *= square_size
  286. obj_points = []
  287. img_points = []
  288. h, w = 0, 0
  289. h, w = frame.shape[:2]
  290. found, corners = cv2.findChessboardCorners(frame, pattern_size)
  291. if found:
  292. #term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.1)
  293. #cv2.cornerSubPix(frame, corners, (5, 5), (-1, -1), term)
  294. img_points.append(corners.reshape(-1, 2))
  295. obj_points.append(pattern_points)
  296. rms, camera_matrix, dist_coefs, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, (w, h), None, None)
  297. result = {'result': 'OK', 'matrix': camera_matrix.tolist(), 'distortion': dist_coefs.tolist()}
  298. if not found:
  299. result = {'result': 'error', 'msg': 'chessboard not found'}
  300. else:
  301. confData[num]['vid'].release()
  302. result = {'result': 'error', 'msg': 'cannot read image from camera'}
  303. return result
  304. ##################################
  305. ##################################
  306. # Flask App
  307. ##################################
  308. app = Flask(__name__, template_folder='templates')
  309. app.secret_key = 'ASA:VTMJI~ZvPOS8W:0L2cAb?WB}R0V_'
  310. origins_domains = '*'
  311. def shutdown_handler(signum, frame):
  312. print("Shutdown signal received")
  313. for num in range(2):
  314. if confData[num]['vid'] != None:
  315. if confData[num]['vid'].isOpened():
  316. confData[num]['vid'].release()
  317. print("Camera released")
  318. sys.exit(0)
  319. signal.signal(signal.SIGTERM, shutdown_handler)
  320. signal.signal(signal.SIGINT, shutdown_handler)
  321. ##############################
  322. # Static files
  323. ##############################
  324. @app.route('/assets/<path:path>')
  325. def send_frontend_files(path):
  326. return send_from_directory('frontend/assets/', path)
  327. @app.route('/images/<path:path>')
  328. def send_images_files(path):
  329. return send_from_directory('frontend/images/', path)
  330. @app.route('/cases/<path:path>')
  331. def send_cases_files(path):
  332. return send_from_directory('frontend/cases/', path)
  333. ##############################
  334. # Main Page
  335. ##############################
  336. @app.route('/', methods=['GET'])
  337. def home_page():
  338. #return render_template('index.html'), 200
  339. return send_from_directory('frontend/', 'index.html')
  340. @app.route("/video_feed/<int:num>", defaults={'raw':0, 'cv': 0, 'ppmm': 10, 'grid':0, 'marker':0 })
  341. @app.route("/video_feed/<int:num>/<int:raw>/<int:cv>/<int:ppmm>/<int:grid>/<int:marker>") # integer ppmm
  342. @app.route("/video_feed/<int:num>/<int:raw>/<int:cv>/<float:ppmm>/<int:grid>/<int:marker>") # float ppmm
  343. def video_feed0(num, raw, cv, ppmm, grid, marker):
  344. global confData
  345. confData[num]['viewParams']['raw'] = raw
  346. confData[num]['viewParams']['cv'] = cv
  347. confData[num]['viewParams']['ppmm'] = ppmm
  348. confData[num]['viewParams']['grid'] = grid
  349. confData[num]['viewParams']['marker'] = marker
  350. return Response(generate_video(num), mimetype = "multipart/x-mixed-replace; boundary=frame")
  351. @app.route("/cv/<int:num>", methods=['POST'])
  352. @cross_origin(origins=origins_domains)
  353. def cv_params(num):
  354. cvParams = request.json
  355. return jsonify(image_processing(num, cvParams)), 200
  356. @app.route("/calibration/<int:num>")
  357. @cross_origin(origins=origins_domains)
  358. def calibration(num):
  359. return jsonify(cam_calibration(num)), 200
  360. @app.route("/settings", methods=['GET'])
  361. @cross_origin(origins=origins_domains)
  362. def settingsGet():
  363. global settingsFilename
  364. with open(settingsFilename) as infile:
  365. result = json.load(infile)
  366. return result, 200
  367. @app.route("/settings", methods=['POST'])
  368. @cross_origin(origins=origins_domains)
  369. def settingsSave():
  370. global settingsFilename
  371. try:
  372. json_object = json.dumps(request.json, indent=2)
  373. with open(settingsFilename, "w") as outfile:
  374. outfile.write(json_object)
  375. load_cam_settings()
  376. except Exception as e:
  377. return jsonify({'error': repr(e)}), 200
  378. else:
  379. return jsonify({'result': 'OK'}), 200
  380. @app.route("/json/<string:file>", defaults={'folder':''}, methods=['GET'])
  381. @app.route("/json/<string:folder>/<string:file>", methods=['GET'])
  382. @cross_origin(origins=origins_domains)
  383. def jsonGet(folder, file):
  384. global storageFolder
  385. #filename = 'settings/' + folder + '/' + file + '.json'
  386. if folder == '':
  387. #filename = 'settings/' + file + '.json'
  388. filename = storageFolder + '/' + file + '.json'
  389. else:
  390. #filename = 'settings/' + folder + '/' + file + '.json'
  391. filename = storageFolder + '/' + folder + '/' + file + '.json'
  392. with open(filename) as infile:
  393. result = json.load(infile)
  394. result = jsonify({"result": result})
  395. return result, 200
  396. @app.route("/json-list/<string:folder>", methods=['GET'])
  397. @cross_origin(origins=origins_domains)
  398. def jsonList(folder):
  399. global storageFolder
  400. #files = os.listdir('settings/' + folder)
  401. files = os.listdir(storageFolder + '/' + folder)
  402. result = jsonify({"result": files})
  403. return result, 200
  404. @app.route("/json/<string:file>", defaults={'folder':''}, methods=['POST'])
  405. @app.route("/json/<string:folder>/<string:file>", methods=['POST'])
  406. @cross_origin(origins=origins_domains)
  407. def jsonSave(folder, file):
  408. global storageFolder
  409. try:
  410. json_object = json.dumps(request.json, indent=2)
  411. #filename = 'settings/' + folder + '/' + file + '.json'
  412. if folder == '':
  413. #filename = 'settings/' + file + '.json'
  414. filename = storageFolder + '/' + file + '.json'
  415. else:
  416. #filename = 'settings/' + folder + '/' + file + '.json'
  417. filename = storageFolder + '/' + folder + '/' + file + '.json'
  418. with open(filename, "w") as outfile:
  419. outfile.write(json_object)
  420. except Exception as e:
  421. return jsonify({'error': repr(e)}), 200
  422. else:
  423. return jsonify({'result': 'OK'}), 200
  424. @app.route("/json/rename/<string:folder>/<string:file_old>/<string:file_new>", methods=['GET'])
  425. @cross_origin(origins=origins_domains)
  426. def jsonRename(folder, file_old, file_new):
  427. global storageFolder
  428. filename_old = storageFolder + '/' + folder + '/' + file_old + '.json'
  429. filename_new = storageFolder + '/' + folder + '/' + file_new + '.json'
  430. if os.path.exists(filename_old):
  431. try:
  432. os.rename(filename_old, filename_new)
  433. return jsonify({'result': 'OK'}), 200
  434. except Exception as e:
  435. return jsonify({'error': repr(e)}), 200
  436. else:
  437. return jsonify({'error': 'File not exists'}), 200
  438. @app.route("/json/delete/<string:folder>/<string:file>", methods=['GET'])
  439. @cross_origin(origins=origins_domains)
  440. def jsonDelete(folder, file):
  441. global storageFolder
  442. filename = storageFolder + '/' + folder + '/' + file + '.json'
  443. if os.path.exists(filename):
  444. try:
  445. os.remove(filename)
  446. return jsonify({'result': 'OK'}), 200
  447. except Exception as e:
  448. return jsonify({'error': repr(e)}), 200
  449. else:
  450. return jsonify({'error': 'File not exists'}), 200
  451. ##############################
  452. # Error Pages
  453. ##############################
  454. @ app.errorhandler(404)
  455. def err_404(error):
  456. return render_template('error.html', message='Page not found'), 404
  457. @ app.errorhandler(500)
  458. def err_500(error):
  459. return render_template('error.html', message='Internal server error'), 500
  460. ##############################
  461. # Logging configure
  462. ##############################
  463. if not app.debug:
  464. import logging
  465. from logging.handlers import RotatingFileHandler
  466. file_handler = RotatingFileHandler('log/my_app.log', 'a', 1 * 1024 * 1024, 10)
  467. file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
  468. app.logger.setLevel(logging.INFO)
  469. file_handler.setLevel(logging.INFO)
  470. app.logger.addHandler(file_handler)
  471. app.logger.info('startup')
  472. ##############################
  473. # Run app
  474. ##############################
  475. if __name__ == '__main__':
  476. t1 = threading.Thread(target=video_processing, args=(0,), daemon = True).start()
  477. t2 = threading.Thread(target=video_processing, args=(1,), daemon = True).start()
  478. app.run(host='0.0.0.0')