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)
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)
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)
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
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)