main.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import numpy as np
  2. import cv2
  3. import time
  4. from threading import Thread
  5. import ctypes
  6. class Eclipse:
  7. def __init__(self, a: "float", b: "float"):
  8. c = np.sqrt(a * a - b * b)
  9. self.a2l, self.b2l = 2 * np.abs(a), 2 * np.abs(b)
  10. self.f1, self.f2 = (-c, 0), (c, 0)
  11. @staticmethod
  12. def __dis(p1: "tuple", p2: "tuple") -> "np.double":
  13. return np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
  14. def inner_rect(self, *, al: "float" = None, bl: "float" = None) -> "np.double":
  15. if al is None and bl is None:
  16. return np.double(0)
  17. if al is not None:
  18. al = np.abs(al)
  19. if al < 1E-5:
  20. return self.b2l
  21. if al >= self.a2l - 1E-5:
  22. return np.double(0)
  23. start, end, x, y = 0, self.b2l / 2, al / 2, self.b2l / 4
  24. while True:
  25. p = (x, y)
  26. dis = self.__dis(p, self.f1) + self.__dis(p, self.f2)
  27. if np.abs(dis - self.a2l) < 1E-5:
  28. return np.double(2 * y)
  29. if dis < self.a2l:
  30. start = y
  31. else:
  32. end = y
  33. y = (start + end) / 2
  34. else:
  35. bl = np.abs(bl)
  36. if bl < 1E-5:
  37. return self.a2l
  38. if bl >= self.b2l - 1E-5:
  39. return np.double(0)
  40. start, end, x, y = 0, self.a2l / 2, self.b2l / 4, bl / 2
  41. while True:
  42. p = (x, y)
  43. dis = self.__dis(p, self.f1) + self.__dis(p, self.f2)
  44. if np.abs(dis - self.a2l) < 1E-5:
  45. return np.double(2 * x)
  46. if dis < self.a2l:
  47. start = x
  48. else:
  49. end = x
  50. x = (start + end) / 2
  51. class Egg:
  52. Long, Short, Bright = 250, 175, 80
  53. HalfLong, HalfShort, Count = Long / 2, Short / 2, 60
  54. GapLong, GapShort = HalfLong / Count, HalfShort / Count
  55. def __init__(self, x, y, r):
  56. self.center, self.angle = np.array([x, y]), r
  57. sin, cos = np.sin(r), np.cos(r)
  58. self.xs = max(cos * self.HalfLong, sin * self.HalfShort)
  59. self.ys = max(sin * self.HalfLong, cos * self.HalfShort)
  60. def move(self, speed: "int" = 5):
  61. self.center[0] -= speed
  62. def isVisible(self):
  63. return self.center[0] + self.HalfLong > 0
  64. def isOverlap(self, other: "Egg") -> "bool":
  65. distance = np.linalg.norm(self.center - other.center)
  66. angle_diff = np.abs(self.angle - other.angle)
  67. if angle_diff > np.pi:
  68. angle_diff = 2 * np.pi - angle_diff
  69. return distance <= self.Long and angle_diff <= np.pi / 2
  70. def __draw(self, can, long: "int", short: "int", color: "int"):
  71. cv2.ellipse(
  72. can,
  73. center=self.center,
  74. axes=(long, short),
  75. angle=self.angle,
  76. startAngle=0,
  77. endAngle=360,
  78. color=color,
  79. thickness=-1,
  80. )
  81. def draw(self, can):
  82. for i in range(0, self.Count):
  83. self.__draw(
  84. can,
  85. int(self.HalfLong - self.GapLong * i), int(self.HalfShort - self.GapShort * i),
  86. int(self.HalfShort + self.GapShort * i) + self.Bright
  87. )
  88. class Canvas:
  89. Width, Height = 1800, 550 # 画布宽、高
  90. DetectorHeight, Ratio = 10, Height / 11
  91. barX, barW, dW, dH = 50, 14, 14, 10
  92. SX, Speed = barX + dW + 10, 5
  93. Start, Gap = 55, 110
  94. def __init__(self):
  95. self.canvas = np.zeros((self.Height, self.Width), np.uint8)
  96. self.copy = self.canvas.copy()
  97. self.curLeft = np.random.randint(400, 550)
  98. self.eggs: "list[Egg]" = []
  99. self.sample = np.zeros(5, np.uint8)
  100. self.total, self.cur, self.real = 0, 0, 0
  101. # self.adjust_detector()
  102. def adjust_detector(self):
  103. hold = Counter.Threshold
  104. Height = (self.DetectorHeight - hold) * self.Ratio
  105. height = 2 * Height - Egg.Short
  106. eclipse = Eclipse(a=Egg.Long / 2, b=Egg.Short / 2)
  107. long2x = eclipse.inner_rect(bl=height)
  108. short2x = np.sqrt(Egg.Short ** 2 - height ** 2)
  109. self.Gap = int(long2x / 4 + short2x / 2)
  110. self.Start = (self.Height - 4 * self.Gap) // 2
  111. print(f"Threshold: {hold}, short: {short2x}, long: {long2x}, format: [{self.Start}::{self.Gap}]")
  112. def have_space(self):
  113. return self.curLeft < self.Width - Egg.Long
  114. def gen_one_col(self):
  115. seed, mx = np.random.random(), 0
  116. if seed < 0.1: # 0: 0.1
  117. mx = np.random.randint(30, 50)
  118. elif seed < 0.3: # 1: 0.2
  119. while True:
  120. half = np.random.randint(Egg.HalfShort, Egg.HalfLong) + np.random.randint(10, 40)
  121. y, r = np.random.randint(Egg.Long, self.Height - Egg.Long), np.random.randint(0, 180)
  122. egg = Egg(self.curLeft + half, y, r)
  123. if half > egg.xs:
  124. mx = 2 * half
  125. self.eggs.append(egg)
  126. break
  127. elif seed < 0.65: # 2: 0.35
  128. while True:
  129. half1 = np.random.randint(Egg.HalfShort, Egg.HalfLong) + np.random.randint(10, 40)
  130. half2 = np.random.randint(Egg.HalfShort, Egg.HalfLong) + np.random.randint(20, 50)
  131. y1 = np.random.randint(Egg.HalfLong, (self.Height - Egg.Long) / 2)
  132. y2 = np.random.randint((self.Height + Egg.Long) / 2, self.Height - Egg.HalfLong)
  133. r1, r2 = np.random.randint(0, 180), np.random.randint(0, 180)
  134. e1, e2 = Egg(self.curLeft + half1, y1, r1), Egg(self.curLeft + half2, y2, r2)
  135. if half1 > e1.xs and half2 > e2.xs and not e1.isOverlap(e2):
  136. self.eggs += [e1, e2]
  137. mx = 2 * max(half1, half2)
  138. break
  139. else: # 3: 0.35
  140. while True:
  141. half1 = np.random.randint(Egg.HalfShort, Egg.HalfLong) + np.random.randint(10, 200)
  142. half2 = np.random.randint(Egg.HalfShort, Egg.HalfLong) + np.random.randint(10, 200)
  143. half3 = np.random.randint(Egg.HalfShort, Egg.HalfLong) + np.random.randint(10, 200)
  144. y1 = np.random.randint(Egg.HalfShort, self.Height / 3 - Egg.HalfShort)
  145. y2 = np.random.randint(self.Height / 3 + Egg.HalfShort, self.Height * 2 / 3 - Egg.HalfShort)
  146. y3 = np.random.randint(self.Height * 2 / 3 + Egg.HalfShort, self.Height - Egg.HalfShort)
  147. r1, r2, r3 = np.random.randint(0, 30), np.random.randint(0, 30), np.random.randint(0, 30)
  148. e1 = Egg(self.curLeft + half1, y1, r1)
  149. e2 = Egg(self.curLeft + half2, y2, r2)
  150. e3 = Egg(self.curLeft + half3, y3, r3)
  151. if half1 > e1.xs and half2 > e2.xs and half3 > e3.xs and not e1.isOverlap(e2) and not e2.isOverlap(e3):
  152. self.eggs += [e1, e2, e3]
  153. mx = 2 * max(half1, half2, half3)
  154. break
  155. self.curLeft += mx
  156. def draw(self):
  157. self.copy = self.canvas.copy()
  158. [egg.draw(self.copy) for egg in self.eggs] # eggs
  159. # detector
  160. cv2.line(self.copy, (self.barX, 0), (self.barX, self.Height), 100, self.barW)
  161. for y in range(self.Start, self.Height, self.Gap):
  162. cv2.line(self.copy, (self.barX, y), (self.barX + self.dW, y), 255, self.dH)
  163. # info
  164. cv2.putText(
  165. img=self.copy,
  166. text=f"Total: {self.total:<4d}, Current: {self.cur}",
  167. org=(self.Width - 380, 30),
  168. fontFace=cv2.FONT_HERSHEY_PLAIN,
  169. fontScale=1.8,
  170. color=255,
  171. thickness=2
  172. )
  173. # show
  174. cv2.imshow("Egg Monitor", self.copy)
  175. cv2.waitKey(1)
  176. def inner(self):
  177. while self.have_space():
  178. self.gen_one_col() # gen_eggs
  179. while True:
  180. self.move_eggs()
  181. if self.have_space():
  182. self.gen_one_col() # gen_eggs
  183. self.draw()
  184. self.sample = self.copy[self.Start::self.Gap, self.SX]
  185. def run(self):
  186. def trans(num: "np.double"):
  187. if num > 0:
  188. num -= Egg.Bright
  189. return self.DetectorHeight - num / self.Ratio
  190. Thread(target=self.inner, name="inner").start()
  191. while True:
  192. time.sleep(0.1)
  193. yield list(map(trans, self.sample.astype(np.double)))
  194. def move_eggs(self):
  195. self.curLeft -= self.Speed
  196. [egg.move(self.Speed) for egg in self.eggs]
  197. tmp = []
  198. for egg in self.eggs:
  199. if egg.isVisible():
  200. tmp.append(egg)
  201. else:
  202. self.real += 1
  203. self.eggs = tmp
  204. class Status:
  205. Names = ["准备", "主态", "辅态", "真空"]
  206. def __init__(self, sta: "np.unint8" = 0):
  207. self.sta, self.num, self.paired = sta, np.uint8(0), False
  208. def has_data(self) -> "bool":
  209. return self.sta == 1 or self.sta == 2
  210. def update(self, *, sta=None, num=None, paired: "bool" = None):
  211. if sta is not None:
  212. self.sta = sta
  213. if num is not None:
  214. self.num = num
  215. if paired is not None:
  216. self.paired = paired
  217. @property
  218. def name(self) -> "str":
  219. return self.Names[self.sta]
  220. class Counter:
  221. """
  222. statements:
  223. Short, Long: 鸡蛋长短轴长
  224. Short', Long': 检测阈值所在的等比缩放鸡蛋长短轴长
  225. hold: 检测阈值
  226. distance: 相邻检测器的距离
  227. margin: 边缘检测器与传送带边缘的距离
  228. require:
  229. 1. distance < Short', 2*distance > Long'
  230. 2. !!!: 3*distance = 2*Short'+(Short-Short')
  231. 3. 检测器距离要小于检测鸡蛋的短轴长,防止漏数;
  232. 同一个鸡蛋仅能被1、2个检测器检测到;
  233. 两个鸡蛋并排时,必须被3、4个检测器检测到,防止两个鸡蛋被认作一个鸡蛋;
  234. default:
  235. hold: 8.2 cm
  236. distance: 105 px
  237. margin: 65 px
  238. Short': 140 px
  239. Long': 200px
  240. history:
  241. 8.0, 25, 125: 99.96%
  242. 7.25, 29, 123: 99.7%
  243. 7.09, 51, 112:
  244. 7.0, 67, 104: 99.91%
  245. """
  246. Threshold = 8.0 # => Threshold: 7.25, short: 143.61406616345073, long: 205.1629364490509, format: [29::123]
  247. def __init__(self):
  248. self.total: "np.uint16" = np.uint16(0)
  249. self.current: "np.uint8" = np.uint8(0)
  250. self.status = [Status(), Status(), Status(), Status(), Status()]
  251. def detected(self, dis: "np.double") -> bool:
  252. return dis < self.Threshold
  253. @staticmethod
  254. def neighbors(idx: "int", mm: "int" = 4) -> "list[int]":
  255. if idx == 0:
  256. return [1]
  257. if idx == mm:
  258. return [mm - 1]
  259. return [idx - 1, idx + 1]
  260. def __call__(self, sample: "list", nums: "np.uint8" = 5) -> "tuple[np.uint16, np.uint8]": # magic
  261. for i in range(nums):
  262. nears = self.neighbors(i, nums - 1)
  263. if self.detected(sample[i]):
  264. if self.status[i].sta == 0: # di上次无数据,本次检测到鸡蛋
  265. joined = False
  266. for ni in nears:
  267. if self.status[ni].sta == 1 and not self.status[ni].paired: # 有邻居是未配对的主数据,加入该邻居
  268. self.status[i].update(sta=2, num=self.status[ni].num, paired=True)
  269. self.status[ni].paired = True
  270. joined = True
  271. break
  272. if not joined:
  273. self.total += 1
  274. self.current += 1
  275. self.status[i].update(sta=1, num=self.total, paired=False)
  276. elif self.status[i].has_data(): # di.sta=1/2,di已经是主数据或辅数据
  277. pass
  278. else: # di.sta=3,di上次为真空期,本次di为新主
  279. self.total += 1
  280. self.current += 1
  281. self.status[i].update(sta=1, num=self.total, paired=False)
  282. else: # di本次无数据
  283. if self.status[i].sta == 0: # di之前无数据
  284. pass
  285. elif self.status[i].paired: # di之前可能存在数据,且已经配对
  286. have = False
  287. for ni in nears:
  288. if self.status[ni].num == self.status[i].num: # 该邻居与di为同一个鸡蛋
  289. have = True
  290. if self.status[ni].has_data(): # dni.sta=1/2,邻居未进入真空状态
  291. self.status[i].sta = 3
  292. elif self.status[ni].sta == 3: # 邻居已经是真空状态
  293. self.current -= 1
  294. self.status[i].update(sta=0, paired=False)
  295. self.status[ni].update(sta=0, paired=False)
  296. if not have: # 配对的邻居已经进入下一个鸡蛋
  297. self.current -= 1
  298. self.status[i].update(sta=0, paired=False)
  299. else:
  300. self.current -= 1
  301. self.status[i].update(sta=0, paired=False)
  302. return self.total, self.current
  303. def main():
  304. canvas = Canvas()
  305. # C, accuracy: 99.96%
  306. dll = ctypes.CDLL("./EggCounter.dll")
  307. dll.CreateWorld.argtypes = [ctypes.c_uint8, ctypes.c_double]
  308. dll.CreateWorld.restype = ctypes.c_void_p
  309. dll.CountByData.argtypes = [
  310. ctypes.c_void_p, ctypes.POINTER(ctypes.c_double),
  311. ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint8)
  312. ]
  313. dll.CountByData.restype = None
  314. counter = dll.CreateWorld(5, 8.0)
  315. total = ctypes.c_uint(0)
  316. cur = ctypes.c_uint8(0)
  317. for sample in canvas.run():
  318. sample_arr = (ctypes.c_double * 5)(*sample)
  319. dll.CountByData(counter, sample_arr, total, cur)
  320. canvas.total, canvas.cur = total.value, cur.value
  321. print(
  322. f"[{sample[0]:<8.2f}, {sample[1]:<8.2f}, {sample[2]:<8.2f}, {sample[3]:<8.2f}, {sample[4]:<8.2f}]"
  323. f" ==> Real: {canvas.real:<3d}, Total: {canvas.total:<3d}, Current: {canvas.cur:<2d}"
  324. )
  325. # python
  326. # counter = Counter()
  327. # for sample in canvas.run():
  328. # # count = counter.call(*sample)
  329. # canvas.total, canvas.cur = counter(sample)
  330. # print(
  331. # f"[{sample[0]:<8.2f}, {sample[1]:<8.2f}, {sample[2]:<8.2f}, {sample[3]:<8.2f}, {sample[4]:<8.2f}]"
  332. # f" ==> Total: {canvas.total:<3d}, Current: {canvas.cur:<2d}"
  333. # )
  334. if __name__ == "__main__":
  335. main()