setup.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import argparse
  2. import cv2
  3. import json
  4. import datetime
  5. import time
  6. import numpy as np
  7. from shutil import copyfile
  8. from os.path import exists
  9. from os import makedirs
  10. from math import sqrt
  11. def main():
  12. ap = argparse.ArgumentParser()
  13. ap.add_argument("-i", "--input", required=True, help="input image")
  14. ap.add_argument("-s", "--scale", type=float, default=0.25, help="scales input image by this factor (default: x0.25)")
  15. ap.add_argument("-c", "--config", type=str, default="config.json",
  16. help="name file to save config (default: config.json)")
  17. args = vars(ap.parse_args())
  18. img = cv2.imread(args["input"])
  19. height, width, _ = img.shape
  20. scale = args["scale"]
  21. window_name = "Setup"
  22. cv2.namedWindow(window_name)
  23. cv2.imshow(window_name, cv2.resize(img, (0, 0), fx=scale, fy=scale))
  24. cv2.setMouseCallback(window_name, null_callback)
  25. show_menu()
  26. food_color = FoodColor(img, scale, window_name)
  27. board_setup = BoardLimits(img, scale, window_name)
  28. food_limits = FoodLimits(img, scale, window_name)
  29. done = False
  30. state = 0
  31. setup_vars = {'Aspect Ratio': 1.0, 'Discrete Height': 100, 'Discrete Width': 100}
  32. while not done:
  33. cv2.waitKey(10)
  34. if state == 0:
  35. cv2.imshow(window_name, cv2.resize(img, (0, 0), fx=scale, fy=scale))
  36. cv2.setMouseCallback(window_name, null_callback)
  37. key = input("Enter command: ")
  38. if key == "q":
  39. done = True
  40. elif key == "1":
  41. state = 1
  42. board_setup.clear()
  43. board_setup.help()
  44. cv2.setMouseCallback(window_name, board_setup.on_mouse)
  45. elif key == "2":
  46. state = 2
  47. food_color.clear()
  48. food_color.help()
  49. cv2.setMouseCallback(window_name, food_color.on_mouse)
  50. elif key == "3":
  51. setup_vars['Aspect Ratio'] = -1
  52. while setup_vars['Aspect Ratio'] <= 0:
  53. try:
  54. setup_vars['Aspect Ratio'] = float(input("Insert image height by width ratio here: "))
  55. except ValueError:
  56. setup_vars['Aspect Ratio'] = -1
  57. if setup_vars['Aspect Ratio'] <= 0:
  58. print("Insert only a floating number with dot as separator.")
  59. show_menu()
  60. elif key == "4":
  61. setup_vars['Discrete Height'] = -1
  62. while setup_vars['Discrete Height'] <= 0:
  63. try:
  64. setup_vars['Discrete Height'] = int(input("Insert height discrete resolution: "))
  65. except ValueError:
  66. setup_vars['Discrete Height'] = -1
  67. if setup_vars['Discrete Height'] <= 0:
  68. print("Insert only round numbers.")
  69. setup_vars['Discrete Width'] = -1
  70. while setup_vars['Discrete Width'] <= 0:
  71. try:
  72. setup_vars['Discrete Width'] = int(input("Insert width discrete resolution: "))
  73. except ValueError:
  74. setup_vars['Discrete Width'] = -1
  75. if setup_vars['Discrete Width'] <= 0:
  76. print("Insert only round numbers.")
  77. show_menu()
  78. elif key == "5":
  79. state = 3
  80. food_limits.clear()
  81. food_limits.help()
  82. cv2.setMouseCallback(window_name, food_limits.on_mouse)
  83. elif key == "s":
  84. ts = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d_%H.%M.%S-')
  85. if exists(args["config"]):
  86. if not exists("bkp/"):
  87. makedirs("bkp")
  88. copyfile(args["config"], "bkp/" + ts + args["config"])
  89. with open(args["config"], "w") as file:
  90. json.dump({**setup_vars, **board_setup.toJSON(), **food_color.toJSON(), **food_limits.toJSON()},
  91. file, indent=4, sort_keys=True)
  92. done = True
  93. else:
  94. print("Error: Unrecognised Command.")
  95. show_menu()
  96. elif state == 1:
  97. board_setup.draw()
  98. if board_setup.done:
  99. state = 0
  100. show_menu()
  101. elif state == 2:
  102. food_color.draw()
  103. if food_color.done:
  104. state = 0
  105. show_menu()
  106. elif state == 3:
  107. food_limits.draw()
  108. if food_limits.done:
  109. state = 0
  110. show_menu()
  111. def show_menu():
  112. print("\nCommands: ")
  113. print("\tEnter '1' to setup board limits")
  114. print("\tEnter '2' to setup food color")
  115. print("\tEnter '3' to insert image aspect ratio")
  116. print("\tEnter '4' to insert discrete image height and width")
  117. print("\tEnter '5' to setup food limits")
  118. print("\tEnter 's' to save & quit")
  119. print("\tEnter 'q' to quit without saving")
  120. def null_callback(event, x, y, flags, param):
  121. return
  122. class BoardLimits:
  123. def __init__(self, img, scale, window_name):
  124. self.limits = []
  125. self.max_limits = 4
  126. self.orig = img
  127. self.img = img.copy()
  128. self.scale = scale
  129. self.window_name = window_name
  130. self.done = False
  131. self.limits_drawn = False
  132. def add_limit(self, x, y):
  133. x_img = int(x / self.scale)
  134. y_img = int(y / self.scale)
  135. self.limits.append((x_img, y_img))
  136. cv2.drawMarker(self.img, (x_img, y_img), (0, 0, 255), thickness=5)
  137. def draw(self):
  138. if len(self.limits) == self.max_limits and not self.limits_drawn:
  139. for i, limit in enumerate(self.limits):
  140. cv2.line(self.img, self.limits[i-1], limit, (0, 0, 255), thickness=3)
  141. self.limits_drawn = True
  142. cv2.imshow(self.window_name, cv2.resize(self.img, (0, 0), fx=self.scale, fy=self.scale))
  143. if self.enough_data():
  144. self.confirm()
  145. def enough_data(self):
  146. return len(self.limits) == self.max_limits
  147. def compute(self):
  148. return self.limits
  149. def toJSON(self):
  150. return {'Limits': self.limits}
  151. def help(self):
  152. print("--- Board Setup: Click on the {} corners of the board".format(self.max_limits))
  153. print("--- Board Setup: Please start from left corner and do it in the right order")
  154. def clear(self):
  155. self.limits = []
  156. self.limits_drawn = False
  157. self.img = self.orig.copy()
  158. self.done = False
  159. def on_mouse(self, event, x, y, flags, param):
  160. if event == cv2.EVENT_LBUTTONUP and not self.enough_data() :
  161. if len(self.limits) < self.max_limits:
  162. self.add_limit(x, y)
  163. def confirm(self):
  164. print("--- Board Setup: Press enter if you're ok with data or any other key if you want to restart setup...")
  165. key = cv2.waitKey(0)
  166. if key == 13: # Enter
  167. print("--- Board Setup: " + str(self.compute()))
  168. self.done = True
  169. else:
  170. self.clear()
  171. self.help()
  172. class FoodLimits:
  173. def __init__(self, img, scale, window_name):
  174. self.limits = []
  175. self.max_limits = 4
  176. self.orig = img
  177. self.img = img.copy()
  178. self.scale = scale
  179. self.window_name = window_name
  180. self.done = False
  181. self.limits_drawn = False
  182. self.min_dist = 5
  183. def add_limit(self, x, y):
  184. x_img = int(x / self.scale)
  185. y_img = int(y / self.scale)
  186. self.limits.append((x_img, y_img))
  187. cv2.drawMarker(self.img, (x_img, y_img), (0, 0, 255), thickness=5)
  188. def draw(self):
  189. if len(self.limits) == self.max_limits and not self.limits_drawn:
  190. for i, limit in enumerate(self.limits):
  191. cv2.line(self.img, self.limits[i-1], limit, (0, 0, 255), thickness=3)
  192. self.limits_drawn = True
  193. cv2.imshow(self.window_name, cv2.resize(self.img, (0, 0), fx=self.scale, fy=self.scale))
  194. if self.enough_data():
  195. self.confirm()
  196. def enough_data(self):
  197. return len(self.limits) == self.max_limits
  198. def compute(self):
  199. # TODO Improve with warp perspective...
  200. self.min_dist = self.img.shape[0]
  201. for i, (x,y) in enumerate(self.limits):
  202. dist = sqrt((x - self.limits[i-1][0])**2 + (y - self.limits[i-1][1])**2)
  203. self.min_dist = dist if dist < self.min_dist else self.min_dist
  204. return self.min_dist
  205. def toJSON(self):
  206. return {'Min Food Size': self.min_dist}
  207. def help(self):
  208. print("--- Food Setup: Click on the {} corners of the tiniest food".format(self.max_limits))
  209. def clear(self):
  210. self.limits = []
  211. self.limits_drawn = False
  212. self.img = self.orig.copy()
  213. self.done = False
  214. def on_mouse(self, event, x, y, flags, param):
  215. if event == cv2.EVENT_LBUTTONUP and not self.enough_data() :
  216. if len(self.limits) < self.max_limits:
  217. self.add_limit(x, y)
  218. def confirm(self):
  219. print("--- Food Setup: Press enter if you're ok with data or any other key if you want to restart setup...")
  220. key = cv2.waitKey(0)
  221. if key == 13: # Enter
  222. print("--- Food Setup: " + str(self.compute()))
  223. self.done = True
  224. else:
  225. self.clear()
  226. self.help()
  227. class FoodColor:
  228. def __init__(self, img, scale, window_name):
  229. self.colors = []
  230. self.orig = img
  231. self.img = img.copy()
  232. self.scale = scale
  233. self.window_name = window_name
  234. self.done = False
  235. def add(self, x, y):
  236. x_img = int(x / self.scale)
  237. y_img = int(y / self.scale)
  238. self.colors.append(self.orig[y_img, x_img])
  239. self.show_selected()
  240. def show_selected(self):
  241. if len(self.colors) >= 2:
  242. low, high = self.compute()
  243. mask = cv2.inRange(self.img, np.array(low, dtype=np.uint8), np.array(high, dtype=np.uint8))
  244. maskrgb = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
  245. selected = np.zeros(self.img.shape, dtype=np.uint8)
  246. selected[:, :, 2] = mask
  247. self.img = cv2.add(cv2.subtract(self.img, maskrgb), selected)
  248. def draw(self):
  249. cv2.imshow(self.window_name, cv2.resize(self.img, (0, 0), fx=self.scale, fy=self.scale))
  250. self.confirm()
  251. def compute(self):
  252. low_color = [255, 255, 255]
  253. high_color = [0, 0, 0]
  254. if len(self.colors) == 0:
  255. return tuple(high_color), tuple(low_color)
  256. for color in self.colors:
  257. for i, c in enumerate(color):
  258. if c < low_color[i]:
  259. low_color[i] = c
  260. if c > high_color[i]:
  261. high_color[i] = c
  262. return tuple(low_color), tuple(high_color)
  263. def toJSON(self):
  264. l, h = self.compute()
  265. l = tuple([int(x) for x in l])
  266. h = tuple([int(x) for x in h])
  267. return {'Low Food Color': l, 'High Food Color': h}
  268. def help(self):
  269. print("--- Color Setup: Click several times on foods to setup food color and then press enter.")
  270. def clear(self):
  271. self.colors = []
  272. self.img = self.orig.copy()
  273. self.done = False
  274. def on_mouse(self, event, x, y, flags, param):
  275. if event == cv2.EVENT_LBUTTONUP:
  276. self.add(x, y)
  277. def confirm(self):
  278. key = cv2.waitKey(10)
  279. if key == 13: # Enter
  280. print("--- Color Setup: " + str(self.compute()))
  281. self.done = True
  282. elif key == 0 and len(self.colors) > 0: # Delete
  283. del self.colors[len(self.colors)-1]
  284. self.img = self.orig.copy()
  285. self.show_selected()
  286. print("Last color removed. {} remaining(s).".format(len(self.colors)))
  287. if __name__ == "__main__":
  288. main()