app.py 19 KB

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