Beispiel #1
0
 def setUp(self) -> None:
     self.tempdir = TemporaryDirectory()
     self.session_path = utils.create_fake_session_folder(self.tempdir.name)
     utils.create_fake_raw_video_data_folder(self.session_path)
     self.eid = 'd3372b15-f696-4279-9be5-98f15783b5bb'
     self.qc = CameraQC(self.session_path,
                        one=self.one,
                        n_samples=5,
                        side='left',
                        stream=False,
                        download_data=False)
     self.qc._type = 'ephys'
Beispiel #2
0
    def test_incomplete_session(self):
        # Verify using local path
        session_path = self.incomplete

        qc = CameraQC(session_path, 'left',
                      stream=False, download_data=False, one=ONE(offline=True), n_samples=20)
        outcome, extended = qc.run(update=False)
        self.assertEqual('FAIL', outcome)
        expected = {
            '_videoLeft_brightness': 'FAIL',
            '_videoLeft_camera_times': ('PASS', 0),
            '_videoLeft_dropped_frames': 'NOT_SET',
            '_videoLeft_file_headers': 'PASS',
            '_videoLeft_focus': 'PASS',
            '_videoLeft_framerate': 'NOT_SET',
            '_videoLeft_pin_state': 'NOT_SET',
            '_videoLeft_position': 'WARNING',
            '_videoLeft_resolution': 'PASS',
            '_videoLeft_timestamps': 'NOT_SET',
            '_videoLeft_wheel_alignment': 'NOT_SET'
        }
        self.assertEqual(expected, extended)
Beispiel #3
0
    def test_training_session(self, mock_ext, mock_meta):
        """
        Tests the full QC process for a training session.  Mock a load of things so we don't need
        the ful video file.
        :param mock_ext: mock cv.VideoCapture in camera extractor module
        :param mock_meta: mock get_video_meta
        :return:
        """
        n_samples = 100
        length = 107913
        eid, session_path = self.training
        self.frames = np.load(self.data_path / 'camera' / f'{eid}_frame_samples.npy')
        mock_meta.return_value = \
            Bunch({'length': length, **CameraQC.video_meta['training']['left']})
        mock_ext().get.return_value = length
        mock_ext().read.side_effect = self.side_effect()

        qc = CameraQC(session_path, 'left',
                      stream=False, n_samples=n_samples, one=ONE(offline=True))
        qc.load_data(download_data=False, extract_times=True)
        outcome, extended = qc.run(update=False)
        self.assertEqual('FAIL', outcome)
        expected = {
            '_videoLeft_brightness': 'FAIL',
            '_videoLeft_camera_times': ('PASS', 0),
            '_videoLeft_dropped_frames': ('PASS', 1, 0),
            '_videoLeft_file_headers': 'PASS',
            '_videoLeft_focus': 'FAIL',
            '_videoLeft_framerate': ('FAIL', 32.895),
            '_videoLeft_pin_state': ('WARNING', 1151, 0),
            '_videoLeft_position': 'FAIL',
            '_videoLeft_resolution': 'PASS',
            '_videoLeft_timestamps': 'PASS',
            '_videoLeft_wheel_alignment': ('FAIL', -95)
        }
        self.assertEqual(expected, extended)
Beispiel #4
0
    def _run(self):
        # avi to mp4 compression
        command = ('ffmpeg -i {file_in} -y -nostdin -codec:v libx264 -preset slow -crf 29 '
                   '-nostats -codec:a copy {file_out}')
        output_files = ffmpeg.iblrig_video_compression(self.session_path, command)

        if len(output_files) == 0:
            _logger.info('No compressed videos found; skipping timestamp extraction')
            return  # labels the task as empty if no output

        # Video timestamps extraction
        data, files = camera.extract_all(self.session_path, save=True, video_path=output_files[0])
        output_files.extend(files)

        # Video QC
        CameraQC(self.session_path, 'left', one=self.one, stream=False).run(update=True)
        return output_files
Beispiel #5
0
 def setUpClass(cls) -> None:
     """Load a few 10 second videos for testing the various video QC checks"""
     data_path = base.IntegrationTest.default_data_root()
     video_path = data_path.joinpath('camera')
     videos = sorted(video_path.rglob('*.mp4'))
     # Instantiate using session with a video path to fool constructor.
     # To remove once we use ONE cache file
     one = ONE(base_url='https://test.alyx.internationalbrainlab.org',
               username='******',
               password='******')
     dummy_id = 'd3372b15-f696-4279-9be5-98f15783b5bb'
     qc = CameraQC(dummy_id, 'left',
                   n_samples=10, stream=False, download_data=False, one=one)
     qc.one = None
     qc._type = 'ephys'  # All videos come from ephys sessions
     qcs = OrderedDict()
     for video in videos:
         qc.video_path = video
         qc.label = vidio.label_from_path(video)
         qc.n_samples = 10
         qc.load_video_data()
         qcs[video] = qc.data.copy()
     cls.qc = qc
     cls.data = qcs
Beispiel #6
0
class TestCameraQC(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.one = ONE(
            base_url="https://test.alyx.internationalbrainlab.org",
            username="******",
            password="******",
        )
        cls.backend = matplotlib.get_backend()
        matplotlib.use('Agg')

    @classmethod
    def tearDownClass(cls) -> None:
        matplotlib.use(cls.backend)

    def setUp(self) -> None:
        self.tempdir = TemporaryDirectory()
        self.session_path = utils.create_fake_session_folder(self.tempdir.name)
        utils.create_fake_raw_video_data_folder(self.session_path)
        self.eid = 'd3372b15-f696-4279-9be5-98f15783b5bb'
        self.qc = CameraQC(self.session_path,
                           one=self.one,
                           n_samples=5,
                           side='left',
                           stream=False,
                           download_data=False)
        self.qc._type = 'ephys'

    def tearDown(self) -> None:
        self.tempdir.cleanup()
        plt.close('all')

    def test_check_brightness(self):
        self.qc.data['frame_samples'] = self.qc.load_reference_frames('left')
        n = len(self.qc.data['frame_samples'])
        self.qc.frame_samples_idx = np.linspace(0, 1000, n, dtype=int)
        self.assertEqual('PASS', self.qc.check_brightness(display=True))
        # Check plots
        fig = plt.gcf()
        self.assertEqual(3, len(fig.axes))
        expected = np.array([58.07007217, 56.55802917, 46.09558182])
        np.testing.assert_array_almost_equal(fig.axes[0].lines[0]._y, expected)
        # Make frames a third as bright
        self.qc.data['frame_samples'] = (self.qc.data['frame_samples'] /
                                         3).astype(np.int32)
        self.assertEqual('FAIL', self.qc.check_brightness())
        # Change thresholds
        self.qc.data['frame_samples'] = self.qc.load_reference_frames('left')
        self.assertEqual('FAIL', self.qc.check_brightness(bounds=(10, 20)))
        self.assertEqual('FAIL', self.qc.check_brightness(max_std=1e-6))
        # Check outcome when no frame samples loaded
        self.qc.data['frame_samples'] = None
        self.assertEqual('NOT_SET', self.qc.check_brightness())

    def test_check_file_headers(self):
        self.qc.data['video'] = {'fps': 60.}
        self.assertEqual('PASS', self.qc.check_file_headers())
        self.qc.data['video']['fps'] = 150
        self.assertEqual('FAIL', self.qc.check_file_headers())
        self.qc.data['video'] = None
        self.assertEqual('NOT_SET', self.qc.check_file_headers())

    def test_check_framerate(self):
        FPS = 60.
        self.qc.data['video'] = {'fps': FPS}
        self.qc.data['timestamps'] = np.array([round(1 / FPS, 4)] *
                                              1000).cumsum()
        outcome, frate = self.qc.check_framerate()
        self.assertEqual('PASS', outcome)
        self.assertEqual(59.88, frate)
        self.assertEqual('FAIL', self.qc.check_framerate(threshold=1e-2)[0])
        self.qc.data['timestamps'] = None
        self.assertEqual('NOT_SET', self.qc.check_framerate())

    def test_check_pin_state(self):
        FPS = 60.
        self.assertEqual('NOT_SET', self.qc.check_pin_state())
        # Add some dummy data
        self.qc.data.timestamps = np.array([round(1 / FPS, 4)] * 5).cumsum()
        self.qc.data.pin_state = np.zeros((self.qc.data.timestamps.size, 4),
                                          dtype=bool)
        self.qc.data.pin_state[1:-1, -1] = True  # Pulse on 4th pin
        self.qc.data['video'] = {
            'fps': FPS,
            'length': len(self.qc.data.timestamps)
        }
        self.qc.data.audio = self.qc.data.timestamps[[0, -1]] - 10e-3

        # Check passes and plots results
        outcome, *_ = self.qc.check_pin_state(display=True)
        self.assertEqual('PASS', outcome)
        a, b = [ln.get_xdata() for ln in plt.gcf().axes[0].lines]
        self.assertEqual(a, self.qc.data.timestamps[1])
        np.testing.assert_array_equal(b, self.qc.data.audio)

        # Fudge some numbers
        self.qc.data['video']['length'] = self.qc.data.pin_state.shape[0] - 3
        self.assertEqual('WARNING', self.qc.check_pin_state()[0])
        self.qc.data['video']['length'] = 10
        outcome, *dTTL = self.qc.check_pin_state()
        self.assertEqual('FAIL', outcome)
        self.assertEqual([0, -5], dTTL)

    def test_check_dropped_frames(self):
        n = 20
        self.qc.data.count = np.arange(n)
        self.qc.data.video = {'length': n}
        self.assertEqual('PASS', self.qc.check_dropped_frames()[0])

        # Drop some frames
        dropped = 6
        self.qc.data.count = np.append(self.qc.data.count, n + dropped)
        outcome, dframe, sz_diff = self.qc.check_dropped_frames()
        self.assertEqual('FAIL', outcome)
        self.assertEqual(dropped, dframe)
        self.assertEqual(1, sz_diff)

        # Verify threshold arg; should be warning due to size diff
        outcome, *_ = self.qc.check_dropped_frames(threshold=70.)
        self.assertEqual('WARNING', outcome)

        # Verify critical outcome
        self.qc.data.count = np.random.permutation(
            self.qc.data.count)  # Count out of order
        self.assertEqual('CRITICAL', self.qc.check_dropped_frames([0]))

        # Verify not set outcome
        self.qc.data.video = None
        self.assertEqual('NOT_SET', self.qc.check_dropped_frames())

    def test_check_focus(self):
        self.qc.side = 'left'
        self.qc.frame_samples_idx = np.linspace(0, 100, 20, dtype=int)
        outcome = self.qc.check_focus(test=True, display=True)
        self.assertEqual('FAIL', outcome)
        # Verify figures
        figs = plt.get_fignums()
        self.assertEqual(len(plt.figure(figs[0]).axes), 16)
        # Verify Laplacian on blurred images
        expected = np.array([
            13.19, 14.24, 15.44, 16.64, 18.67, 21.51, 25.99, 31.77, 40.75,
            52.52, 71.12, 98.26, 149.85, 229.96, 563.53, 563.53
        ])
        actual = [
            round(x, 2)
            for x in plt.figure(figs[1]).axes[3].lines[0]._y.tolist()
        ]
        np.testing.assert_array_equal(expected, actual)
        # Verify fft on blurred images
        expected = np.array([
            6.91, 7.2, 7.61, 8.08, 8.76, 9.47, 10.35, 11.22, 11.04, 11.42,
            11.35, 11.94, 12.45, 13.22, 13.6, 13.6
        ])
        actual = [
            round(x, 2)
            for x in plt.figure(figs[2]).axes[3].lines[0]._y.tolist()
        ]
        np.testing.assert_array_equal(expected, actual)

        # Verify not set outcome
        outcome = self.qc.check_focus()
        self.assertEqual('NOT_SET', outcome)

        # Verify ROI
        self.qc.data.frame_samples = self.qc.load_reference_frames('left')
        outcome = self.qc.check_focus(roi=None)
        self.assertEqual('PASS', outcome)

    def test_check_position(self):
        # Verify test mode
        outcome = self.qc.check_position(test=True, display=True)
        self.assertEqual('PASS', outcome)

        # Verify plots
        axes = plt.gcf().axes
        self.assertEqual(3, len(axes))
        expected = np.array([100., 93.74829841, 93.2494463])
        np.testing.assert_almost_equal(axes[2].lines[0]._y, expected)

        # Verify not set (no frame samples and not in test mode)
        outcome = self.qc.check_position()
        self.assertEqual('NOT_SET', outcome)

        # Verify percent threshold as False
        thresh = (75, 80)
        outcome = self.qc.check_position(test=True,
                                         pct_thresh=False,
                                         hist_thresh=thresh,
                                         display=True)
        self.assertEqual('FAIL', outcome)
        fig = plt.get_fignums()[-1]
        thr = [ln._y[0] for ln in plt.figure(fig).axes[2].lines[1:]]
        self.assertCountEqual(thr, thresh, 'unexpected thresholds in figure')

    def test_check_resolution(self):
        self.qc.data['video'] = {'width': 1280, 'height': 1024}
        self.assertEqual('PASS', self.qc.check_resolution())
        self.qc.data['video']['width'] = 150
        self.assertEqual('FAIL', self.qc.check_resolution())
        self.qc.data['video'] = None
        self.assertEqual('NOT_SET', self.qc.check_resolution())

    def test_check_timestamps(self):
        FPS = 60.
        n = 1000
        self.qc.data['video'] = Bunch({'fps': FPS, 'length': n})
        self.qc.data['timestamps'] = np.array([round(1 / FPS, 4)] * n).cumsum()
        # Verify passes
        self.assertEqual('PASS', self.qc.check_timestamps())
        # Verify fails
        self.qc.data['timestamps'] = np.array([round(1 / 30, 4)] *
                                              100).cumsum()
        self.assertEqual('FAIL', self.qc.check_timestamps())
        # Verify not set
        self.qc.data['video'] = None
        self.assertEqual('NOT_SET', self.qc.check_timestamps())

    def test_check_camera_times(self):
        outcome = self.qc.check_camera_times()
        self.assertEqual('NOT_SET', outcome)

        # Verify passes
        self.qc.side = 'body'
        ts_path = Path(__file__).parents[1].joinpath('extractors', 'data',
                                                     'session_ephys')
        ssv_times = load_camera_ssv_times(ts_path, self.qc.side)
        self.qc.data.bonsai_times, self.qc.data.camera_times = ssv_times
        self.qc.data.video = Bunch({'length': self.qc.data.bonsai_times.size})

        outcome, _ = self.qc.check_camera_times()
        self.assertEqual('PASS', outcome)

        # Verify warning
        n_over = 14
        self.qc.data.video['length'] -= n_over
        outcome, actual = self.qc.check_camera_times()

        self.assertEqual('WARNING', outcome)
        self.assertEqual(n_over, actual)

    def test_check_wheel_alignment(self):
        """This just checks data validation.  Integration tests test the MotionAlignment class"""
        outcome = self.qc.check_wheel_alignment()
        self.assertEqual('NOT_SET', outcome)

        # Expect FAIL when no overlapping timestamps between wheel and camera
        self.qc.data['wheel'] = {
            'timestamps': np.arange(4000),
            'position': np.random.random(4000),
            'period': np.array([3000, 3050])
        }
        self.qc.data['timestamps'] = np.arange(5000, 6000)
        outcome = self.qc.check_wheel_alignment()
        self.assertEqual('FAIL', outcome)

        # Expect NOT_SET when some overlapping timestamps but chosen period out of range
        self.qc.data['timestamps'] -= 1500
        with self.assertLogs(logging.getLogger('ibllib'), logging.WARNING):
            outcome = self.qc.check_wheel_alignment()
        self.assertEqual('NOT_SET', outcome)

    def test_ensure_data(self):
        self.qc.eid = self.eid
        self.qc.download_data = False
        # If data for this session exists locally, overwrite the methods so it is not found
        if self.one.path_from_eid(self.eid).exists():
            self.qc.one.to_eid = lambda _: self.eid
            self.qc.one.download_datasets = lambda _: None
        with self.assertRaises(AssertionError):
            self.qc.run(update=False)