def updateVideoFile(self, vfilename):

        # close the if there was another file opened before.
        if self.fid is not None:
            self.fid.close()
            self.mainImage.cleanCanvas()
            self.fid = None
            self.image_group = None
            self.imgstore_name = ''
            self.fovsplitter = None
            self.well_name = ''
            self.well_names = []
            self.tiles = None
            self.ui.wells_comboBox.clear()
            self.wells_df = None

        self.vfilename = vfilename
        self.imgstore_name = Path(vfilename).parent.name
        self.ui.label_vid.setText(self.imgstore_name)
        # self.videos_dir = self.vfilename.rpartition(os.sep)[0] + os.sep

        # try:
        #     with tables.File(vfilename, 'r') as fid:
        #         if '/fov_wells' not in fid:
        #             QMessageBox.critical(self, '',
        #                                  "The FOV was not split",
        #                                  QMessageBox.Ok)
        #             return
        # except (IOError, tables.exceptions.HDF5ExtError):
        #     self.fid = None
        #     self.image_group = None
        #     QMessageBox.critical(
        #         self, '',
        #         "The selected file is not a valid .hdf5. Please select a valid file",
        #         QMessageBox.Ok)
        #     return

        fovsplitter = FOVMultiWellsSplitter(self.vfilename)
        with tables.File(vfilename) as fid:
            img_stack = fid.get_node('/full_data').read().copy()
        tiles_list = fovsplitter.tile_FOV(img_stack)

        self.wells_df = fovsplitter.wells.copy().set_index('well_name')
        self.tiles = {k: v for (k, v) in tiles_list}
        self.well_names = self.wells_df.index.to_list()
        self.ui.wells_comboBox.clear()
        for wi, wn in enumerate(self.well_names):
            self.ui.wells_comboBox.addItem(wn)
        self.updateImGroup(0)
Exemple #2
0
def tierpsy_trajectories_summary(
        fname, time_windows, time_units, only_abs_ventral=False,
        selected_feat=None, is_manual_index=False, delta_time=1/3):
    """
    Calculate the trajectory summaries for a given file fname, within a given time window
    (units of start time and end time are in frame numbers).
    """
    fps = read_fps(fname)
    data_in = read_data(fname, time_windows, time_units, fps, is_manual_index)
    if data_in is None:
        return [pd.DataFrame() for iwin in range(len(time_windows))]
    timeseries_data, blob_features = data_in

    is_fov_tosplit = was_fov_split(timeseries_data[0])
    #    is_fov_tosplit = False
    if is_fov_tosplit:
        fovsplitter = FOVMultiWellsSplitter(fname)
        good_wells_df = fovsplitter.wells[['well_name','is_good_well']].copy()
        # print(good_wells_df)

    # initialize list of summaries for all time windows
    all_summaries_list = []
    # loop over time windows
    for iwin,window in enumerate(time_windows):
        if timeseries_data[iwin].empty:
            all_summary = pd.DataFrame([])
        else:
            # initialize list of trajectory summaries for given time window
            all_summary = []
            # loop over worm indexes (individual trajectories)
            for w_ind, w_ts_data in timeseries_data[iwin].groupby('worm_index'):
                w_blobs = blob_features[iwin].loc[w_ts_data.index]

                w_ts_data = w_ts_data.reset_index(drop=True)
                w_blobs = w_blobs.reset_index(drop=True)

                worm_feats = get_summary_stats(
                    w_ts_data, fps,  w_blobs, delta_time,
                    only_abs_ventral=only_abs_ventral,
                    selected_feat=selected_feat
                    ) # returns empty dataframe when w_ts_data is empty
                worm_feats = pd.DataFrame(worm_feats).T
                worm_feats = add_trajectory_info(worm_feats, w_ind, w_ts_data, fps)

                all_summary.append(worm_feats)
            # concatenate all trajectories in given time window into one dataframe
            all_summary = pd.concat(all_summary, ignore_index=True, sort=False)
            # attach whether the wells was good or bad
            if is_fov_tosplit:  #  but only do this if we have wells
                all_summary = all_summary.merge(good_wells_df,
                                                on='well_name',
                                                how='left')

        # add dataframe to the list of summaries for all time windows
        all_summaries_list.append(all_summary)

    return all_summaries_list
 def updateVideoFile(self, vfilename):
     super().updateVideoFile(vfilename)
     # check if /fov_wells exists in masked video
     if self.fid is not None:
         if '/fov_wells' not in self.fid:
             self.is_fov_tosplit = False
         else:
             self.is_fov_tosplit = True
         # if it exists, read it
         if self.is_fov_tosplit:
             #                self.wells_in_mask = pd.DataFrame(
             #                        self.fid.get_node('/fov_wells').read())
             self.fovsplitter_mask = FOVMultiWellsSplitter(self.vfilename)
Exemple #4
0
    def updateSkelFile(self, skeletons_file):
        super().updateSkelFile(skeletons_file)
        # if no skeletons, skip
        if not self.skeletons_file:
            return
        # check if /fov_wells exists in features video
        with tables.File(self.skeletons_file, 'r') as fid:
            if '/fov_wells' not in fid:
                self.is_fov_tosplit = False
#                print("didn't find fov wells though")
            else:
                self.is_fov_tosplit = True
#                print("found fov wells in featuresN")
            # if it exists, read it
            if self.is_fov_tosplit:
#                print('reading fov_wells from featuresN')
#                print('pre-reading:')
#                print(self.wells)
#                self.wells_in_feat = pd.DataFrame(
#                        fid.get_node('/fov_wells').read())
                self.fovsplitter_feat = FOVMultiWellsSplitter(self.skeletons_file)
Exemple #5
0
def compressVideo(video_file,
                  masked_image_file,
                  mask_param,
                  expected_fps=25,
                  microns_per_pixel=None,
                  bgnd_param={},
                  buffer_size=-1,
                  save_full_interval=-1,
                  max_frame=1e32,
                  is_extract_timestamp=False,
                  fovsplitter_param={}):
    '''
    Compresses video by selecting pixels that are likely to have worms on it and making the rest of
    the image zero. By creating a large amount of redundant data, any lossless compression
    algorithm will dramatically increase its efficiency. The masked images are saved as hdf5 with gzip compression.
    The mask is calculated over a minimum projection of an image stack. This projection preserves darker regions
    (or brighter regions, in the case of fluorescent labelling)
    where the worm has more probability to be located. Additionally it has the advantage of reducing
    the processing load by only requiring to calculate the mask once per image stack.
     video_file --  original video file
     masked_image_file --
     buffer_size -- size of the image stack used to calculate the minimal projection and the mask
     save_full_interval -- have often a full image is saved
     max_frame -- last frame saved (default a very large number, so it goes until the end of the video)
     mask_param -- parameters used to calculate the mask
    '''

    #get the default values if there is any bad parameter
    output = compress_defaults(masked_image_file,
                               expected_fps,
                               buffer_size=buffer_size,
                               save_full_interval=save_full_interval)

    buffer_size = output['buffer_size']
    save_full_interval = output['save_full_interval']

    if len(bgnd_param) > 0:
        is_bgnd_subtraction = True
        assert bgnd_param['buff_size'] > 0 and bgnd_param['frame_gap'] > 0
    else:
        is_bgnd_subtraction = False

    if len(fovsplitter_param) > 0:
        is_fov_tosplit = True
        assert all(key in fovsplitter_param
                   for key in ['total_n_wells', 'whichsideup', 'well_shape'])
        assert fovsplitter_param['total_n_wells'] > 0
    else:
        is_fov_tosplit = False

    # processes identifier.
    base_name = masked_image_file.rpartition('.')[0].rpartition(os.sep)[-1]

    # select the video reader class according to the file type.
    vid = selectVideoReader(video_file)

    # delete any previous  if it existed
    with tables.File(masked_image_file, "w") as mask_fid:
        pass

    #Extract metadata
    if is_extract_timestamp:
        # extract and store video metadata using ffprobe
        #NOTE: i cannot calculate /timestamp until i am sure of the total number of frames
        print_flush(base_name + ' Extracting video metadata...')
        expected_frames = store_meta_data(video_file, masked_image_file)

    else:
        expected_frames = 1

    # Initialize background subtraction if required

    if is_bgnd_subtraction:
        print_flush(base_name + ' Initializing background subtraction.')
        bgnd_subtractor = BackgroundSubtractorVideo(video_file, **bgnd_param)

    # intialize some variables
    max_intensity, min_intensity = np.nan, np.nan
    frame_number = 0
    full_frame_number = 0
    image_prev = np.zeros([])

    # Initialise FOV splitting if needed
    if is_bgnd_subtraction:
        img_fov = bgnd_subtractor.bgnd.astype(np.uint8)
    else:
        ret, img_fov = vid.read()
        # close and reopen the video, to restart from the beginning
        vid.release()
        vid = selectVideoReader(video_file)

    if is_fov_tosplit:
        # TODO: change class creator so it only needs the video name? by using
        # Tierpsy's functions such as selectVideoReader it can then read the first image by itself

        camera_serial = parse_camera_serial(masked_image_file)

        fovsplitter = FOVMultiWellsSplitter(img_fov,
                                            camera_serial=camera_serial,
                                            px2um=microns_per_pixel,
                                            **fovsplitter_param)
        wells_mask = fovsplitter.wells_mask
    else:
        wells_mask = None

    # initialize timers
    print_flush(base_name + ' Starting video compression.')

    if expected_frames == 1:
        progressTime = TimeCounter('Compressing video.')
    else:
        #if we know the number of frames display it in the progress
        progressTime = TimeCounter('Compressing video.', expected_frames)

    with tables.File(masked_image_file, "r+") as mask_fid:

        #initialize masks groups
        attr_params = dict(expected_fps=expected_fps,
                           microns_per_pixel=microns_per_pixel,
                           is_light_background=int(
                               mask_param['is_light_background']))
        mask_dataset, full_dataset, mean_intensity = initMasksGroups(
            mask_fid, expected_frames, vid.height, vid.width, attr_params,
            save_full_interval)

        if is_bgnd_subtraction:
            bg_dataset = createImgGroup(mask_fid,
                                        "/bgnd",
                                        1,
                                        vid.height,
                                        vid.width,
                                        is_expandable=False)
            bg_dataset[0, :, :] = img_fov

        if vid.dtype != np.uint8:
            # this will worm as flags to be sure that the normalization took place.
            normalization_range = mask_fid.create_earray(
                '/',
                'normalization_range',
                atom=tables.Float32Atom(),
                shape=(0, 2),
                expectedrows=expected_frames,
                filters=TABLE_FILTERS)

        while frame_number < max_frame:

            ret, image = vid.read()
            if ret != 0:
                # increase frame number
                frame_number += 1

                # opencv can give an artificial rgb image. Let's get it back to
                # gray scale.
                if image.ndim == 3:
                    image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)

                if image.dtype != np.uint8:
                    # normalise image intensities if the data type is other
                    # than uint8
                    image, img_norm_range = normalizeImage(image)
                    normalization_range.append(img_norm_range)

                #limit the image range to 1 to 255, 0 is a reserved value for the background
                assert image.dtype == np.uint8
                image = np.clip(image, 1, 255)

                # Add a full frame every save_full_interval
                if frame_number % save_full_interval == 1:
                    full_dataset.append(image[np.newaxis, :, :])
                    full_frame_number += 1

                # buffer index
                ind_buff = (frame_number - 1) % buffer_size

                # initialize the buffer when the index correspond to 0
                if ind_buff == 0:
                    Ibuff = np.zeros((buffer_size, vid.height, vid.width),
                                     dtype=np.uint8)

                # add image to the buffer
                Ibuff[ind_buff, :, :] = image.copy()
                mean_int = np.mean(image)
                assert mean_int >= 0
                mean_intensity.append(np.array([mean_int]))

            else:
                # sometimes the last image is all zeros, control for this case
                if np.all(Ibuff[ind_buff] == 0):
                    frame_number -= 1
                    ind_buff -= 1

                # close the buffer
                Ibuff = Ibuff[:ind_buff + 1]

            # mask buffer and save data into the hdf5 file
            if (ind_buff == buffer_size - 1 or ret == 0) and Ibuff.size > 0:
                if is_bgnd_subtraction:
                    Ibuff_b = bgnd_subtractor.apply(Ibuff, frame_number)
                else:
                    Ibuff_b = Ibuff

                #calculate the max/min in the of the buffer
                img_reduce = reduceBuffer(Ibuff_b,
                                          mask_param['is_light_background'])

                mask = getROIMask(img_reduce,
                                  wells_mask=wells_mask,
                                  **mask_param)

                Ibuff *= mask

                # now apply the well_mask if is MWP
                if is_fov_tosplit:
                    fovsplitter.apply_wells_mask(
                        Ibuff)  # Ibuff will be modified after this

                # add buffer to the hdf5 file
                frame_first_buff = frame_number - Ibuff.shape[0]
                mask_dataset.append(Ibuff)

            if frame_number % 500 == 0:
                # calculate the progress and put it in a string
                progress_str = progressTime.get_str(frame_number)
                print_flush(base_name + ' ' + progress_str)

            # finish process
            if ret == 0:
                break

        # now that the whole video is read, we definitely have a better estimate
        # for its number of frames. so set the save_interval again
        if is_bgnd_subtraction:
            # bg_dataset._v_attrs['save_interval'] = len(vid)
            # the above line is not accurate when using ffmpeg,
            # it's just safer to do:
            bg_dataset._v_attrs['save_interval'] = mask_dataset.shape[0]

        # close the video
        vid.release()

    # save fovsplitting data
    if is_fov_tosplit:
        fovsplitter.write_fov_wells_to_file(masked_image_file)
        if fovsplitter.is_dubious:
            print(f'Check {masked_image_file} for plate alignment')

    read_and_save_timestamp(masked_image_file)
    print_flush(base_name + ' Compressed video done.')
def tierpsy_plate_summary(fname,
                          filter_params,
                          time_windows,
                          time_units,
                          only_abs_ventral=False,
                          selected_feat=None,
                          is_manual_index=False,
                          delta_time=1 / 3):
    """
    Calculate the plate summaries for a given file fname, within a given time window
    (units of start time and end time are in frame numbers).
    """
    fps = read_fps(fname)
    data_in = read_data(fname, filter_params, time_windows, time_units, fps,
                        is_manual_index)

    # if manual annotation was chosen and the trajectories_data does not contain
    # worm_index_manual, then data_in is None
    # if time_windows in seconds and fps is not defined (fps=-1), then data_in is None
    if data_in is None:
        return [pd.DataFrame() for iwin in range(len(time_windows))]

    timeseries_data, blob_features = data_in

    # was the fov split in wells? only use the first window to detect that,
    # and to extract the list of well names
    is_fov_tosplit = was_fov_split(fname)
    #    is_fov_tosplit = False

    if is_fov_tosplit:
        fovsplitter = FOVMultiWellsSplitter(fname)
        good_wells_df = fovsplitter.wells[['well_name', 'is_good_well']].copy()
        # print(good_wells_df)

    # initialize list of plate summaries for all time windows
    plate_feats_list = []
    for iwin, window in enumerate(time_windows):
        if is_fov_tosplit == False:
            plate_feats = get_summary_stats(timeseries_data[iwin],
                                            fps,
                                            blob_features[iwin],
                                            delta_time,
                                            only_abs_ventral=only_abs_ventral,
                                            selected_feat=selected_feat)
            plate_feats['n_skeletons'] = count_skeletons(timeseries_data[iwin])
            plate_feats_list.append(pd.DataFrame(plate_feats).T)
        else:
            # get list of well names in this time window
            # (maybe some wells looked empty during a whole window,
            # this prevents errors later on)
            well_names_list = list(
                set(timeseries_data[iwin]['well_name']) - set(['n/a']))
            # create a list of well-specific, one-line long dataframes
            well_feats_list = []
            for well_name in well_names_list:
                # find entries in timeseries_data[iwin] belonging to the right well
                idx_well = timeseries_data[iwin]['well_name'] == well_name
                well_feats = get_summary_stats(
                    timeseries_data[iwin][idx_well].reset_index(),
                    fps,
                    blob_features[iwin][idx_well].reset_index(),
                    delta_time,
                    only_abs_ventral=only_abs_ventral,
                    selected_feat=selected_feat)
                well_feats['n_skeletons'] = count_skeletons(
                    timeseries_data[iwin][idx_well])
                # first prepend the well_name_s to the well_feats series,
                # then transpose it so it is a single-row dataframe,
                # and append it to the well_feats_list
                well_name_s = pd.Series({'well_name': well_name})
                well_feats_list.append(
                    pd.DataFrame(pd.concat([well_name_s, well_feats])).T)
            # check: did we find any well?
            if len(well_feats_list) == 0:
                plate_feats_list.append(pd.DataFrame())
            else:
                # now concatenate all the single-row df in well_feats_list in a single df
                # and append it to the growing list (1 entry = 1 window)
                plate_feats = pd.concat(well_feats_list,
                                        ignore_index=True,
                                        sort=False)
                #                import pdb; pdb.set_trace()
                plate_feats = plate_feats.merge(good_wells_df,
                                                on='well_name',
                                                how='left')
                plate_feats_list.append(plate_feats)

    return plate_feats_list
Exemple #7
0
def plot_plate_trajectories(featurefilepath, 
                            saveDir=None, 
                            downsample=10,
                            filter_trajectories=False,
                            mark_endpoints=False,
                            del_if_exists=False):
    """ Tile plots and merge into a single plot for the 
        entire 96-well plate, correcting for camera orientation. """

    file_dict = get_video_set(featurefilepath)
    
    # define multi-panel figure
    columns = 3
    rows = 2
    x = 25.5
    y = 16
    plt.ioff() if saveDir else plt.ion()
    plt.close('all')
    fig, axs = plt.subplots(rows,columns,figsize=[x,y])
    
    x_offset = 1.5 / x  # for bottom left image
    width = 0.3137      # for all but top left image
    width_tl = 0.3725   # for top left image
    height = 0.5        # for all images

    errlog = []    
    for channel, (maskedfilepath, featurefilepath) in file_dict.items():

        if saveDir:
            saveName = maskedfilepath.parent.stem + ('_filtered.png' if filter_trajectories else '.png')
            savePath = Path(saveDir) / saveName
            if savePath.exists():
                if del_if_exists:
                    os.remove(savePath)
                else:
                    print("Skipping file '%s' (already exists)" % savePath.name)
                    continue
        
        _loc, rotate = CH2PLATE_dict[channel]
        _ri, _ci = _loc

        # create bbox for image layout in figure
        if (_ri == 0) and (_ci == 0):
            # first image (with well names), bbox slightly shifted
            bbox = [0, height, width_tl, height]
        else:
            # other images
            bbox = [x_offset + width * _ci, height * (rows - (_ri + 1)), width, height]   
        
        # get location of subplot for camera
        ax = axs[_loc]
        
        try:
            # plot first frame of video + annotate wells
            FOVsplitter = FOVMultiWellsSplitter(maskedfilepath)
            FOVsplitter.plot_wells(is_rotate180=rotate, ax=ax, line_thickness=10)
            
            # plot worm trajectories
            plot_trajectory(featurefilepath, 
                            downsample=downsample,
                            filter_trajectories=filter_trajectories,
                            mark_endpoints=mark_endpoints,
                            rotate=rotate,
                            img_shape=FOVsplitter.img_shape,
                            legend=False, 
                            ax=ax)
        except Exception as e:
            print("WARNING: Could not plot video file: '%s'\n%s" % (maskedfilepath, e))
            errlog.append(maskedfilepath)
        
        # set image position in figure
        ax.set_position(bbox)
    
    if saveDir:
        if savePath.exists():
            print("Skipping file '%s' (already exists)" % savePath.name)
        else:
            Path(saveDir).mkdir(exist_ok=True, parents=True)
            fig.savefig(savePath,
                        bbox_inches='tight',
                        dpi=300,
                        pad_inches=0,
                        transparent=True)
    
    print(errlog) # TODO: Save errlog to file?
    return
def save_timeseries_feats_table(features_file,
                                derivate_delta_time,
                                fovsplitter_param={}):
    timeseries_features = []
    fps = read_fps(features_file)

    # initialise class for splitting fov
    if len(fovsplitter_param) > 0:
        is_fov_tosplit = True
        assert all(key in fovsplitter_param
                   for key in ['total_n_wells', 'whichsideup', 'well_shape'])
        assert fovsplitter_param['total_n_wells'] > 0
    else:
        is_fov_tosplit = False
    print('is fov to split?', is_fov_tosplit)

    if is_fov_tosplit:
        # split fov in wells
        masked_image_file = features_file.replace('Results', 'MaskedVideos')
        masked_image_file = masked_image_file.replace('_featuresN.hdf5',
                                                      '.hdf5')
        #        fovsplitter = FOVMultiWellsSplitter(masked_image_file=masked_image_file,
        #                                            total_n_wells=fovsplitter_param['total_n_wells'],
        #                                            whichsideup=fovsplitter_param['whichsideup'],
        #                                            well_shape=fovsplitter_param['well_shape'])
        fovsplitter = FOVMultiWellsSplitter(masked_image_file,
                                            **fovsplitter_param)
        # store wells data in the features file
        fovsplitter.write_fov_wells_to_file(features_file)

    with pd.HDFStore(features_file, 'r') as fid:
        trajectories_data = fid['/trajectories_data']

    trajectories_data_g = trajectories_data.groupby('worm_index_joined')
    progress_timer = TimeCounter('')
    base_name = get_base_name(features_file)
    tot_worms = len(trajectories_data_g)

    def _display_progress(n):
        # display progress
        dd = " Calculating tierpsy features. Worm %i of %i done." % (n + 1,
                                                                     tot_worms)
        print_flush(base_name + dd + ' Total time:' +
                    progress_timer.get_time_str())

    _display_progress(0)
    with tables.File(features_file, 'r+') as fid:

        for gg in [
                '/timeseries_data', '/event_durations', '/timeseries_features'
        ]:
            if gg in fid:
                fid.remove_node(gg)

        feat_dtypes = [(x, np.float32) for x in timeseries_all_columns]

        feat_dtypes = [('worm_index', np.int32), ('timestamp', np.int32),
                       ('well_name', 'S3')] + feat_dtypes

        timeseries_features = fid.create_table('/',
                                               'timeseries_data',
                                               obj=np.recarray(0, feat_dtypes),
                                               filters=TABLE_FILTERS)

        if '/food_cnt_coord' in fid:
            food_cnt = fid.get_node('/food_cnt_coord')[:]
        else:
            food_cnt = None

        #If i find the ventral side in the multiworm case this has to change
        ventral_side = read_ventral_side(features_file)

        for ind_n, (worm_index, worm_data) in enumerate(trajectories_data_g):

            skel_id = worm_data['skeleton_id'].values

            #deal with any nan in the skeletons
            good_id = skel_id >= 0
            skel_id_val = skel_id[good_id]
            traj_size = skel_id.size

            args = []
            for p in ('skeletons', 'widths', 'dorsal_contours',
                      'ventral_contours'):

                node_str = '/coordinates/' + p
                if node_str in fid:
                    node = fid.get_node(node_str)
                    dat = np.full((traj_size, *node.shape[1:]), np.nan)
                    if skel_id_val.size > 0:
                        if len(node.shape) == 3:
                            dd = node[skel_id_val, :, :]
                        else:
                            dd = node[skel_id_val, :]
                        dat[good_id] = dd
                else:
                    dat = None

                args.append(dat)

            timestamp = worm_data['timestamp_raw'].values.astype(np.int32)

            feats = get_timeseries_features(
                *args,
                timestamp=timestamp,
                food_cnt=food_cnt,
                fps=fps,
                ventral_side=ventral_side,
                derivate_delta_time=derivate_delta_time)
            #save timeseries features data
            feats = feats.astype(np.float32)
            feats['worm_index'] = worm_index
            if is_fov_tosplit:
                feats[
                    'well_name'] = fovsplitter.find_well_from_trajectories_data(
                        worm_data)
            else:
                feats['well_name'] = 'n/a'
            # cast well_name to the correct type
            # (before shuffling columns, so it remains the last entry)
            # needed because for some reason this does not work:
            # feats['well_name'] = feats['well_name'].astype('S3')
            feats['_well_name'] = feats['well_name'].astype('S3')
            feats.drop(columns='well_name', inplace=True)
            feats.rename(columns={'_well_name': 'well_name'}, inplace=True)

            #move the last fields to the first columns
            cols = feats.columns.tolist()
            cols = cols[-2:] + cols[:-2]
            cols[1], cols[2] = cols[2], cols[1]

            feats = feats[cols]

            feats['worm_index'] = feats['worm_index'].astype(np.int32)
            feats['timestamp'] = feats['timestamp'].astype(np.int32)
            feats = feats.to_records(index=False)

            timeseries_features.append(feats)
            _display_progress(ind_n)
def plot_well_trajectory(featuresfilepath,
                         maskedvideopath,
                         well_name,
                         downsample=10,
                         filter_trajectories=False,
                         ax=None,
                         verbose=True,
                         **kwargs):
    """ Plot centroid coordinates for worms in a given well """

    # plot first frame of video for sample well
    FOVsplitter = FOVMultiWellsSplitter(maskedvideopath)
    fov_wells = FOVsplitter.wells
    well_fov = fov_wells[fov_wells['well_name'] == well_name]

    if well_fov.iloc[0]['is_good_well'] != 1:
        print("WARNING: Bad well data for:\n%s\t%s" %
              (featuresfilepath, well_name))
        return

    if not ax:
        fig, ax = plt.subplots(**kwargs)

    img_list = FOVsplitter.tile_FOV(FOVsplitter.img)
    well_img = [i[1] for i in img_list if i[0] == well_name][0]

    ax.imshow(well_img, cmap='gray')

    df = read_timeseries(featuresfilepath,
                         names=[
                             'worm_index', 'timestamp', 'well_name',
                             'coord_x_body', 'coord_y_body'
                         ],
                         only_wells=[well_name])

    microns_per_pixel = read_microns_per_pixel(featuresfilepath)
    df['x'] = df['coord_x_body'] / microns_per_pixel
    df['y'] = df['coord_y_body'] / microns_per_pixel

    # subtract x,y offset to set bottom left coords of well as plot origin for trajectory plot
    df['x'] = df['x'] - well_fov.iloc[0]['x_min']
    df['y'] = df['y'] - well_fov.iloc[0]['y_min']

    # Optional - filter trajectories using movement/time threshold parameters (globals)
    if filter_trajectories:
        df, _ = filter_worm_trajectories(
            df,
            threshold_move=THRESHOLD_DISTANCE_PIXELS,
            threshold_time=THRESHOLD_DURATION_FRAMES,
            fps=read_fps(featuresfilepath),
            microns_per_pixel=microns_per_pixel,
            timestamp_col='timestamp',
            worm_id_col='worm_index',
            x_coord_col='x',
            y_coord_col='y',
            verbose=verbose)

    # Plot trajectory
    if downsample is not None:
        # Downsample frames for plotting
        downsample = 1 if downsample < 1 else downsample

        ax.scatter(x=df['x'][::downsample],
                   y=df['y'][::downsample],
                   c=df['timestamp'][::downsample],
                   cmap='plasma',
                   s=7)
    else:
        ax.scatter(x=df['x'], y=df['y'], c=df['timestamp'], cmap='plasma', s=7)

    ax.axes.get_xaxis().set_visible(False)
    ax.axes.get_yaxis().set_visible(False)
    ax.autoscale(enable=True, axis='x', tight=True)  # re-scaling axes
    ax.autoscale(enable=True, axis='y', tight=True)

    return
def plot_plate_trajectories(featurefilepath, saveDir=None, downsample=10):
    """ Tile plots and merge into a single plot for the
        entire 96-well plate, correcting for camera orientation. """

    from tierpsy.analysis.split_fov.FOVMultiWellsSplitter import FOVMultiWellsSplitter

    file_dict = get_video_set(featurefilepath)

    # define multi-panel figure
    columns = 3
    rows = 2
    h_in = 6
    x_off_abs = (3600 - 3036) / 3036 * h_in
    x = columns * h_in + x_off_abs
    y = rows * h_in
    fig, axs = plt.subplots(rows, columns, figsize=[x, y])

    x_offset = x_off_abs / x  # for bottom left image
    width = (1 - x_offset) / columns  # for all but top left image
    width_tl = width + x_offset  # for top left image
    height = 1 / rows  # for all images

    for channel, ch_featfilepath in file_dict.items():

        _loc, rotate = CH2PLATE_dict[channel]
        _ri, _ci = _loc

        # create bbox for image layout in figure
        if (_ri == 0) and (_ci == 0):
            # first image (with well names), bbox slightly shifted
            bbox = [0, height, width_tl, height]
        else:
            # other images
            bbox = [
                x_offset + width * _ci, height * (rows - (_ri + 1)), width,
                height
            ]

        # get location of subplot for camera
        ax = axs[_loc]

        # plot first frame of video + annotate wells
        fovsplitter = FOVMultiWellsSplitter(ch_featfilepath)
        # let's check if the fovsplitter managed to get a frame automatically
        if fovsplitter.img is None:
            fovsplitter.img = get_frame_from_raw(feat2raw(ch_featfilepath))

        fovsplitter.plot_wells(is_rotate180=rotate, ax=ax, line_thickness=10)

        # plot worm trajectories
        plot_trajectory(ch_featfilepath,
                        ax=ax,
                        downsample=downsample,
                        legend=False,
                        rotate=rotate,
                        img_shape=fovsplitter.img_shape)

        # set image position in figure
        ax.set_position(bbox)

    plt.show()
    if saveDir:
        saveName = Path(featurefilepath).parent.stem + '.png'
        savePath = Path(saveDir) / saveName
        fig.savefig(savePath,
                    bbox_inches='tight',
                    dpi=300,
                    pad_inches=0,
                    transparent=True)
    return (fig)