CoolFace
Apppublic

neuralcomputation/batik

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
utils.py638 linesDownload Raw Back to utils
1import os2import io3import pickle4import copy5from collections import Counter6from pathlib import Path7from tempfile import NamedTemporaryFile8import regex as re9import numpy as np10import pandas as pd11from sklearn.manifold import TSNE12from sklearn.svm import SVC13from sklearn.model_selection import train_test_split14from sklearn.metrics import accuracy_score, classification_report15import torch16from tqdm import tqdm17from PIL import Image18from transformers import AutoProcessor, AutoModel19import streamlit as st20from .data_loading import load_multiple_annotations, load_multiple_annotations_io21from .data_processing import generate_label_array22from .seqIo import seqIo_reader23from .mp4Io import mp4Io_reader24 25SLIP_MODEL_ID = "google/siglip-so400m-patch14-384"26CLIP_MODEL_ID = "openai/clip-vit-base-patch32"27 28def create_annot_fname_dict(annot_fnames: list[str])-> dict:29    fs = re.compile(r'.*(_\d+)$')30 31    unique_files = set()32    for file in annot_fnames:33        file_name = os.fsdecode(file)34        base_name, _ = os.path.splitext(file_name)35        if fs.match(base_name):36            ind = len(fs.match(base_name).group(1))37            unique_files.add(base_name[:-ind])38        else:39            unique_files.add(base_name)40    41    annot_fname_dict = {}42    for unique_file in unique_files:43        annot_fname_dict.update({unique_file: [file for file in annot_fnames if unique_file in file]})44    return annot_fname_dict45 46def create_annot_fname_dict_io(annot_fnames: list[str], annot_files: list)-> dict:47    annot_file_dict = {}48    for file in annot_files:49        annot_file_dict.update({file.name : file})50    fs = re.compile(r'.*(_\d+)$')51 52    unique_files = set()53    for file in annot_fnames:54        file_name = os.fsdecode(file)55        base_name, _ = os.path.splitext(file_name)56        if fs.match(base_name):57            ind = len(fs.match(base_name).group(1))58            unique_files.add(base_name[:-ind])59        else:60            unique_files.add(base_name)61    62    annot_fname_dict = {}63    for unique_file in unique_files:64        annot_list = [file for file in annot_fnames if unique_file in file]65        annot_list.sort()66        annot_file_list = [annot_file_dict[annot_file_name] for annot_file_name in annot_list]67        annot_fname_dict.update({unique_file: annot_file_list})68    return annot_fname_dict69 70def get_io_reader(uploaded_file):71    assert uploaded_file.name[-3:]=='seq', 'Not a seq file'72    with NamedTemporaryFile(suffix="seq", delete=False) as temp:73        temp.write(uploaded_file.getvalue())74        sr = seqIo_reader(temp.name)75    return sr76 77def load_slip_model(device):78    return AutoModel.from_pretrained(SLIP_MODEL_ID).to(device)79 80def load_slip_preprocessor():81    return AutoProcessor.from_pretrained(SLIP_MODEL_ID)82 83def load_clip_model(device):84    return AutoModel.from_pretrained(CLIP_MODEL_ID).to(device)85 86def load_clip_preprocessor():87    return AutoProcessor.from_pretrained(CLIP_MODEL_ID)88 89def encode_image(image, device, model, processor):90    with torch.no_grad():91        #convert_models_to_fp32(model)92        inputs = processor(images=image, return_tensors="pt").to(device)93        image_features = model.get_image_features(**inputs)94    return image_features.cpu().numpy().flatten()95 96def generate_embeddings_stream(fnames : list[str],97                        model = 'SLIP',98                        downsample_rate = 4,99                        save_csv = False)-> tuple[list, list, list]:100    # set up model and device101    device = "cuda" if torch.cuda.is_available() else "cpu"102    os.environ['CUDA_VISIBLE_DEVICES'] = '0'103    if model == 'SLIP':104        embed_model = load_slip_model(device)105        processor = load_slip_preprocessor()106    elif model == 'CLIP':107        embed_model = load_clip_model(device)108        processor = load_clip_preprocessor()109 110    all_video_embeddings = []111    all_video_frames = []112    for fname in fnames:113        # read in file114        is_seq = False115        if fname[-3:] == 'seq': is_seq = True116        117        if is_seq:118            sr = seqIo_reader(fname)119        else:120            sr = mp4Io_reader(fname)121        N  = sr.header['numFrames']122 123        # set up embeddings and frame arrays124        embeddings = []125        frames = list(range(N))[::downsample_rate]126        print(frames)127 128        # create progress bar129        i = 0130        pbar_text = lambda i: f'Creating embeddings for {fname}. {i}/{len(frames)} frames.'131        pbar = st.progress(0, text=pbar_text(0))132 133        # convert each frame to embeddings134        for f in tqdm(frames):135            img, _ = sr.getFrame(f)136            img_arr = np.array(img)137            if is_seq:138                img_rgb = Image.fromarray(img_arr, 'L').convert('RGB')139            else:140                img_rgb = Image.fromarray(img_arr).convert('RGB')141 142            embeddings.append(encode_image(img_rgb, device, embed_model, processor))143 144            # update progress bar145            i += 1146            pbar.progress(i/len(frames), pbar_text(i))147 148        # save csv of single file149        if save_csv:150            df = pd.DataFrame(embeddings)151            df['Frame'] = frames152 153            # save csv154            basename = Path(fname).stem155            df.to_csv(f'{basename}_embeddings_downsample_{downsample_rate}.csv', index=False)156 157        all_video_embeddings.append(np.array(embeddings))158        all_video_frames.append(frames)159    return all_video_embeddings, all_video_frames160 161def get_io_reader(uploaded_file):162    if uploaded_file.name[-3:]=='seq':163        with NamedTemporaryFile(suffix="seq", delete=False) as temp:164            temp.write(uploaded_file.getvalue())165            sr = seqIo_reader(temp.name)166    else:167        with NamedTemporaryFile(suffix="mp4", delete=False) as temp:168            temp.write(uploaded_file.getvalue())169            sr = mp4Io_reader(temp.name)170    return sr171 172def generate_embeddings_stream_io(uploaded_files : list,173                                model = 'SLIP',174                                downsample_rate = 4,175                                save_csv = False)-> tuple[list, list, list]:176    # set up model and device177    device = "cuda" if torch.cuda.is_available() else "cpu"178    os.environ['CUDA_VISIBLE_DEVICES'] = '0'179    with st.spinner('Loading multimodal model...'):180        if model == 'SLIP':181            embed_model = load_slip_model(device)182            processor = load_slip_preprocessor()183        elif model == 'CLIP':184            embed_model = load_clip_model(device)185            processor = load_clip_preprocessor()186 187    all_video_embeddings = []188    all_video_frames = []189    for file in uploaded_files:190        is_seq = False191        if file.name[-3:] == 'seq': is_seq = True192 193        # read in file194        sr = get_io_reader(file)195        N  = sr.header['numFrames']196 197        # set up embeddings and frame arrays198        embeddings = []199        frames = list(range(N))[::downsample_rate]200        print(frames)201 202        # create progress bar203        i = 0204        pbar_text = lambda i: f'Creating embeddings for {file.name}. {i}/{len(frames)} frames.'205        pbar = st.progress(0, text=pbar_text(0))206 207        # convert each frame to embeddings208        for f in tqdm(frames):209            img, _ = sr.getFrame(f)210            img_arr = np.array(img)211            if is_seq:212                img_rgb = Image.fromarray(img_arr, 'L').convert('RGB')213            else:214                img_rgb = Image.fromarray(img_arr).convert('RGB')215 216            embeddings.append(encode_image(img_rgb, device, embed_model, processor))217 218            # update progress bar219            i += 1220            pbar.progress(i/len(frames), pbar_text(i))221 222        # save csv of single file223        if save_csv:224            df = pd.DataFrame(embeddings)225            df['Frame'] = frames226 227            # save csv228            df.to_csv(f'embeddings_downsample_{downsample_rate}_{N}_frames.csv', index=False)229 230        all_video_embeddings.append(np.array(embeddings))231        all_video_frames.append(frames)232    return all_video_embeddings, all_video_frames233 234def create_embeddings_csv(out: str,235                          fnames: list[str],236                          embeddings: list[np.ndarray],237                          frames: list[list[int]],238                          annotations: list[list[str]],239                          test_fnames: None | list[str],240                          views: None | list[str],241                          conditions: None | list[str],242                          downsample_rate = 4,243                          filesystem = None):244    """245    Creates a .csv file containing all of the generated embeddings and provived information.246 247    Parameters:248    -----------249    out : str250        The name of the resulting file.251    fnames : list[str]252        Video sources for each of the embedding arrays.253    embeddings : np.ndarray254        The generated embeddings from the images.255    downsample_rate : int256        The downsample_rate used for generating the embeddings.257    """258    assert len(fnames) == len(embeddings)259    assert len(embeddings) == len(frames)260    all_embeddings = np.vstack(embeddings)261    df = pd.DataFrame(all_embeddings)262    263    labels = []264    for i, annot_fnames in enumerate(annotations):265        _, ext = os.path.splitext(annot_fnames[0])266        if ext == '.annot':267            annot, _, _, sr = load_multiple_annotations(annot_fnames, filesystem=filesystem)268            annot_labels = generate_label_array(annot, downsample_rate, len(frames[i]))269        elif ext == '.csv':270            if not filesystem: 271                annot_df = pd.read_csv(annot_fnames[0], header=None)272            else:273                with filesystem.open(annot_fnames[0], 'r') as csv_file:274                    annot_df = pd.read_csv(csv_file, header=None)275            annot_labels = annot_df[0].to_list()[::downsample_rate]276            assert len(annot_labels) == len(frames[i]), "There is a mismatch between the number of frames and number of labels. Make sure that the passed in csv file has no header."277        else:278            raise ValueError(f'Incompatible file for annotations used. Got a file of type "{ext}".')279        assert len(annot_labels) == len(frames[i]), "There is a mismatch between the number of frames and number of labels. Make sure you have passed in the correct files."280        print(annot_labels)281        labels.append(annot_labels)282    all_labels = np.hstack(labels)283    print(len(all_labels))284    df['Label'] = all_labels285    286    all_frames = np.hstack(frames)287    df['Frame'] = all_frames288    sources = [[fname for _ in range(len(frames[i]))] for i, fname in enumerate(fnames)]289    all_sources = np.hstack(sources)290    df['Source'] = all_sources291 292    if test_fnames:293        t_split = lambda x: True if x in test_fnames else False294        test = [[t_split(fname) for _ in range(len(frames[i]))] for i, fname in enumerate(fnames)]295    else:296        test = [[True for _ in range(len(frames[i]))] for i, _ in enumerate(fnames)]297    all_test = np.hstack(test)298    df['Test'] = all_test299 300    if views:301        view = [[views[i] for _ in range(len(frames[i]))] for i in range(len(fnames))]302    else:303        view = [[None for _ in range(len(frames[i]))] for i in range(len(fnames))]304    all_view = np.hstack(view)305    df['View'] = all_view306    307    if conditions:308        condition = [[conditions[i] for _ in range(len(frames[i]))] for i in range(len(fnames))]309    else:310        condition = [[None for _ in range(len(frames[i]))] for i in range(len(fnames))]311    all_condition = np.hstack(condition)312    df['Condition'] = all_condition313    return df314 315def create_embeddings_csv_io(out: str,316                          fnames: list[str],317                          embeddings: list[np.ndarray],318                          frames: list[list[int]],319                          annotations: list,320                          test_fnames: None | list[str],321                          views: None | list[str],322                          conditions: None | list[str],323                          downsample_rate = 4):324    """325    Creates a .csv file containing all of the generated embeddings and provived information.326 327    Parameters:328    -----------329    out : str330        The name of the resulting file.331    fnames : list[str]332        Video sources for each of the embedding arrays.333    embeddings : np.ndarray334        The generated embeddings from the images.335    downsample_rate : int336        The downsample_rate used for generating the embeddings.337    """338    assert len(fnames) == len(embeddings)339    assert len(embeddings) == len(frames)340    all_embeddings = np.vstack(embeddings)341    df = pd.DataFrame(all_embeddings)342    343    labels = []344    for i, uploaded_annots in enumerate(annotations):345        print(i)346        _, ext = os.path.splitext(uploaded_annots[0].name)347        if ext == '.annot':348            annot, _, _, sr = load_multiple_annotations_io(uploaded_annots)349            annot_labels = generate_label_array(annot, downsample_rate, len(frames[i]))350        elif ext == '.csv':351            annot_df = pd.read_csv(uploaded_annots[0], header=None)352            annot_labels = annot_df[0].to_list()[::downsample_rate]353            assert len(annot_labels) == len(frames[i]), "There is a mismatch between the number of frames and number of labels. Make sure that the passed in csv file has no header."354        else:355            raise ValueError(f'Incompatible file for annotations used. Got a file of type "{ext}".')356        assert len(annot_labels) == len(frames[i]), "There is a mismatch between the number of frames and number of labels. Make sure you have passed in the correct files."357        print(annot_labels)358        labels.append(annot_labels)359    all_labels = np.hstack(labels)360    print(len(all_labels))361    df['Label'] = all_labels362    363    all_frames = np.hstack(frames)364    df['Frame'] = all_frames365    sources = [[fname for _ in range(len(frames[i]))] for i, fname in enumerate(fnames)]366    all_sources = np.hstack(sources)367    df['Source'] = all_sources368 369    if test_fnames:370        t_split = lambda x: True if x in test_fnames else False371        test = [[t_split(fname) for _ in range(len(frames[i]))] for i, fname in enumerate(fnames)]372    else:373        test = [[True for _ in range(len(frames[i]))] for i, _ in enumerate(fnames)]374    all_test = np.hstack(test)375    df['Test'] = all_test376 377    if views:378        view = [[views[i] for _ in range(len(frames[i]))] for i in range(len(fnames))]379    else:380        view = [[None for _ in range(len(frames[i]))] for i in range(len(fnames))]381    all_view = np.hstack(view)382    df['View'] = all_view383    384    if conditions:385        condition = [[conditions[i] for _ in range(len(frames[i]))] for i in range(len(fnames))]386    else:387        condition = [[None for _ in range(len(frames[i]))] for i in range(len(fnames))]388    all_condition = np.hstack(condition)389    df['Condition'] = all_condition390    return df391 392def process_dataset_in_mem(embeddings_df: pd.DataFrame,393                    specified_classes=None,394                    classes_to_remove=None,395                    max_class_size=None,396                    animal_state=None,397                    view=None,398                    shuffle_data=False,399                    test_videos=None):400    """401    Processes output generated from embeddings paired with images and behavior labels.402 403    Parameters:404    -----------405    csv_path : str406        Path to the file containing the original data. This should contain embeddings,407        a column named `'Label'` and a column named `'Images'`.408    specified_classes : None | list[str]409        An optional input. Defines labels which should be kept as is in the `'Label'`410        column and which should be changed to a default `other` label.411    classes_to_remove : None | list[str]412        An optional input. Drops rows from the dataframe which contain a label in the413        list.414    max_class_size : None | int415        An optional input. Determines the maximum amount of rows a single label can416        appear in for each unique label in the `'Label'` column.417    animal_state : None | str418        An optional input. Drops rows from the dataframe which do not contain a match419        for `animal_state` in the text field within the `'Images'` column.420    view : None | str421        An optional input. Drops rows from the dataframe which do not contain a match422        for `view` in the text field within the `'Images'` column.423    shuffle_data : bool424        Determines wether the dataframe should have its rows shuffled.425    test_videos : None | list[str]426        An optional input. Determines what rows should be in the `test` dataframe, and427        which should be in the `train` dataframe. It drops rows from the respective428        dataframe by keeping or dropping rows which do not contain a match for a `str`429        in `test_videos` in the text field within the `'Images'` column, respectively.430    431    Returns:432    --------433    balanced_train_embeddings : pandas.DataFrame434        A processed dataframe whose rows contain the embeddings for each of the images435        at the corresponding index within `balanced_train_images`.436    balanced_train_labels : list[str]437        A list of labels for each of the images at the corresponing index within438        `balanced_train_images`.439    balanced_train_images: list[str]440        A list of paths to images with each image at an index corresponding to a label441        with the same index in `balanced_train_labels` and the same row index within442        `balanced_train_embeddings`.443    test_embeddings : pandas.DataFrame444        A processed dataframe whose rows contain the embeddings for each of the images445        at the corresponding index within `test_images`.446    test_labels : list[str]447        A list of labels for each of the images at the corresponing index within448        `test_images`.449    test_images : list[str]450        A list of paths to images with each image at an index corresponding to a label451        with the same index in `test_labels` and the same row index within452        `test_embeddings`.453    """454    # Convert embeddings, labels, and images to a DataFrame for easy manipulation455    df = copy.deepcopy(embeddings_df)456    df_keys = [str(x) for x in df.keys()]457    #Filter by fed or fasted458    if 'Condition' in df_keys and animal_state:459        df = df[df['Condition'].str.contains(animal_state, na=False)]460 461    if 'View' in df_keys and view:462        df = df[df['View'].str.contains(view, na=False)]463 464    # Extract unique video names excluding the frame number465    #unique_video_names = df['Images'].apply(lambda x: '_'.join(x.split('_')[:-1])).unique()466    #print("\nUnique video names:\n", unique_video_names)467 468    if classes_to_remove:469        df = df[~df['Label'].str.contains('|'.join(classes_to_remove), na=False)]470    elif classes_to_remove and 'all' in classes_to_remove:471        df = df[df['Label'].str.contains('|'.join(classes_to_remove), na=False)]472 473    # Further filter to include only specified_classes474    if specified_classes:475        single_match = lambda x: list(set(x.split('||')) & set(specified_classes))[0]476        df['Label'] = df['Label'].apply(lambda x: single_match(x) if not set(x.split('||')).isdisjoint(specified_classes) else 'other')477        specified_classes.append('other')478 479    # Separate the DataFrame into test and training sets based on test_videos480    if 'Test' in df_keys and test_videos:481        test_df = df[df['Test']]482        train_df = df[~df['Test']]483    elif test_videos:484        test_df = df[df['Images'].str.contains('|'.join(test_videos), na=False)]485        train_df = df[~df['Images'].str.contains('|'.join(test_videos), na=False)]486    else:487        test_df = pd.DataFrame(columns=df.columns)488        train_df = df489    490    # Print the number of frames in each class before balancing491    label_counts = train_df['Label'].value_counts()492    print("\nNumber of training frames in each class before balancing:")493    print(label_counts)494    495    if max_class_size:496        balanced_train_df = pd.concat([497            group.sample(n=min(len(group), max_class_size), random_state=1)498            for label, group in train_df.groupby('Label')499        ])500    else:501        balanced_train_df = train_df502 503    # Shuffle the training DataFrame504    if shuffle_data:505        balanced_train_df = balanced_train_df.sample(frac=1).reset_index(drop=True)506    507    # Convert training set back to numpy array and list508    if not "Images" in df_keys:509        balanced_train_embeddings = balanced_train_df.drop(columns=['Label', 'Frame', 'Source', 'Test','View','Condition']).to_numpy()510        balanced_train_labels = balanced_train_df['Label'].tolist()511        balanced_train_images = balanced_train_df['Frame'].tolist()512        513        # Convert test set back to numpy array and list514        test_embeddings = test_df.drop(columns=['Label', 'Frame', 'Source', 'Test','View','Condition']).to_numpy()515        test_labels = test_df['Label'].tolist()516        test_images = test_df['Frame'].tolist()517    else:518        # Convert training set back to numpy array and list519        balanced_train_embeddings = balanced_train_df.drop(columns=['Label', 'Images']).to_numpy()520        balanced_train_labels = balanced_train_df['Label'].tolist()521        balanced_train_images = balanced_train_df['Images'].tolist()522        523        # Convert test set back to numpy array and list524        if 'Test' in test_df:525            test_embeddings = test_df.drop(columns=['Label', 'Images', 'Test']).to_numpy()526        else:527            test_embeddings = test_df.drop(columns=['Label', 'Images']).to_numpy()528 529        test_labels = test_df['Label'].tolist()530        test_images = test_df['Images'].tolist()531    532    # Print the number of frames in each class after balancing533    if specified_classes or max_class_size:534        balanced_label_counts = Counter(balanced_train_labels)535        print("\nNumber of training frames in each class after balancing:")536        print(balanced_label_counts)537 538    test_label_counts = test_df['Label'].value_counts()539    # print("\nNumber of testing frames in each class:")540    print(test_label_counts)541    542    return balanced_train_embeddings, balanced_train_labels, balanced_train_images, test_embeddings, test_labels, test_images543 544def multiclass_merge_and_filter_bouts(multiclass_vector, bout_threshold, proximity_threshold):545    # Get the unique labels in the multiclass vector (excluding zero, assuming zero is the background/no label)546    unique_labels = np.unique(multiclass_vector)547    unique_labels = unique_labels[unique_labels != 0]548 549    # Initialize a vector to store the merged and filtered multiclass vector550    merged_vector = np.zeros_like(multiclass_vector)551 552    for label in unique_labels:553        # Create a binary vector for the current label554        binary_vector = (multiclass_vector == label)555 556        # Find the start and end indices of all sequences of 1's for this label557        starts = np.where(np.diff(np.concatenate(([0], binary_vector))) == 1)[0]558        ends = np.where(np.diff(np.concatenate((binary_vector, [0]))) == -1)[0]559 560        # Step 1: Merge close short bouts561        i = 0562        while i < len(starts) - 1:563            # Check if the gap between the end of the current bout and the start of the next bout564            # is within the proximity threshold565            if starts[i + 1] - ends[i] <= proximity_threshold:566                # Merge the two bouts by setting all elements between the start of the first567                # and the end of the second bout to 1568                binary_vector[ends[i]:starts[i + 1]] = 1569                # Remove the next bout from consideration570                starts = np.delete(starts, i + 1)571                ends = np.delete(ends, i)572            else:573                i += 1574 575        # Update the starts and ends after merging576        starts = np.where(np.diff(np.concatenate(([0], binary_vector))) == 1)[0]577        ends = np.where(np.diff(np.concatenate((binary_vector, [0]))) == -1)[0]578 579        # Step 2: Remove standalone short bouts580        for i in range(len(starts)):581            # Check the length of the bout582            length_of_bout = ends[i] - starts[i] + 1583 584            # If the length is less than the threshold, set those elements to 0585            if length_of_bout < bout_threshold:586                binary_vector[starts[i]:ends[i] + 1] = 0587 588        # Combine the binary vector with the merged_vector, ensuring only the current label is set589        merged_vector[binary_vector] = label590 591    # Return the filtered multiclass vector592    return merged_vector593 594def get_unique_labels(label_list: list[str]):595    label_set = set()596    for label in label_list:597        individual_labels = label.split('||')598        for individual_label in individual_labels:599            label_set.add(individual_label)600    return list(label_set)601 602def get_train_test_split(train_embeds, numerical_labels, test_size=0.05, random_state=42):603    return train_test_split(train_embeds, numerical_labels, test_size=test_size, random_state=random_state)604 605def train_model(X_train, y_train, random_state=42):606    # Train SVM Classifier607    svm_clf = SVC(kernel='rbf', random_state=random_state, probability=True)608    svm_clf.fit(X_train, y_train)609    return svm_clf610 611def pickle_model(model):612    pickled = io.BytesIO()613    pickle.dump(model, pickled)614    return pickled615 616def get_seq_io_reader(uploaded_file):617    assert uploaded_file.name[-3:]=='seq', 'Not a seq file'618    with NamedTemporaryFile(suffix="seq", delete=False) as temp:619        temp.write(uploaded_file.getvalue())620        sr = seqIo_reader(temp.name)621    return sr622 623def seq_to_arr(sr):624    N = sr.header['numFrames']625    images = []626    for f in range(N):627        I, ts = sr.getFrame(f)628        images.append(I)629    return np.array(images)630 631def get_2d_embedding(embeddings: pd.DataFrame):632    tsne = TSNE(n_jobs=4, n_components=2, random_state=42, perplexity=50)633    embedding_2d = tsne.fit_transform(np.array(embeddings))634    return embedding_2d635 636 637 638