auto-trainer-core 2.0.1.dev353__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. auto_trainer_core-2.0.1.dev353.dist-info/METADATA +18 -0
  2. auto_trainer_core-2.0.1.dev353.dist-info/RECORD +73 -0
  3. auto_trainer_core-2.0.1.dev353.dist-info/WHEEL +5 -0
  4. auto_trainer_core-2.0.1.dev353.dist-info/top_level.txt +1 -0
  5. autotrainer/core/__init__.py +319 -0
  6. autotrainer/core/analysis/__init__.py +6 -0
  7. autotrainer/core/analysis/alarm_monitor.py +484 -0
  8. autotrainer/core/analysis/audio_spectrum_data.py +27 -0
  9. autotrainer/core/analysis/audio_spectrum_monitor.py +134 -0
  10. autotrainer/core/analysis/auto_tunnel_fan_monitor.py +92 -0
  11. autotrainer/core/analysis/detector.py +179 -0
  12. autotrainer/core/analysis/external_doors_monitor.py +124 -0
  13. autotrainer/core/analysis/global_animal_presence_monitor.py +69 -0
  14. autotrainer/core/analysis/head_fix_measurement.py +15 -0
  15. autotrainer/core/analysis/headbar_pressure_monitor.py +158 -0
  16. autotrainer/core/analysis/load_cell_monitor.py +377 -0
  17. autotrainer/core/analysis/load_cell_tare_monitor.py +217 -0
  18. autotrainer/core/analysis/measurement_data.py +80 -0
  19. autotrainer/core/analysis/pellet_position_monitor.py +108 -0
  20. autotrainer/core/analysis/sensor_analysis.py +390 -0
  21. autotrainer/core/analysis/system_fault_monitor.py +81 -0
  22. autotrainer/core/analysis/system_maintenance_monitor.py +88 -0
  23. autotrainer/core/animal/__init__.py +1 -0
  24. autotrainer/core/animal/animal_subject.py +208 -0
  25. autotrainer/core/capture.py +25 -0
  26. autotrainer/core/configuration/__init__.py +88 -0
  27. autotrainer/core/configuration/alarm_configuration.py +55 -0
  28. autotrainer/core/configuration/animal_presence_configuration.py +17 -0
  29. autotrainer/core/configuration/behavior_configuration.py +323 -0
  30. autotrainer/core/configuration/camera_configuration.py +108 -0
  31. autotrainer/core/configuration/external_doors_monitor_configuration.py +9 -0
  32. autotrainer/core/configuration/hardware_configuration.py +30 -0
  33. autotrainer/core/configuration/inference_configuration.py +26 -0
  34. autotrainer/core/configuration/load_cell_config.py +61 -0
  35. autotrainer/core/configuration/persistence_configuration.py +29 -0
  36. autotrainer/core/configuration/presence_detection_configuration.py +9 -0
  37. autotrainer/core/configuration/system_configuration.py +211 -0
  38. autotrainer/core/configuration/system_fault_config.py +9 -0
  39. autotrainer/core/configuration/system_maintenance_config.py +17 -0
  40. autotrainer/core/diamond_triangle_config.py +150 -0
  41. autotrainer/core/event/__init__.py +54 -0
  42. autotrainer/core/event/api_event_kind.py +5 -0
  43. autotrainer/core/event/api_event_plugin.py +57 -0
  44. autotrainer/core/event/event_info.py +53 -0
  45. autotrainer/core/event/event_manager.py +296 -0
  46. autotrainer/core/event/event_manager_plugin.py +45 -0
  47. autotrainer/core/event/file_event_plugin.py +91 -0
  48. autotrainer/core/event/logger_event_plugin.py +43 -0
  49. autotrainer/core/fixed_array_multiqueue.py +263 -0
  50. autotrainer/core/fixed_array_queue.py +124 -0
  51. autotrainer/core/frame_index.py +34 -0
  52. autotrainer/core/interfaces.py +347 -0
  53. autotrainer/core/logging.py +714 -0
  54. autotrainer/core/message/__init__.py +26 -0
  55. autotrainer/core/message/audio_spectrum_message.py +21 -0
  56. autotrainer/core/message/measurement_message.py +61 -0
  57. autotrainer/core/message/message_handler.py +158 -0
  58. autotrainer/core/message/motor_configuration_message.py +128 -0
  59. autotrainer/core/message/system_command_kind.py +74 -0
  60. autotrainer/core/message/system_message_handler.py +122 -0
  61. autotrainer/core/message/system_status_message.py +112 -0
  62. autotrainer/core/multiproc.py +50 -0
  63. autotrainer/core/notification/__init__.py +2 -0
  64. autotrainer/core/notification/notification_center.py +80 -0
  65. autotrainer/core/notification/trigger_notification.py +27 -0
  66. autotrainer/core/observable_object.py +105 -0
  67. autotrainer/core/perf_monitor.py +77 -0
  68. autotrainer/core/pose_elements.py +196 -0
  69. autotrainer/core/project/__init__.py +19 -0
  70. autotrainer/core/project/project_info.py +486 -0
  71. autotrainer/core/queue_util.py +60 -0
  72. autotrainer/core/reach_event.py +38 -0
  73. autotrainer/core/video_detection.py +326 -0
@@ -0,0 +1,326 @@
1
+ import collections
2
+ import csv
3
+ import ctypes
4
+ import dataclasses
5
+ import math
6
+ import statistics
7
+ import os
8
+ import threading
9
+ import time
10
+ from multiprocessing.sharedctypes import Synchronized
11
+ from pathlib import Path
12
+ from datetime import datetime
13
+ from typing import Deque, Tuple, List
14
+
15
+ import cv2
16
+ import numpy
17
+ import numpy as np
18
+ from typing_extensions import Self
19
+
20
+ from autotrainer.core import ValueHolderDescriptor, get_perf_now, RawValueHolder
21
+ from autotrainer.core.configuration.presence_detection_configuration import PresenceDetectionConfig
22
+ from autotrainer.core.project.project_info import ProjectInfo
23
+ from autotrainer.core.logging import get_verbose_logger
24
+ from autotrainer.core.multiproc import get_mp_ctx, EmptyWithContext
25
+
26
+ logger = get_verbose_logger(__name__)
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class PresenceDetectionAttrs:
31
+
32
+ pc_threshold = ValueHolderDescriptor()
33
+ # the fg mask sum (as percent vs max(100)) threshold above which the presence is assumed
34
+ _pc_threshold: Synchronized = None
35
+
36
+ pc_high_exclude_threshold = ValueHolderDescriptor()
37
+ # but if above this exclude threshold then don't trigger.
38
+ _pc_high_exclude_threshold: Synchronized = None
39
+
40
+ mask_lower_zero = ValueHolderDescriptor()
41
+ _mask_lower_zero: Synchronized = None
42
+ # zero all values in the frame below this value
43
+
44
+ max_delay_skip_threshold = ValueHolderDescriptor()
45
+ _max_delay_skip_threshold: Synchronized = None
46
+ # remove frames older than this only. all frames within this delay will be taken into account
47
+
48
+ # could be todo: allow compare with one frame with the xth previous one (second, or third, for instance),
49
+ # not only the very next one, that would/could allow detect slower movement/presence
50
+
51
+ last_absence_start_perf_c = ValueHolderDescriptor()
52
+ _last_absence_start_perf_c: Synchronized = None
53
+
54
+ last_presence_start_perf_c = ValueHolderDescriptor()
55
+ _last_presence_start_perf_c: Synchronized = None
56
+
57
+ presence_detected = ValueHolderDescriptor()
58
+ _presence_detected: Synchronized = None
59
+
60
+ movement_detected = ValueHolderDescriptor()
61
+ _movement_detected: Synchronized = None
62
+
63
+ pc_sum = ValueHolderDescriptor()
64
+ _pc_sum: Synchronized = None
65
+
66
+ def __post_init__(self):
67
+ ctx = get_mp_ctx()
68
+ lock = ctx.RLock()
69
+ used = False
70
+ if self._pc_threshold is None:
71
+ used = True
72
+ self._pc_threshold = ctx.Value(ctypes.c_double, PresenceDetectionConfig.pc_threshold, lock=lock)
73
+ if self._pc_high_exclude_threshold is None:
74
+ used = True
75
+ self._pc_high_exclude_threshold = ctx.Value(ctypes.c_double, PresenceDetectionConfig.pc_high_exclude_threshold, lock=lock)
76
+ if self._mask_lower_zero is None:
77
+ used = True
78
+ self._mask_lower_zero = ctx.Value(ctypes.c_double, PresenceDetectionConfig.mask_lower_zero, lock=lock)
79
+ if self._max_delay_skip_threshold is None:
80
+ used = True
81
+ self._max_delay_skip_threshold = ctx.Value(ctypes.c_double, PresenceDetectionConfig.max_delay_skip_threshold, lock=lock)
82
+ if self._last_absence_start_perf_c is None:
83
+ used = True
84
+ self._last_absence_start_perf_c = ctx.Value(ctypes.c_double, -math.inf, lock=lock)
85
+ if self._last_presence_start_perf_c is None:
86
+ used = True
87
+ self._last_presence_start_perf_c = ctx.Value(ctypes.c_double, -math.inf, lock=lock)
88
+ if self._presence_detected is None:
89
+ used = True
90
+ self._presence_detected = ctx.Value(ctypes.c_bool, False, lock=lock)
91
+ if self._movement_detected is None:
92
+ used = True
93
+ self._movement_detected = ctx.Value(ctypes.c_bool, False, lock=lock)
94
+ if self._pc_sum is None:
95
+ used = True
96
+ self._pc_sum = ctx.Value(ctypes.c_double, 0, lock=lock)
97
+ if not used:
98
+ lock = EmptyWithContext()
99
+ self._lock = lock
100
+
101
+ def _loop_over_value_holder_desc(self):
102
+ cls = self.__class__
103
+ return (
104
+ a
105
+ for a in (getattr(cls, k) for k in dir(cls))
106
+ if isinstance(a, ValueHolderDescriptor)
107
+ )
108
+
109
+ def __eq__(self, other):
110
+ return all(getattr(self, a.name) == getattr(other, a.name)
111
+ for a in self._loop_over_value_holder_desc())
112
+
113
+ @property
114
+ def lock(self):
115
+ return self._lock
116
+
117
+ def to_local_value(self) -> Self:
118
+ """Detach from the shared values"""
119
+ with self._lock:
120
+ dct = {
121
+ f"_{a.name}": RawValueHolder(getattr(self, a.name))
122
+ for a in self._loop_over_value_holder_desc()
123
+ }
124
+ return self.__class__(**dct)
125
+
126
+ def to_config(self) -> PresenceDetectionConfig:
127
+ return PresenceDetectionConfig(**{
128
+ field.name: getattr(self, field.name)
129
+ for field in dataclasses.fields(PresenceDetectionConfig)
130
+ })
131
+
132
+ def load_config(self, cfg: PresenceDetectionConfig):
133
+ for field in dataclasses.fields(cfg):
134
+ setattr(self, field.name, getattr(cfg, field.name))
135
+
136
+
137
+ class VideoDetection(threading.Thread):
138
+
139
+ def __init__(self, project_info: ProjectInfo, detection_attrs: PresenceDetectionAttrs):
140
+ self._project_info = project_info
141
+ self._attrs = detection_attrs
142
+ self._stop_requested = False
143
+ # using a deque with 8 entries,
144
+ # NB: is thread-safe for append/popleft. see (c)python doc.
145
+ self._next_frames: Deque[Tuple[float, numpy.ndarray, float]] = collections.deque(maxlen=10)
146
+ # not sure using a simple thread queue.Queue is not as good, possibly better (can wait on it)
147
+ self._prev_frame = None
148
+ self._prev_when = None
149
+ self._csv_header = ["Time", "Index", "PercentSum", "Motion"]
150
+ self._file_info = None
151
+ self._csv_writer = None
152
+ self._csv_writer_fh = None
153
+ self._got_first_frame = False
154
+ super().__init__(name="PresenceDetection", daemon=True)
155
+
156
+ def cancel(self):
157
+ self._stop_requested = True
158
+
159
+ def update_frame(self, when: float, frame: numpy.ndarray, perf_c: float):
160
+ if not self._got_first_frame:
161
+ self._got_first_frame = True
162
+ perf_now = get_perf_now()
163
+ self._attrs.last_absence_start_perf_c = perf_now
164
+ self._attrs.last_presence_start_perf_c = perf_now
165
+ # this allows to get good measurement from monitors using the detection result(s)
166
+ self._next_frames.append((when, frame, perf_c))
167
+
168
+ def _check_path(self):
169
+ csv_file_info = self._project_info.get_webcam_presence_file(when=datetime.now())
170
+ if csv_file_info == self._file_info:
171
+ return
172
+ if self._csv_writer_fh is not None:
173
+ self._csv_writer_fh.flush()
174
+ self._csv_writer_fh.close()
175
+ if csv_file_info is None:
176
+ self._csv_writer_fh = self._csv_writer = None
177
+ else:
178
+ f_path = Path(csv_file_info.file)
179
+ logger.verbose("Switching to %s", f_path)
180
+ prev_exist = f_path.is_file() and f_path.exists() and f_path.stat().st_size > 0
181
+ self._csv_writer_fh = f_path.open("a")
182
+ self._csv_writer = csv.DictWriter(self._csv_writer_fh, self._csv_header)
183
+ if not prev_exist:
184
+ self._csv_writer.writeheader()
185
+ self._file_info = csv_file_info
186
+
187
+ def run(self):
188
+ try:
189
+ self._run()
190
+ finally:
191
+ fh = self._csv_writer_fh
192
+ if fh is not None:
193
+ fh.flush()
194
+ fh.close()
195
+
196
+ def _run(self):
197
+ attrs = self._attrs
198
+ prev_pc_threshold = attrs.pc_threshold
199
+ prev_gray_frame = prev_frame_perf_c = None
200
+ prev_detected = None # attrs.presence_detected.value
201
+ prev_pc_sum = None
202
+ row_dict = dict.fromkeys(self._csv_header)
203
+ next_log_report = time.perf_counter() + 5
204
+ processed_count = 0
205
+ expired_count = 0
206
+ show_report = os.getenv("PRESENCE_DETECTION_SHOW_REPORT", "").lower() in {"y", "yes", "true", "1"}
207
+ delay_report = int(os.getenv("PRES_DET_DELAY_REPORT", "180"))
208
+ processed_times: List[float] = []
209
+ prev_pc_values = []
210
+ hist_values: List[Tuple[float, np.ndarray, float, float]] = []
211
+ # when, gray_frame, pc_norm, pc_unnorm
212
+ def update_hist(w, new_frame, pc_norm, pc_unnorm):
213
+ idx = len(hist_values) - 1
214
+ while idx >= 0:
215
+ w2 = hist_values[idx][0]
216
+ if w - w2 >= attrs.max_delay_skip_threshold:
217
+ del hist_values[:idx + 1] # need + 1 with range (:+1)
218
+ break
219
+ idx -= 1
220
+ hist_values.append((w, new_frame, pc_norm, pc_unnorm))
221
+ #
222
+ last_log_report = time.perf_counter()
223
+ while not self._stop_requested:
224
+ perf_now = time.perf_counter()
225
+ loop_start_perf_now = perf_now
226
+ if show_report and perf_now >= next_log_report:
227
+ actual_delay = perf_now - last_log_report
228
+ logger.debug("video presence detection: %s, frame/s=%.1f mean_proc_time=%.3f ; expired_count=%.1f/s ; values=%s",
229
+ prev_pc_values[-1][0] if len(prev_pc_values) > 0 else math.nan,
230
+ processed_count / actual_delay,
231
+ statistics.mean(processed_times) if processed_times else math.nan,
232
+ expired_count / actual_delay, prev_pc_values)
233
+ last_log_report = perf_now
234
+ next_log_report = perf_now + delay_report
235
+ processed_count = expired_count = 0
236
+ prev_pc_values.clear()
237
+ processed_times.clear()
238
+ try:
239
+ when_nanos, frame, frame_perf_c = self._next_frames.popleft()
240
+ except IndexError:
241
+ time.sleep(0.003) # current producer is 30 fps. let's use very smallish sleep
242
+ continue
243
+ #
244
+ when = when_nanos / 1e9 # convert to second timestamp
245
+ #
246
+ if frame.ndim > 2:
247
+ gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
248
+ else:
249
+ gray_frame = frame
250
+ # given we divide by fg_mask.size below to calculate a % value,
251
+ # it does not really matter to rescale to any size:
252
+ # gray_frame = cv2.resize(gray_frame, (256, 256), interpolation=cv2.INTER_LINEAR)
253
+ save_prev_perf_c = prev_frame_perf_c
254
+ save_prev_gray_frame = prev_gray_frame
255
+ prev_frame_perf_c = when
256
+ prev_gray_frame = gray_frame
257
+ if save_prev_gray_frame is None:
258
+ # (re)init frame
259
+ continue
260
+ if frame_perf_c - save_prev_perf_c >= attrs.max_delay_skip_threshold:
261
+ expired_count += 1
262
+ continue
263
+ # work on frame
264
+ cur_pc_threshold = attrs.pc_threshold
265
+ if cur_pc_threshold != prev_pc_threshold:
266
+ logger.debug("Using new pc_threshold: %s", cur_pc_threshold)
267
+ prev_pc_threshold = cur_pc_threshold
268
+ processed_count += 1
269
+ fg_mask = cv2.absdiff(save_prev_gray_frame, gray_frame)
270
+ fg_mask[fg_mask < attrs.mask_lower_zero] = 0
271
+ tot_sum_unnormalized = numpy.sum(fg_mask)
272
+ fg_mask[fg_mask >= attrs.mask_lower_zero] = 1
273
+ tot_sum_normalized = numpy.sum(fg_mask)
274
+ pc_normalized = 100 * tot_sum_normalized / fg_mask.size
275
+ pc_normalized = round(pc_normalized, 1)
276
+ # NB: not using the unnormalized value atm:
277
+ pc_unnormalized = (100 * tot_sum_unnormalized) / (fg_mask.size * 8 ** fg_mask.itemsize)
278
+ pc_unnormalized = round(pc_unnormalized, 1)
279
+ update_hist(when, gray_frame, pc_normalized, pc_unnormalized)
280
+ if pc_normalized != prev_pc_sum:
281
+ # NB: we have:
282
+ # fg_mask.size == 2073600 and fg_mask.itemsize == 1
283
+ if __debug__:
284
+ if pc_normalized is None or prev_pc_sum is None or round(pc_normalized, 0) != round(prev_pc_sum, 0):
285
+ logger.spam("pc_norm=%.2f pc_unnorm=%.2f hist=%s",
286
+ pc_normalized, pc_unnormalized, [(d1, d2, d3) for d1, _, d2, d3 in hist_values])
287
+ attrs.pc_sum = pc_normalized
288
+ prev_pc_sum = pc_normalized
289
+ #
290
+ cur_exclude_threshold = attrs.pc_high_exclude_threshold
291
+ is_detected = (
292
+ cur_pc_threshold <= pc_normalized < cur_exclude_threshold
293
+ )
294
+ self._check_path()
295
+ csv_writer = self._csv_writer
296
+ if csv_writer is not None:
297
+ row_dict.update(
298
+ Time=when,
299
+ Index=int(frame_perf_c * 1e9), # to be consistent with others csv data files using Time/Index
300
+ PercentSum=pc_normalized,
301
+ Motion=int(is_detected),
302
+ )
303
+ csv_writer.writerow(row_dict)
304
+ # now use everything available in hist_values:
305
+ is_detected = (
306
+ any(pc_normalized >= cur_pc_threshold
307
+ for _, _, pc_normalized, _ in hist_values)
308
+ ) and not (
309
+ any(pc_normalized >= cur_exclude_threshold
310
+ for _, _, pc_normalized, _ in hist_values)
311
+ )
312
+ if is_detected != prev_detected:
313
+ logger.debug("presence detected: %.1f - %.1f ; hist=%s",
314
+ pc_normalized, pc_unnormalized, [(d1, d2, d3) for d1, _, d2, d3 in hist_values])
315
+ prev_detected = is_detected
316
+ with attrs.lock: # use lock to really ensure consistency
317
+ attrs.presence_detected = is_detected
318
+ if is_detected:
319
+ attrs.last_presence_start_perf_c = frame_perf_c
320
+ else:
321
+ attrs.last_absence_start_perf_c = frame_perf_c
322
+ prev_gray_frame = None # this will make us to get the following necessary next frames for next check
323
+ if show_report:
324
+ prev_pc_values.append((pc_normalized, pc_unnormalized))
325
+ processed_times.append(time.perf_counter() - loop_start_perf_now)
326
+ # end while not self._stop_requested