소스 검색

added HD tracking, need more testing

asobolev 1 년 전
부모
커밋
ed9abfea64
3개의 변경된 파일212개의 추가작업 그리고 618개의 파일을 삭제
  1. 52 70
      SIT.ipynb
  2. 138 532
      controllers/position.ipynb
  3. 22 16
      profiles/default.json

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 52 - 70
SIT.ipynb


+ 138 - 532
controllers/position.ipynb

@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 38,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -15,13 +15,22 @@
     "import matplotlib.pyplot as plt\n",
     "import multiprocessing as mp\n",
     "\n",
-    "from scipy import stats\n",
+    "from scipy import stats, signal\n",
     "from situtils import FPSTimes"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 93,
+   "execution_count": 60,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# TODO - write an Agent class and unify position/HD tracking for multiple agents"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 56,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -135,27 +144,37 @@
     "    \n",
     "    @property\n",
     "    def positions_in_px(self):\n",
+    "        # a list of pairs [(Xi, Yi), ...] of actual positions for each agent tracked - in pixels\n",
     "        return NotImplemented\n",
     "    \n",
     "    @property\n",
     "    def positions_in_m(self):\n",
+    "        # a list of pairs [(Xi, Yi), ...] of actual positions for each agent tracked - in meters\n",
     "        return NotImplemented\n",
     "    \n",
     "    @property\n",
     "    def contours(self):\n",
+    "        return NotImplemented\n",
+    "    \n",
+    "    @property\n",
+    "    def speeds(self):\n",
+    "        return NotImplemented\n",
+    "    \n",
+    "    @property\n",
+    "    def hds(self):\n",
     "        return NotImplemented"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 96,
+   "execution_count": 57,
    "metadata": {},
    "outputs": [],
    "source": [
-    "class PositionTrackerSingle(PositionTrackerBase):\n",
+    "class PositionTrackerSingleNOHD(PositionTrackerBase):\n",
     "    \n",
     "    def __init__(self, status, video_stream, cfg):\n",
-    "        super(PositionTrackerSingle, self).__init__(status, video_stream, cfg)\n",
+    "        super(PositionTrackerSingleNOHD, self).__init__(status, video_stream, cfg)\n",
     "        \n",
     "        self._position = None\n",
     "        self._contour = None\n",
@@ -242,7 +261,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 97,
+   "execution_count": 58,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -370,374 +389,44 @@
     "        \n",
     "    @property\n",
     "    def contours(self):  # always in pixels\n",
-    "        return [self._contour1, self._contour2] if self._contour1 is not None else None"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 91,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "class ZZDoublePositionTracker(FPSTimes):\n",
-    "\n",
-    "    default_cfg = {\n",
-    "        \"background_light\": \"background_light.png\",\n",
-    "        \"background_dark\": \"background_dark.png\",\n",
-    "        \"threshold_light\": 60,\n",
-    "        \"threshold_dark\": 30,\n",
-    "        \"min_blob_size\": 100,\n",
-    "        \"subtract\": 1,\n",
-    "        \"arena_x\": 522,\n",
-    "        \"arena_y\": 372,\n",
-    "        \"arena_radius\": 330,\n",
-    "        \"floor_radius\": 287,\n",
-    "        \"max_fps\": 50,\n",
-    "        \"file_path\": \"positions.csv\",\n",
-    "        \"contour_path\": \"contours.csv\",\n",
-    "        \"floor_r_in_meters\": 0.46,\n",
-    "        \"angle_compensation\": -90,\n",
-    "        \"flip_x\": True,\n",
-    "        \"flip_y\": False\n",
-    "    }\n",
-    "    \n",
-    "    def __init__(self, status, video_stream, cfg):\n",
-    "        super(DoublePositionTracker, self).__init__()\n",
-    "        \n",
-    "        self.status = status\n",
-    "        self.cfg = cfg\n",
-    "        self.video_stream = video_stream\n",
-    "        self.bg_light = cv2.imread(cfg['background_light'], 1)\n",
-    "        self.bg_dark  = cv2.imread(cfg['background_dark'], 1)\n",
-    "        self.background = self.bg_light  # light by default\n",
-    "        self.is_light = True\n",
-    "        self.positions_list_1, self.positions_list_2 = None, None\n",
-    "        self.dist_array = []\n",
-    "        self.pixel_size = cfg['floor_r_in_meters'] / float(cfg['floor_radius'])\n",
-    "        self.contour1, self.contour2 = [], []\n",
-    "        self.lr = None  # linear regression of the contour\n",
-    "        self.stopped = False\n",
-    "\n",
-    "        self.mask = np.zeros(shape=self.background.shape, dtype=\"uint8\")\n",
-    "        cv2.circle(self.mask, (cfg['arena_x'], cfg['arena_y']), cfg['arena_radius'], (255,255,255), -1)\n",
-    "\n",
-    "        with open(cfg['file_path'], 'w') as f:\n",
-    "            f.write(\"time,x1,y1,x2,y2\\n\")\n",
-    "        with open(cfg['contour_path'], 'w') as f:\n",
-    "            f.write(\"x:y,...\\n\")\n",
-    "\n",
-    "    def reload_background(self):\n",
-    "        self.bg_light = cv2.imread(self.cfg['background_light'], 1)\n",
-    "        self.bg_dark  = cv2.imread(self.cfg['background_dark'], 1)\n",
-    "        self.background = self.bg_light if self.is_light else self.bg_dark\n",
-    "        print('Position tracker - background reloaded')\n",
-    "        \n",
-    "    def px_to_meters(self, x, y):\n",
-    "        x_m = float(self.cfg['arena_x'] - x) * self.pixel_size * (-1 if self.cfg['flip_x'] else 1)\n",
-    "        y_m = float(self.cfg['arena_y'] - y) * self.pixel_size * (-1 if self.cfg['flip_y'] else 1)\n",
-    "        return x_m, y_m\n",
-    "\n",
-    "    def meters_to_px(self, x, y):\n",
-    "        x_m = self.cfg['arena_x'] - (x / self.pixel_size) * (-1 if self.cfg['flip_x'] else 1)\n",
-    "        y_m = self.cfg['arena_y'] - (y / self.pixel_size) * (-1 if self.cfg['flip_y'] else 1)\n",
-    "        return int(x_m), int(y_m)\n",
-    "\n",
-    "    def correct_angle(self, phi):\n",
-    "        return (2*np.pi - phi) + np.deg2rad(self.cfg['angle_compensation'])\n",
-    "    \n",
-    "    def start(self):\n",
-    "        self._th = threading.Thread(target=self.update, args=())\n",
-    "        self._th.start()\n",
-    "    \n",
-    "    def stop(self):\n",
-    "        self.stopped = True\n",
-    "        self._th.join()\n",
-    "        print('Position tracker stopped')\n",
-    "        \n",
-    "    def update(self):\n",
-    "        next_frame = time.time() + 1.0/self.cfg['max_fps']\n",
-    "        \n",
-    "        while not self.stopped:\n",
-    "            frame = self.video_stream.read()\n",
-    "            if frame is None:\n",
-    "                time.sleep(0.05)\n",
-    "                continue\n",
-    "                \n",
-    "            if time.time() < next_frame:\n",
-    "                time.sleep(0.001)\n",
-    "                continue\n",
-    "                \n",
-    "            self.count()  # count FPS\n",
-    "            self.detect_position(frame)\n",
-    "            next_frame += 1.0/self.cfg['max_fps']\n",
-    "\n",
-    "            if self.status.value == 2 and self.positions_list_1 is not None:\n",
-    "                with open(self.cfg['file_path'], 'a') as f:  # save position\n",
-    "                    xy1 = self.xy1_in_m\n",
-    "                    xy2 = self.xy2_in_m\n",
-    "                    f.write(\",\".join([str(x) for x in (self.frame_times[-1], \\\n",
-    "                               round(xy1[0], 4), round(xy1[1], 4), round(xy2[0], 4), round(xy2[1], 4))]) + \"\\n\")         \n",
-    "                    \n",
-    "                # TODO save contours\n",
-    "                #if not len(self.contour) > 0:\n",
-    "                #    print('No contours')\n",
-    "                #    continue\n",
-    "                #ctr_in_m = np.array([self.px_to_meters(x, y) for x, y in zip(self.contour[:, 0, 0], self.contour[:, 0, 1])])\n",
-    "                #data = [\"%.4f:%4f\" % (x[0], x[1]) for x in ctr_in_m]\n",
-    "                #with open(self.cfg['contour_path'], 'a+') as f:    \n",
-    "                #    f.write(\",\".join(data) + \"\\n\")                   \n",
-    "       \n",
-    "    def switch_background(self):\n",
-    "        self.background = self.bg_dark if self.is_light else self.bg_light \n",
-    "        self.is_light = not self.is_light\n",
-    "        \n",
-    "    def detect_position(self, frame):\n",
-    "        masked_frame = cv2.bitwise_and(src1=frame, src2=self.mask)\n",
-    "\n",
-    "        # Substracts background from current frame or takes absdiff\n",
-    "        if 'subtract' in self.cfg:\n",
-    "            if self.cfg['subtract'] > 0:\n",
-    "                subject = cv2.subtract(self.background, masked_frame)\n",
-    "            else:\n",
-    "                subject = cv2.subtract(self.background, masked_frame)\n",
-    "        else:\n",
-    "            subject = cv2.absdiff(masked_frame, self.background)\n",
-    "\n",
-    "        # Converts subject to grey scale\n",
-    "        subject_gray = cv2.cvtColor(subject, cv2.COLOR_BGR2GRAY)\n",
-    "\n",
-    "        # Applies blur and thresholding to the subject\n",
-    "        kernel_size = (25,25)\n",
-    "        frame_blur = cv2.GaussianBlur(subject_gray, kernel_size, 0)\n",
-    "        th = self.cfg['threshold_light'] if self.is_light else self.cfg['threshold_dark']\n",
-    "        _, thresh = cv2.threshold(frame_blur, th, 255, cv2.THRESH_BINARY)\n",
-    "\n",
-    "        # Finds contours and selects the contour with the largest area\n",
-    "        contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)\n",
-    "\n",
-    "        # filter by size\n",
-    "        blobs = [cnt for cnt in contours if cv2.contourArea(cnt) > self.cfg['min_blob_size']]\n",
-    "        if len(blobs) == 0:  # do nothing if nothing is identified\n",
-    "            return\n",
-    "        \n",
-    "        sizes = [int(cv2.contourArea(blob)) for blob in blobs]\n",
-    "        blobs = [blobs[i] for i in np.argsort(sizes)][:2]  # two largest blobs\n",
-    "        \n",
-    "        if len(blobs) == 1:  # when two objects are together\n",
-    "            blobs.append(np.array(blobs[0]))\n",
-    "        \n",
-    "        #if np.random.rand() > 0.5:  # randomize for testing\n",
-    "        #    blobs[0], blobs[1] = blobs[1], blobs[0]\n",
-    "            \n",
-    "        M1 = cv2.moments(blobs[0])\n",
-    "        M2 = cv2.moments(blobs[1])\n",
-    "        if M1['m00'] == 0 or M2['m00'] == 0:\n",
-    "            return\n",
-    "        \n",
-    "        xy1 = np.array([M1['m10'] / M1['m00'], M1['m01'] / M1['m00']])  # single pair\n",
-    "        xy2 = np.array([M2['m10'] / M2['m00'], M2['m01'] / M2['m00']])  # single pair\n",
-    "\n",
-    "        if self.positions_list_1 is None:  # first detected coordinates\n",
-    "            self.positions_list_1 = np.array([xy1])  # array of XY pairs\n",
-    "            self.positions_list_2 = np.array([xy2])  # array of XY pairs\n",
-    "\n",
-    "        else:\n",
-    "            idx = 0 if len(self.positions_list_1) < 20 else 1  # append or shift position list, collecting position history\n",
-    "\n",
-    "            # compute distance matrix: current XY1 and XY2 to previous positions\n",
-    "            dist_array = []\n",
-    "            for point in (xy1, xy2):\n",
-    "                for p_list in (self.positions_list_1, self.positions_list_2):\n",
-    "                    distance = np.sqrt( ((p_list[:, 0] - point[0]).mean())**2 + \\\n",
-    "                        ((p_list[:, 1] - point[1]).mean())**2 )\n",
-    "                    dist_array.append(distance)\n",
-    "\n",
-    "            self.dist_array = dist_array\n",
-    "\n",
-    "            if np.argmin(np.array(dist_array)) < 1 or np.argmin(np.array(dist_array)) > 2: # 1 goes to 1, 2 to 2\n",
-    "                self.positions_list_1 = np.concatenate([self.positions_list_1[idx:], [xy1]])\n",
-    "                self.positions_list_2 = np.concatenate([self.positions_list_2[idx:], [xy2]])\n",
-    "                self.contour1, self.contour2 = blobs[0], blobs[1]\n",
-    "            else:  # swap\n",
-    "                self.positions_list_1 = np.concatenate([self.positions_list_1[idx:], [xy2]])\n",
-    "                self.positions_list_2 = np.concatenate([self.positions_list_2[idx:], [xy1]])\n",
-    "                self.contour1, self.contour2 = blobs[1], blobs[0]\n",
-    "        \n",
-    "    @property\n",
-    "    def xy1_in_px(self):\n",
-    "        return self.positions_list_1[-1].astype('int32') if self.positions_list_1 is not None else None\n",
-    "\n",
-    "    @property\n",
-    "    def xy2_in_px(self):\n",
-    "        return self.positions_list_2[-1].astype('int32') if self.positions_list_2 is not None else None\n",
-    "    \n",
-    "    @property\n",
-    "    def xy1_in_m(self):\n",
-    "        if self.positions_list_1 is None:\n",
-    "            return None\n",
-    "        x = (self.cfg['arena_x'] - self.positions_list_1[-1][0]) * self.pixel_size * (-1 if self.cfg['flip_x'] else 1)\n",
-    "        y = (self.cfg['arena_y'] - self.positions_list_1[-1][1]) * self.pixel_size * (-1 if self.cfg['flip_y'] else 1)\n",
-    "        return np.array([x, y])\n",
+    "        return [self._contour1, self._contour2] if self._contour1 is not None else None\n",
     "    \n",
     "    @property\n",
-    "    def xy2_in_m(self):\n",
-    "        if self.positions_list_2 is None:\n",
-    "            return None\n",
-    "        x = (self.cfg['arena_x'] - self.positions_list_2[-1][0]) * self.pixel_size * (-1 if self.cfg['flip_x'] else 1)\n",
-    "        y = (self.cfg['arena_y'] - self.positions_list_2[-1][1]) * self.pixel_size * (-1 if self.cfg['flip_y'] else 1)\n",
-    "        return np.array([x, y])\n",
-    "\n",
-    "    # generic interface\n",
-    "    \n",
-    "    @property\n",
-    "    def positions_in_px(self):\n",
-    "        return np.array([self.xy1_in_px, self.xy2_in_px]) if self.xy1_in_px is not None else None\n",
+    "    def speeds(self):\n",
+    "        # Not Implemented\n",
+    "        return [0, 0] if self.positions_in_px is not None else None\n",
     "    \n",
     "    @property\n",
-    "    def positions_in_m(self):\n",
-    "        return np.array([self.xy1_in_m, self.xy2_in_m]) if self.xy1_in_px is not None else None\n",
-    "    \n",
-    "    @property\n",
-    "    def contours(self):  # always in pixels\n",
-    "        return [self.contour1, self.contour2]\n",
-    "    \n",
-    "    def is_inside(self, x, y, r):\n",
-    "        for pos in self.positions_in_m:\n",
-    "            if (pos[0] - x)**2 + (pos[1] - y)**2 <= r**2:\n",
-    "                return True\n",
-    "        return False"
+    "    def hds(self):\n",
+    "        # Not Implemented\n",
+    "        return [0, 0] if self.positions_in_px is not None else None"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 89,
+   "execution_count": 61,
    "metadata": {},
    "outputs": [],
    "source": [
-    "class ZZPositionTracker(FPSTimes):\n",
-    "\n",
-    "    default_cfg = {\n",
-    "        \"background_light\": \"background_light.png\",\n",
-    "        \"background_dark\": \"background_dark.png\",\n",
-    "        \"threshold_light\": 60,\n",
-    "        \"threshold_dark\": 30,\n",
-    "        \"min_blob_size\": 100,\n",
-    "        \"subtract\": 1,\n",
-    "        \"arena_x\": 522,\n",
-    "        \"arena_y\": 372,\n",
-    "        \"arena_radius\": 330,\n",
-    "        \"floor_radius\": 287,\n",
-    "        \"max_fps\": 50,\n",
-    "        \"file_path\": \"positions.csv\",\n",
-    "        \"contour_path\": \"contours.csv\",\n",
-    "        \"floor_r_in_meters\": 0.46,\n",
-    "        \"angle_compensation\": -90,\n",
-    "        \"flip_x\": True,\n",
-    "        \"flip_y\": False\n",
-    "    }\n",
+    "class PositionTrackerSingle(PositionTrackerBase):\n",
     "    \n",
     "    def __init__(self, status, video_stream, cfg):\n",
-    "        super(PositionTracker, self).__init__()\n",
+    "        super(PositionTrackerSingle, self).__init__(status, video_stream, cfg)\n",
+    "        \n",
+    "        # an array of [[t1, X1, Y1, speed1, HD1], [t2, X2, Y2, speed2, HD2], ...] of 'history_duration' length\n",
+    "        # X, Y - in pixels, Speed - in m/s, head direction (HD) - in rad\n",
+    "        self._positions_list = None  \n",
+    "        self._contour = None\n",
+    "        self._lr = None  # linear regression of the contour\n",
+    "        \n",
+    "        width = 50  # 50 points ~= 1 sec with at 50Hz - to smooth the trajectory\n",
+    "        self.kernel = signal.gaussian(width, std=(width) / 7.2)\n",
     "        \n",
-    "        self.status = status\n",
-    "        self.cfg = cfg\n",
-    "        self.video_stream = video_stream\n",
-    "        self.bg_light = cv2.imread(cfg['background_light'], 1)\n",
-    "        self.bg_dark  = cv2.imread(cfg['background_dark'], 1)\n",
-    "        self.background = self.bg_light  # light by default\n",
-    "        self.is_light = True\n",
-    "        self._x, self._y = None, None\n",
-    "        self.pixel_size = cfg['floor_r_in_meters'] / float(cfg['floor_radius'])\n",
-    "        self.contour = []\n",
-    "        self.lr = None  # linear regression of the contour\n",
-    "        self.stopped = False\n",
-    "\n",
-    "        self.mask = np.zeros(shape=self.background.shape, dtype=\"uint8\")\n",
-    "        cv2.circle(self.mask, (cfg['arena_x'], cfg['arena_y']), cfg['arena_radius'], (255,255,255), -1)\n",
-    "\n",
     "        with open(cfg['file_path'], 'w') as f:\n",
     "            f.write(\"time,x,y\\n\")\n",
     "        with open(cfg['contour_path'], 'w') as f:\n",
     "            f.write(\"x:y,...\\n\")\n",
-    "\n",
-    "    def reload_background(self):\n",
-    "        self.bg_light = cv2.imread(self.cfg['background_light'], 1)\n",
-    "        self.bg_dark  = cv2.imread(self.cfg['background_dark'], 1)\n",
-    "        self.background = self.bg_light if self.is_light else self.bg_dark\n",
-    "        print('Position tracker - background reloaded')\n",
-    "        \n",
-    "    def px_to_meters(self, x, y):\n",
-    "        x_m = float(self.cfg['arena_x'] - x) * self.pixel_size * (-1 if self.cfg['flip_x'] else 1)\n",
-    "        y_m = float(self.cfg['arena_y'] - y) * self.pixel_size * (-1 if self.cfg['flip_y'] else 1)\n",
-    "        return x_m, y_m\n",
-    "\n",
-    "    def meters_to_px(self, x, y):\n",
-    "        x_m = self.cfg['arena_x'] - (x / self.pixel_size) * (-1 if self.cfg['flip_x'] else 1)\n",
-    "        y_m = self.cfg['arena_y'] - (y / self.pixel_size) * (-1 if self.cfg['flip_y'] else 1)\n",
-    "        return int(x_m), int(y_m)\n",
-    "\n",
-    "    def correct_angle(self, phi):\n",
-    "        return (2*np.pi - phi) + np.deg2rad(self.cfg['angle_compensation'])\n",
-    "    \n",
-    "    def start(self):\n",
-    "        self._th = threading.Thread(target=self.update, args=())\n",
-    "        self._th.start()\n",
-    "    \n",
-    "    def stop(self):\n",
-    "        self.stopped = True\n",
-    "        self._th.join()\n",
-    "        print('Position tracker stopped')\n",
-    "        \n",
-    "    def update(self):\n",
-    "        next_frame = time.time() + 1.0/self.cfg['max_fps']\n",
-    "        \n",
-    "        while not self.stopped:\n",
-    "            frame = self.video_stream.read()\n",
-    "            if frame is None:\n",
-    "                time.sleep(0.05)\n",
-    "                continue\n",
-    "                \n",
-    "            if time.time() < next_frame:\n",
-    "                time.sleep(0.001)\n",
-    "                continue\n",
-    "                \n",
-    "            self.count()  # count FPS\n",
-    "            self.detect_position(frame)\n",
-    "            next_frame += 1.0/self.cfg['max_fps']\n",
-    "\n",
-    "            if self.status.value == 2 and self._x is not None:\n",
-    "                # save position\n",
-    "                with open(self.cfg['file_path'], 'a') as f:\n",
-    "                    f.write(\",\".join([str(x) for x in (self.frame_times[-1], \\\n",
-    "                               round(self.x_in_m, 4), round(self.y_in_m, 4))]) + \"\\n\")         \n",
-    "                    \n",
-    "                # save contours\n",
-    "                if not len(self.contour) > 0:\n",
-    "                    print('No contours')\n",
-    "                    continue\n",
-    "                ctr_in_m = np.array([self.px_to_meters(x, y) for x, y in zip(self.contour[:, 0, 0], self.contour[:, 0, 1])])\n",
-    "                data = [\"%.4f:%4f\" % (x[0], x[1]) for x in ctr_in_m]\n",
-    "                with open(self.cfg['contour_path'], 'a+') as f:    \n",
-    "                    f.write(\",\".join(data) + \"\\n\")                   \n",
-    "       \n",
-    "    def switch_background(self):\n",
-    "        self.background = self.bg_dark if self.is_light else self.bg_light \n",
-    "        self.is_light = not self.is_light\n",
-    "        \n",
+    "            \n",
     "    def detect_position(self, frame):\n",
     "        masked_frame = cv2.bitwise_and(src1=frame, src2=self.mask)\n",
     "\n",
@@ -769,61 +458,100 @@
     "        if (M['m00'] == 0):\n",
     "            return\n",
     "\n",
-    "        self._x = M['m10'] / M['m00']\n",
-    "        self._y = M['m01'] / M['m00']\n",
-    "        self.contour = contour  # in pixels\n",
-    "        \n",
-    "        ctr_in_px = np.array([x for x in zip(contour[:, 0, 0], contour[:, 0, 1])])\n",
-    "        if ctr_in_px[:, 0].max() - ctr_in_px[:, 0].min() > ctr_in_px[:, 1].max() - ctr_in_px[:, 1].min():\n",
-    "            slope, intercept, r_value, p_value, std_err = stats.linregress(ctr_in_px[:, 0], ctr_in_px[:, 1])\n",
-    "            self.lr = {'slope': slope, 'intercept': intercept, 'r_value': r_value, 'p_value': p_value, 'std_err': std_err}\n",
+    "        x, y = M['m10'] / M['m00'], M['m01'] / M['m00']\n",
+    "        if self._positions_list is None:\n",
+    "            self._positions_list = np.array([[time.time(), x, y, 0, 0]])\n",
     "        else:\n",
-    "            slope, intercept, r_value, p_value, std_err = stats.linregress(ctr_in_px[:, 1], ctr_in_px[:, 0])\n",
-    "            self.lr = {'slope': 1 - slope, 'intercept': intercept, 'r_value': r_value, 'p_value': p_value, 'std_err': std_err}\n",
+    "            t1 = time.time() - self.cfg['history_duration']\n",
+    "            idx = np.argmin(np.abs(self._positions_list[:, 0] - t1))\n",
+    "            last_hd = self._positions_list[-1][4]\n",
+    "            self._positions_list = np.concatenate([self._positions_list[idx:], [np.array([time.time(), x, y, 0, last_hd])]])\n",
+    "\n",
+    "        # speed\n",
+    "        if len(self._positions_list) > len(self.kernel):\n",
+    "            x = (-self._positions_list[:, 1] + self.cfg['arena_x']) * self.pixel_size * (-1 if self.cfg['flip_x'] else 1)\n",
+    "            y = (-self._positions_list[:, 2] + self.cfg['arena_y']) * self.pixel_size * (-1 if self.cfg['flip_y'] else 1)\n",
     "            \n",
-    "    # local single-agent interface\n",
-    "    \n",
-    "    @property\n",
-    "    def x_in_px(self):\n",
-    "        return int(self._x) if self._x is not None else None\n",
+    "            # to avoid boundary effects\n",
+    "            x = np.concatenate([np.ones(int(len(self.kernel)/2) - 1) * x[0], x, np.ones(int(len(self.kernel)/2)) * x[-1]])\n",
+    "            y = np.concatenate([np.ones(int(len(self.kernel)/2) - 1) * y[0], y, np.ones(int(len(self.kernel)/2)) * y[-1]])\n",
+    "            \n",
+    "            x_smooth = np.convolve(x, self.kernel, 'valid') / self.kernel.sum()  # valid mode to avoid boundary effects\n",
+    "            y_smooth = np.convolve(y, self.kernel, 'valid') / self.kernel.sum()\n",
     "\n",
-    "    @property\n",
-    "    def y_in_px(self):\n",
-    "        return int(self._y) if self._y is not None else None\n",
-    "    \n",
-    "    @property\n",
-    "    def x_in_m(self):\n",
-    "        if self._x is None:\n",
-    "            return None\n",
-    "        return (self.cfg['arena_x'] - self._x) * self.pixel_size * (-1 if self.cfg['flip_x'] else 1)\n",
-    "    \n",
-    "    @property\n",
-    "    def y_in_m(self):\n",
-    "        if self._y is None:\n",
-    "            return None\n",
-    "        return (self.cfg['arena_y'] - self._y) * self.pixel_size * (-1 if self.cfg['flip_y'] else 1)    \n",
+    "            dx = np.sqrt(np.square(np.diff(x_smooth)) + np.square(np.diff(y_smooth)))\n",
+    "            dt = np.diff(self._positions_list[:, 0])\n",
+    "            speed = np.concatenate([dx/dt, [dx[-1]/dt[-1]]])\n",
+    "            self._positions_list[:, 3] = speed  # in m/s\n",
+    "        \n",
+    "        # head direction\n",
+    "        recent_traj = self._positions_list[self._positions_list[:, 0] > time.time() - 0.5]\n",
+    "        avg_speed = recent_traj[:, 3].mean()\n",
+    "        if avg_speed > self.cfg['hd_update_speed']:  # if animal runs basically\n",
+    "            x, y = recent_traj[0][1], recent_traj[0][2]\n",
+    "            vectors = [np.array([a[1], a[2]]) - np.array([x, y]) for a in recent_traj[1:]]\n",
+    "            avg_direction = np.array(vectors).sum(axis=0) / len(vectors)\n",
+    "\n",
+    "            avg_angle = -np.arctan2(avg_direction[1], avg_direction[0])\n",
+    "            self._positions_list[-1][4] = avg_angle  # in radians\n",
+    "        \n",
+    "        self._contour = contour  # in pixels\n",
+    "        \n",
+    "        #ctr_in_px = np.array([x for x in zip(contour[:, 0, 0], contour[:, 0, 1])])\n",
+    "        #if ctr_in_px[:, 0].max() - ctr_in_px[:, 0].min() > ctr_in_px[:, 1].max() - ctr_in_px[:, 1].min():\n",
+    "        #    slope, intercept, r_value, p_value, std_err = stats.linregress(ctr_in_px[:, 0], ctr_in_px[:, 1])\n",
+    "        #    self.lr = {'slope': slope, 'intercept': intercept, 'r_value': r_value, 'p_value': p_value, 'std_err': std_err}\n",
+    "        #else:\n",
+    "        #    slope, intercept, r_value, p_value, std_err = stats.linregress(ctr_in_px[:, 1], ctr_in_px[:, 0])\n",
+    "        #    self.lr = {'slope': 1 - slope, 'intercept': intercept, 'r_value': r_value, 'p_value': p_value, 'std_err': std_err}\n",
+    "\n",
+    "    def save_position(self):\n",
+    "        if self.positions_in_px is None:\n",
+    "            return\n",
+    "        with open(self.cfg['file_path'], 'a') as f:\n",
+    "            f.write(\",\".join([str(x) for x in (self.frame_times[-1], \\\n",
+    "                   round(self.positions_in_m[0][0], 4), round(self.positions_in_m[0][1], 4))]) + \"\\n\") \n",
     "    \n",
-    "    # generic interface\n",
+    "    def save_contours(self):\n",
+    "        if self.contours is None:\n",
+    "            return\n",
+    "        ctr_in_m = np.array([self.px_to_meters(x, y) for x, y in zip(self._contour[:, 0, 0], self._contour[:, 0, 1])])\n",
+    "        data = [\"%.4f:%.4f\" % (x[0], x[1]) for x in ctr_in_m]\n",
+    "        with open(self.cfg['contour_path'], 'a+') as f:    \n",
+    "            f.write(\",\".join(data) + \"\\n\")\n",
     "    \n",
     "    @property\n",
     "    def positions_in_px(self):\n",
-    "        return [np.array([self.x_in_px, self.y_in_px])] if self.x_in_px is not None else None\n",
+    "        return [self._positions_list[-1][1:].astype('int32')] if self._positions_list is not None else None\n",
     "    \n",
     "    @property\n",
     "    def positions_in_m(self):\n",
-    "        return [np.array([self.x_in_m, self.y_in_m])] if self.x_in_m is not None else None\n",
+    "        if self._positions_list is None:\n",
+    "            return None\n",
+    "        x = (self.cfg['arena_x'] - self._positions_list[-1][1]) * self.pixel_size * (-1 if self.cfg['flip_x'] else 1)\n",
+    "        y = (self.cfg['arena_y'] - self._positions_list[-1][2]) * self.pixel_size * (-1 if self.cfg['flip_y'] else 1)\n",
+    "        return [np.array([x, y])]\n",
     "    \n",
     "    @property\n",
     "    def contours(self):\n",
-    "        return [self.contour]\n",
+    "        return [self._contour] if self._contour is not None else None\n",
     "    \n",
-    "    def is_inside(self, x, y, r):\n",
-    "        for pos in self.positions_in_m:\n",
-    "            if (pos[0] - x)**2 + (pos[1] - y)**2 <= r**2:\n",
-    "                return True\n",
-    "        return False"
+    "    @property\n",
+    "    def speeds(self):\n",
+    "        return [self._positions_list[-1][3]] if self._positions_list is not None else None\n",
+    "    \n",
+    "    @property\n",
+    "    def hds(self):\n",
+    "        return [np.degrees(self._positions_list[-1][4])] if self._positions_list is not None else None"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -833,11 +561,11 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 84,
+   "execution_count": 48,
    "metadata": {},
    "outputs": [],
    "source": [
-    "with open(os.path.join('..', 'profiles', 'andrey_hippoSIT_008229_multiple.json')) as json_file:\n",
+    "with open(os.path.join('..', 'profiles', 'default.json')) as json_file:\n",
     "    cfg = json.load(json_file)\n",
     "\n",
     "pt_cfg = cfg['position']\n",
@@ -847,7 +575,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 85,
+   "execution_count": 49,
    "metadata": {
     "scrolled": true
    },
@@ -857,129 +585,6 @@
      "output_type": "stream",
      "text": [
       "Webcam stream 1024.0:768.0 at 30.00 FPS started\n",
-      "[[649.13703317 450.80120622]\n",
-      " [649.13703317 450.80120622]\n",
-      " [649.17979025 450.82246944]\n",
-      " [649.12542687 450.80997858]\n",
-      " [649.12542687 450.80997858]\n",
-      " [649.11041485 450.78497244]\n",
-      " [649.18280506 450.7834088 ]\n",
-      " [649.18280506 450.7834088 ]\n",
-      " [649.17481868 450.77905425]\n",
-      " [649.17481868 450.77905425]\n",
-      " [649.17921957 450.76529251]\n",
-      " [649.17921957 450.76529251]\n",
-      " [649.16588365 450.80766777]\n",
-      " [649.16588365 450.80766777]\n",
-      " [649.16214178 450.83542174]\n",
-      " [649.21385402 450.77969549]\n",
-      " [649.21385402 450.77969549]\n",
-      " [649.14980297 450.81664349]\n",
-      " [649.14980297 450.81664349]\n",
-      " [649.14715447 450.79343786]]\n",
-      "[[361.16319846 244.74470135]\n",
-      " [361.16319846 244.74470135]\n",
-      " [361.2920982  244.7079018 ]\n",
-      " [361.03257576 244.89924242]\n",
-      " [361.03257576 244.89924242]\n",
-      " [361.04383457 244.82008767]\n",
-      " [361.15450399 244.81071836]\n",
-      " [361.15450399 244.81071836]\n",
-      " [360.9655106  244.76840077]\n",
-      " [360.9655106  244.76840077]\n",
-      " [361.09638554 244.80569899]\n",
-      " [361.09638554 244.80569899]\n",
-      " [361.01837121 244.85340909]\n",
-      " [361.01837121 244.85340909]\n",
-      " [361.07641259 244.85381115]\n",
-      " [361.00415722 244.8622449 ]\n",
-      " [361.00415722 244.8622449 ]\n",
-      " [361.31685824 244.70708812]\n",
-      " [361.31685824 244.70708812]\n",
-      " [361.24548251 244.69184929]]\n",
-      "[0.020170695587446313, 354.12117522709224, 354.08654128820183, 0.1781380511142404]\n",
-      "[[649.17870015 450.76584118]\n",
-      " [649.18850695 450.80451346]\n",
-      " [649.2322352  450.7480681 ]\n",
-      " [649.2322352  450.7480681 ]\n",
-      " [649.2153238  450.78049064]\n",
-      " [649.2153238  450.78049064]\n",
-      " [649.14344739 450.78933892]\n",
-      " [649.16678301 450.74037231]\n",
-      " [649.16678301 450.74037231]\n",
-      " [649.19526799 450.80891757]\n",
-      " [649.19526799 450.80891757]\n",
-      " [649.15023884 450.79593382]\n",
-      " [649.15023884 450.79593382]\n",
-      " [649.18931333 450.7689982 ]\n",
-      " [649.21511018 450.75743267]\n",
-      " [649.21511018 450.75743267]\n",
-      " [649.23117623 450.73794988]\n",
-      " [649.23117623 450.73794988]\n",
-      " [649.18650378 450.7646306 ]\n",
-      " [649.20895696 450.79417566]]\n",
-      "[[361.0732792  244.78982985]\n",
-      " [361.08523592 244.8053653 ]\n",
-      " [361.19641826 244.70595032]\n",
-      " [361.19641826 244.70595032]\n",
-      " [361.10225303 244.77488927]\n",
-      " [361.10225303 244.77488927]\n",
-      " [361.09928668 244.7312512 ]\n",
-      " [361.15027006 244.76948302]\n",
-      " [361.15027006 244.76948302]\n",
-      " [360.97819851 244.81564353]\n",
-      " [360.97819851 244.81564353]\n",
-      " [361.00902978 244.83938521]\n",
-      " [361.00902978 244.83938521]\n",
-      " [360.99651365 244.75305055]\n",
-      " [361.08419048 244.82857143]\n",
-      " [361.08419048 244.82857143]\n",
-      " [361.33256528 244.6858679 ]\n",
-      " [361.33256528 244.6858679 ]\n",
-      " [361.12078119 244.85381115]\n",
-      " [361.25416906 244.71765382]]\n",
-      "[0.028602276327075016, 354.1820666577614, 354.0717304601498, 0.15845490244796706]\n",
-      "[[649.19886927 450.77035612]\n",
-      " [649.20092996 450.7617553 ]\n",
-      " [649.20092996 450.7617553 ]\n",
-      " [649.16084933 450.76340896]\n",
-      " [649.16306118 450.81548034]\n",
-      " [649.16306118 450.81548034]\n",
-      " [649.26006878 450.73188786]\n",
-      " [649.19757618 450.74177009]\n",
-      " [649.19757618 450.74177009]\n",
-      " [649.22239711 450.73108313]\n",
-      " [649.22239711 450.73108313]\n",
-      " [649.19528973 450.76238778]\n",
-      " [649.19528973 450.76238778]\n",
-      " [649.17570246 450.79456686]\n",
-      " [649.24678199 450.74552973]\n",
-      " [649.24678199 450.74552973]\n",
-      " [649.23368298 450.75635198]\n",
-      " [649.23368298 450.75635198]\n",
-      " [649.22400652 450.7307849 ]\n",
-      " [649.20655146 450.74789085]]\n",
-      "[[360.9847343  244.80888889]\n",
-      " [361.17565455 244.70672138]\n",
-      " [361.17565455 244.70672138]\n",
-      " [361.36436378 244.59992194]\n",
-      " [361.21778549 244.72453704]\n",
-      " [361.21778549 244.72453704]\n",
-      " [361.24727414 244.61487539]\n",
-      " [361.14763015 244.7039627 ]\n",
-      " [361.14763015 244.7039627 ]\n",
-      " [361.33064825 244.73571155]\n",
-      " [361.33064825 244.73571155]\n",
-      " [361.14544753 244.73206019]\n",
-      " [361.14544753 244.73206019]\n",
-      " [361.18310955 244.66686126]\n",
-      " [361.14240631 244.69171598]\n",
-      " [361.14240631 244.69171598]\n",
-      " [361.2206655  244.70052539]\n",
-      " [361.2206655  244.70052539]\n",
-      " [361.09668026 244.67171423]\n",
-      " [361.32717475 244.71920708]]\n",
-      "[0.011617968545925627, 354.13427120702806, 354.01600877300143, 0.14652917487666503]\n",
       "Camera released\n",
       "Position tracker stopped\n"
      ]
@@ -997,7 +602,8 @@
     "vs.start()  # stream runs in a separate thread\n",
     "\n",
     "# init controller\n",
-    "pt = DoublePositionTracker(status, vs, pt_cfg)\n",
+    "#pt = DoublePositionTracker(status, vs, pt_cfg)\n",
+    "pt = PositionTrackerSingleHD(status, vs, pt_cfg)\n",
     "pt.start()\n",
     "kernel_size = (25,25)\n",
     "\n",
@@ -1016,18 +622,18 @@
     "            cv2.putText(masked_frame, 'Position: %.2f FPS' % pt.get_avg_fps(), \n",
     "                     (10, 80), cv2.FONT_HERSHEY_DUPLEX, .5, (255, 255, 255))        \n",
     "            \n",
-    "            if pt.positions_list_1 is not None:\n",
+    "            if pt.positions_in_px is not None:\n",
     "                color1 = (127, 255, 0)\n",
-    "                color2 = (0, 0, 255)\n",
-    "                cv2.circle(masked_frame, (pt.xy1_in_px[0], pt.xy1_in_px[1]), 2, color1, -1)\n",
-    "                cv2.circle(masked_frame, (pt.xy2_in_px[0], pt.xy2_in_px[1]), 2, color2, -1)\n",
+    "                #color2 = (0, 0, 255)\n",
+    "                cv2.circle(masked_frame, (pt.positions_in_px[0][0], pt.positions_in_px[0][1]), 2, color1, -1)\n",
+    "                #cv2.circle(masked_frame, (pt.xy2_in_px[0], pt.xy2_in_px[1]), 2, color2, -1)\n",
     "                \n",
-    "                cv2.drawContours(masked_frame, [pt.contour1], 0, color1, 1, cv2.LINE_AA)\n",
-    "                cv2.drawContours(masked_frame, [pt.contour2], 0, color2, 1, cv2.LINE_AA)\n",
+    "                cv2.drawContours(masked_frame, [pt.contours[0]], 0, color1, 1, cv2.LINE_AA)\n",
+    "                #cv2.drawContours(masked_frame, [pt.contour2], 0, color2, 1, cv2.LINE_AA)\n",
     "                \n",
-    "                cv2.putText(masked_frame, 'Animal1: %.3f %.3f %d' % (pt.xy1_in_m[0], pt.xy1_in_m[1], cv2.contourArea(pt.contour1)), \\\n",
-    "                            (10, 40), cv2.FONT_HERSHEY_DUPLEX, 0.5, (255, 255, 255))\n",
-    "                cv2.putText(masked_frame, 'Animal2: %.3f %.3f %d' % (pt.xy2_in_m[0], pt.xy2_in_m[1], cv2.contourArea(pt.contour2)), \\\n",
+    "                cv2.putText(masked_frame, 'Animal1: %.3f %.3f %d' % (pt.positions_in_m[0][0], pt.positions_in_m[0][1], \\\n",
+    "                         cv2.contourArea(pt.contours[0])), (10, 40), cv2.FONT_HERSHEY_DUPLEX, 0.5, (255, 255, 255))\n",
+    "                cv2.putText(masked_frame, 'History: %d' % (len(pt._positions_list)), \\\n",
     "                            (10, 60), cv2.FONT_HERSHEY_DUPLEX, 0.5, (255, 255, 255))\n",
     "            \n",
     "                # regression line\n",

+ 22 - 16
profiles/default.json

@@ -3,7 +3,7 @@
         "source": 0,
         "frame_width": 1024,
         "frame_height": 768,
-        "fps": 25,
+        "fps": 30,
         "api": 700,
         "verbose": true
     },
@@ -12,13 +12,17 @@
         "file_path": "video.avi"
     },
     "position": {
+        "single_agent": true,
+        "history_duration": 5,
+        "hd_update_speed": 0.02,
         "background_light": "background_light.png",
         "background_dark": "background_dark.png",
         "threshold_light": 60,
         "threshold_dark": 30,
+        "min_blob_size": 100,
         "subtract": 1,
-        "arena_x": 527,
-        "arena_y": 377,
+        "arena_x": 524,
+        "arena_y": 372,
         "arena_radius": 315,
         "floor_radius": 284,
         "max_fps": 50,
@@ -33,12 +37,11 @@
         "device": [1, 26],
         "n_channels": 10,
         "sounds": {
-            "noise": {"amp": 0.5, "channels": [6, 8]},
-            "background": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": true, "channels": [1, 8]},
-            "target": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": true, "channels": [3, 8]}, 
-            "distractor1": {"freq": 860, "amp": 0.15, "duration": 0.05, "harmonics": true, "channels": [6, 8], "enabled": false},
-            "distractor2": {"freq": 1060, "amp": 0.25, "duration": 0.05, "harmonics": true, "channels": [6, 8], "enabled": false},
-            "distractor3": {"freq": 1320, "amp": 0.2, "duration": 0.05, "harmonics": true, "channels": [6, 8], "enabled": false}
+            "noise": {"amp": 0.2, "channels": [6, 8]},
+            "background": {"freq": 660, "amp": 0.05, "duration": 0.05, "harmonics": true, "channels": [1, 8]},
+            "target": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": true, "channels": [2, 8]}, 
+            "distractor1": {"freq": 660, "amp": 0.05, "duration": 0.05, "harmonics": true, "channels": [3, 8], "enabled": true},
+            "distractor2": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": true, "channels": [2, 8], "enabled": false}
         },
         "pulse_duration": 0.05,
         "sample_rate": 44100,
@@ -48,24 +51,27 @@
         "file_path": "sounds.csv"
     },
     "experiment": {
-        "trial_number": 60,
+        "trial_number": 100,
         "session_duration": 3000,
-        "trial_duration": 60,
+        "trial_duration": 6000,
         "target_radius": 0.14,
-        "target_duration": 4.5,
+        "target_duration": 50.0,
         "target_angle": "random",
         "unaudible_profile": 0,
         "subject": "000000",
-        "experiment_type": "SIT",
+        "experiment_type": "hippoSIT",
         "MCSArduinoPort": "COM10",
         "file_path": "events.csv",
-        "light_events": [10000, 10000],
+        "light_events": [9600, 92400],
         "phi_max": 45,
-        "timepoints": [15100, 15200, 15300, 15400],
+        "timepoints": [9600, 91200, 91800, 92400],
         "iti_distance": 2.0,
         "iti_duration": 20,
         "punishment_duration": 10,
-        "distractor_islands": 0,
+        "distractor_islands": 1,
+        "distractor_fail": true,
+        "enable_motors": true,
+        "motors_port": "COM12",
         "cable_motor_port": "COM13"
     }
 }