{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# NFL - Visualizing Player Contact Labels\nThis notebook shows how to visualize player contact labels and helmet data of NFL play videos provided in the [1st and Future - Player Contact Detection Competiton](https://www.kaggle.com/competitions/nfl-player-contact-detection).","metadata":{}},{"cell_type":"code","source":"# import dependencies\nimport numpy as np\nimport pandas as pd\nimport os\nimport cv2\nimport subprocess\n\n# IPython\nimport IPython\nfrom IPython.display import Video, display\n\n# matplotlib\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nfrom matplotlib.markers import MarkerStyle\nimport matplotlib.animation as animation","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:09.956964Z","iopub.execute_input":"2023-01-20T09:15:09.958109Z","iopub.status.idle":"2023-01-20T09:15:10.128669Z","shell.execute_reply.started":"2023-01-20T09:15:09.957990Z","shell.execute_reply":"2023-01-20T09:15:10.127684Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# paths\n\nTEST_DIR = '/kaggle/input/nfl-player-contact-detection/test'\nTRAIN_DIR = '/kaggle/input/nfl-player-contact-detection/train'\n\nTEST_BASELINE_HELMETS = '/kaggle/input/nfl-player-contact-detection/test_baseline_helmets.csv'\nTRAIN_BASELINE_HELMETS = '/kaggle/input/nfl-player-contact-detection/train_baseline_helmets.csv'\n\nTEST_PLAYER_TRACKING = '/kaggle/input/nfl-player-contact-detection/test_player_tracking.csv'\nTRAIN_PLAYER_TRACKING = '/kaggle/input/nfl-player-contact-detection/train_player_tracking.csv'\n\nTEST_VIDEO_METADATA = '/kaggle/input/nfl-player-contact-detection/test_video_metadata.csv'\nTRAIN_VIDEO_METADATA = '/kaggle/input/nfl-player-contact-detection/train_video_metadata.csv'\n\nTRAIN_LABELS = '/kaggle/input/nfl-player-contact-detection/train_labels.csv'\nSAMPLE_SUBMISSION = '/kaggle/input/nfl-player-contact-detection/sample_submission.csv'","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:10.135635Z","iopub.execute_input":"2023-01-20T09:15:10.135915Z","iopub.status.idle":"2023-01-20T09:15:10.141506Z","shell.execute_reply.started":"2023-01-20T09:15:10.135890Z","shell.execute_reply":"2023-01-20T09:15:10.140528Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# TRAIN_BASELINE_HELMETS\n\ntrain_baseline_helmets_data = pd.read_csv(TRAIN_BASELINE_HELMETS)\nprint(train_baseline_helmets_data.shape)\ntrain_baseline_helmets_data.head()","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:10.322589Z","iopub.execute_input":"2023-01-20T09:15:10.322973Z","iopub.status.idle":"2023-01-20T09:15:18.184341Z","shell.execute_reply.started":"2023-01-20T09:15:10.322944Z","shell.execute_reply":"2023-01-20T09:15:18.183450Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# TRAIN_VIDEO_METADATA\n\ntrain_video_metadata = pd.read_csv(TRAIN_VIDEO_METADATA, parse_dates=['start_time', 'end_time', 'snap_time'])\nprint(train_video_metadata.shape)\ntrain_video_metadata.head()","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:18.185873Z","iopub.execute_input":"2023-01-20T09:15:18.186257Z","iopub.status.idle":"2023-01-20T09:15:18.223920Z","shell.execute_reply.started":"2023-01-20T09:15:18.186229Z","shell.execute_reply":"2023-01-20T09:15:18.222881Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# TRAIN_LABELS\n\ntrain_labels_data = pd.read_csv(TRAIN_LABELS, parse_dates=['datetime'])\nprint(train_labels_data.shape)\ntrain_labels_data.head()","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:18.225497Z","iopub.execute_input":"2023-01-20T09:15:18.226209Z","iopub.status.idle":"2023-01-20T09:15:46.053871Z","shell.execute_reply.started":"2023-01-20T09:15:18.226163Z","shell.execute_reply":"2023-01-20T09:15:46.052935Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Play video","metadata":{}},{"cell_type":"code","source":"def play_video(video_path: str):\n    frac = 0.65 # scaling factor for display \n    display(\n        Video(data=video_path, embed=True, height=int(720*frac), width=int(1280*frac))\n    )","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:46.056294Z","iopub.execute_input":"2023-01-20T09:15:46.056679Z","iopub.status.idle":"2023-01-20T09:15:46.064272Z","shell.execute_reply.started":"2023-01-20T09:15:46.056650Z","shell.execute_reply":"2023-01-20T09:15:46.063196Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"video_path = '/kaggle/input/nfl-player-contact-detection/train/58168_003392_Endzone.mp4'\nplay_video(video_path)","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:46.065749Z","iopub.execute_input":"2023-01-20T09:15:46.066057Z","iopub.status.idle":"2023-01-20T09:15:46.309624Z","shell.execute_reply.started":"2023-01-20T09:15:46.066029Z","shell.execute_reply":"2023-01-20T09:15:46.308351Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Video annotated with hemets and contact labels\nThe following colors are used to indicate labels:\n- __Black__ helmet boxes indicate that the player is not in contact. A unique number (home/visiting combined with jersey number) is shown next to their helmet.\n- __Green__ helmet boxes indicate that the player is in contact with one or more players.\n- __Red__ helmet boxes indicates that the player is in contact with the ground (and possibly another player).\n- __Blue__ lines show the link between players in contact with each other.","metadata":{}},{"cell_type":"code","source":"def join_helmets_contact(play_name, labels, helmets, meta, view, fps=59.94):\n    \"\"\"\n    Joins helmets and labels for a given play_name, Results can be \n    used for visualizing labels.\n    Returns a dataframe with the joint dataframe, duplicating rows if \n    multiple contacts occur.\n    \"\"\"\n    # labels and helmets for specific play_name\n    labels = labels.query('game_play==@play_name').copy()\n    helmets = helmets.query('game_play==@play_name and view==@view').copy()\n    \n    # start time of the play\n    start_time = meta.query('game_play==@play_name and view==@view')['start_time'].values[0]\n    \n    # converting frame into datetime in helmets data for merge\n    helmets['datetime'] = pd.to_timedelta(helmets['frame'] * (1/fps), unit='s') + start_time\n    helmets['datetime'] = pd.to_datetime(helmets['datetime'], utc=True)\n    \n    helmets['datetime_ngs'] = pd.DatetimeIndex(helmets['datetime'] + pd.to_timedelta(50, 'ms')).floor('100ms').values\n    helmets['datetime_ngs'] = pd.to_datetime(helmets['datetime_ngs'], utc=True)\n    \n    \n    # converting datetime in lables for merge\n    labels['datetime_ngs'] = pd.to_datetime(labels['datetime'], utc=True)\n    \n    # merge labels and helmets using 'datetime_ngs'\n    play_data = helmets.merge(labels.query('contact==1')[['datetime_ngs', 'nfl_player_id_1', 'nfl_player_id_2', 'contact_id']],\n                             left_on = ['datetime_ngs', 'nfl_player_id'],\n                             right_on = ['datetime_ngs', 'nfl_player_id_1'],\n                             how = 'left')\n    \n    return play_data","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:46.310915Z","iopub.execute_input":"2023-01-20T09:15:46.311384Z","iopub.status.idle":"2023-01-20T09:15:46.319990Z","shell.execute_reply.started":"2023-01-20T09:15:46.311357Z","shell.execute_reply":"2023-01-20T09:15:46.319238Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def annotate_video_with_contact_labels(video_path: str, baseline_boxes:pd.DataFrame, verbose=True) -> str:\n    \"\"\"\n    Annotates a video with baseline model boxes and labels.\n    Helmet boxes are colored based on the contact label.\n    \"\"\"\n    video_name = video_path.split('/')[-1]\n    play_name = video_name.split('.')[0]\n    play_name = '_'.join(play_name.split('_')[:-1])\n    HELMET_COLOR = (0, 0, 0) # black\n    baseline_boxes = baseline_boxes.query(\"game_play==@play_name\").copy()\n    \n    # verbose\n    if verbose==True:\n        print(f\"Running for {video_name}\")\n    \n    # VideoCapture Object\n    cap  = cv2.VideoCapture(video_path)\n    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))\n    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))\n    fps = cap.get(cv2.CAP_PROP_FPS)\n    \n    # VideoWriter Object\n    output_path = \"labeled_\" + video_name\n    tmp_output_path = \"tmp_\" + output_path\n    out = cv2.VideoWriter(tmp_output_path, cv2.VideoWriter_fourcc(*'MP4V'), \n                          fps, (width, height))\n    \n    # check  if camera opened successfully\n    if cap.isOpened() == False:\n        print('Error opening file')\n        \n    # read until video is complete\n    frame  = 1\n    while(cap.isOpened()):\n        # capture frame by frame\n        ret, img = cap.read()\n        \n        if ret==True:\n            # add play_name text\n            cv2.putText(img, play_name,\n                       (int(0.01*width), int(0.05*height)),\n                        cv2.FONT_HERSHEY_SIMPLEX,\n                        1,\n                        HELMET_COLOR,\n                        1\n                       )\n            \n            # add frame counter\n            cv2.putText(img, \"Frame: \"+str(frame),\n                       (int(0.75*width), int(0.05*height)),\n                        cv2.FONT_HERSHEY_SIMPLEX,\n                        1,\n                        HELMET_COLOR,\n                        1\n                       )\n\n            # adding helmet bounding boxes, player tags and contact labels\n            bbox_set = baseline_boxes.query(\"frame==@frame\")\n             # ensuring null values (no contact) come first, the player-player contact and then player-ground contact\n            bbox_set = bbox_set.sort_values(by='nfl_player_id_2', na_position='first', ascending=True)\n            for idx, row in bbox_set.iterrows():\n                if pd.isnull(row.contact_id):\n                    # add black (no contact) helmet bbox\n                    cv2.rectangle(img, \n                                  (int(row.left),int(row.top)), \n                                  (int(row.left+row.width), int(row.top+row.height)),\n                                  HELMET_COLOR, # black\n                                  1\n                                 )\n                else:\n                    if row.nfl_player_id_2=='G':\n                        # add red (contact with ground)helmet bbox\n                        cv2.rectangle(img, \n                                      (int(row.left),int(row.top)), \n                                      (int(row.left+row.width), int(row.top+row.height)),\n                                      (0, 0, 225), # red\n                                      2\n                                     )\n                    else:\n                        # add green (contact with another player) helmet bbox\n                        cv2.rectangle(img, \n                                      (int(row.left),int(row.top)), \n                                      (int(row.left+row.width), int(row.top+row.height)),\n                                      (0, 255, 0), # green\n                                      2\n                                     )\n                        \n                        # check if player 2 in view\n                        row_player_2 = bbox_set.query('nfl_player_id_2==@row.nfl_player_id_2')\n                        if len(row_player_2)==0:\n                            pass\n                        else:\n                            # add green (contact with another player) helmet bbox (for player 2)\n                            cv2.rectangle(img, \n                                          (int(row_player_2.left.values[0]),int(row_player_2.top.values[0])), \n                                          (int(row_player_2.left.values[0]+row_player_2.width.values[0]), int(row_player_2.top.values[0]+row_player_2.height.values[0])),\n                                          (0, 255, 0), # green\n                                          2\n                                         )\n                            # add blue connecting line between players in contact\n                            cv2.line(img,\n                                    (int(row.left+(row.width/2)), int(row.top+(row.height/2))),\n                                    (int(row_player_2.left.values[0]+(row_player_2.width.values[0]/2)), int(row_player_2.top.values[0]+(row_player_2.height.values[0]/2))),\n                                    (255, 0, 0), # blue\n                                    2\n                                    )\n                        \n                # add player label\n                cv2.putText(img, row.player_label,\n                        (int(row.left+10), int(row.top+10)),\n                        cv2.FONT_HERSHEY_SIMPLEX,\n                        1,\n                        HELMET_COLOR,\n                        1\n                        )\n                \n                \n            frame += 1\n            out.write(img) \n                \n        # break the loop   \n        else: \n            break\n        \n        \n    out.release()\n    \n    # Not all browsers support the codec, we will re-load the file at tmp_output_path\n    # and convert to a codec that is more broadly readable using ffmpeg\n    if os.path.exists(output_path):\n        os.remove(output_path)\n    subprocess.run(\n        [\n            \"ffmpeg\",\n            \"-i\",\n            tmp_output_path,\n            \"-crf\",\n            \"18\",\n            \"-preset\",\n            \"veryfast\",\n            \"-hide_banner\",\n            \"-loglevel\",\n            \"error\",\n            \"-vcodec\",\n            \"libx264\",\n            output_path,\n        ]\n    )\n    os.remove(tmp_output_path)\n    \n    return output_path","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:46.321374Z","iopub.execute_input":"2023-01-20T09:15:46.321890Z","iopub.status.idle":"2023-01-20T09:15:46.344708Z","shell.execute_reply.started":"2023-01-20T09:15:46.321860Z","shell.execute_reply":"2023-01-20T09:15:46.343812Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"video_path = '/kaggle/input/nfl-player-contact-detection/train/58168_003392_Endzone.mp4'\nplay_name = '58168_003392'\nview = 'Endzone'","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:46.345718Z","iopub.execute_input":"2023-01-20T09:15:46.346394Z","iopub.status.idle":"2023-01-20T09:15:46.359895Z","shell.execute_reply.started":"2023-01-20T09:15:46.346363Z","shell.execute_reply":"2023-01-20T09:15:46.359202Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"gp = join_helmets_contact(play_name, train_labels_data, train_baseline_helmets_data, train_video_metadata, view=view)\npath = annotate_video_with_contact_labels(video_path, gp)\nplay_video(path)","metadata":{"execution":{"iopub.status.busy":"2023-01-20T09:15:46.360961Z","iopub.execute_input":"2023-01-20T09:15:46.361386Z","iopub.status.idle":"2023-01-20T09:16:09.310182Z","shell.execute_reply.started":"2023-01-20T09:15:46.361358Z","shell.execute_reply":"2023-01-20T09:16:09.308067Z"},"trusted":true},"execution_count":null,"outputs":[]}]}