From e36011e494f69bfe774ef541b31ba2c6bf9ee3cd Mon Sep 17 00:00:00 2001 From: AtomScott Date: Thu, 22 Feb 2024 14:17:40 +0900 Subject: [PATCH 1/2] Initial commit --- scripts/.env | 1 + scripts/calibrate_camera_from_mappings.py | 60 +++++++++++++++++++++++ scripts/checkerboard_calibration.py | 35 +++++++++++++ scripts/crop_image.py | 40 +++++++++++++++ scripts/sample_frames_from_videos.py | 41 ++++++++++++++++ scripts/set_permissions.sh | 22 +++++++++ scripts/upload_to_rf.py | 37 ++++++++++++++ 7 files changed, 236 insertions(+) create mode 100644 scripts/.env create mode 100644 scripts/calibrate_camera_from_mappings.py create mode 100644 scripts/checkerboard_calibration.py create mode 100644 scripts/crop_image.py create mode 100644 scripts/sample_frames_from_videos.py create mode 100755 scripts/set_permissions.sh create mode 100644 scripts/upload_to_rf.py diff --git a/scripts/.env b/scripts/.env new file mode 100644 index 0000000..f4a74d4 --- /dev/null +++ b/scripts/.env @@ -0,0 +1 @@ +RF_PRIVATE_KEY='NbJ3OM8IOeXajBfUkDQ4' \ No newline at end of file diff --git a/scripts/calibrate_camera_from_mappings.py b/scripts/calibrate_camera_from_mappings.py new file mode 100644 index 0000000..85c60bd --- /dev/null +++ b/scripts/calibrate_camera_from_mappings.py @@ -0,0 +1,60 @@ +import numpy as np +import argparse +from pathlib import Path +from sportslabkit.logger import logger +from sportslabkit.camera.calibrate import calibrate_video_from_mappings +from joblib import Parallel, delayed + +def calibrate_video(video_path, mapx_path, mapy_path, save_path, overwrite): + if not overwrite and save_path.exists(): + logger.info(f"Skipping existing file {save_path}") + return + + # Load mapx and mapy + mapx = np.load(mapx_path) + mapy = np.load(mapy_path) + + # Calibrate camera from mappings + calibrate_video_from_mappings( + media_path=video_path, + mapx=mapx, + mapy=mapy, + save_path=save_path + ) + +def calibrate_videos_in_folder(input_folder, output_folder, n_jobs, overwrite): + # Convert string paths to Path objects + input_folder = Path(input_folder) + output_folder = Path(output_folder) + + # Create output folder if it doesn't exist + output_folder.mkdir(parents=True, exist_ok=True) + + # Prepare list of tasks + tasks = [] + for video_path in input_folder.glob('*.mp4'): + mapx_path = input_folder / 'mapx.npy' + mapy_path = input_folder / 'mapy.npy' + + if not mapx_path.exists() or not mapy_path.exists(): + logger.warning(f"Missing map files for video {video_path.name}") + continue + + save_path = output_folder / video_path.name + tasks.append((video_path, mapx_path, mapy_path, save_path, overwrite)) + + # Process videos in parallel + Parallel(n_jobs=n_jobs)(delayed(calibrate_video)(*task) for task in tasks) + +def main(): + parser = argparse.ArgumentParser(description="Batch Calibrate Videos") + parser.add_argument("--input_folder", required=True, type=str, help="Folder containing video and map files") + parser.add_argument("--output_folder", required=True, type=str, help="Folder to save calibrated videos") + parser.add_argument("--n_jobs", type=int, default=1, help="Number of parallel jobs") + parser.add_argument("--overwrite", action='store_true', help="Overwrite existing files in the output folder") + args = parser.parse_args() + + calibrate_videos_in_folder(args.input_folder, args.output_folder, args.n_jobs, args.overwrite) + +if __name__ == "__main__": + main() diff --git a/scripts/checkerboard_calibration.py b/scripts/checkerboard_calibration.py new file mode 100644 index 0000000..48184ea --- /dev/null +++ b/scripts/checkerboard_calibration.py @@ -0,0 +1,35 @@ +import numpy as np +import argparse +from pathlib import Path +from sportslabkit.logger import logger +from sportslabkit.camera.calibrate import find_intrinsic_camera_parameters + +# Create the parser +parser = argparse.ArgumentParser(description='Calibrate camera using a checkerboard pattern.') + +# Add the arguments +parser.add_argument('--checkerboard_mp4_path', type=str, help='The path to the checkerboard mp4 file') +parser.add_argument('--points_to_use', type=int, help='The number of points to use') + +# Parse the arguments +args = parser.parse_args() +checkerboard_mp4_path = Path(args.checkerboard_mp4_path) + +# Find intrinsic camera parameters +K, D, mapx, mapy = find_intrinsic_camera_parameters( + checkerboard_mp4_path, + fps=1, + scale=1, + draw_on_save=True, + points_to_use=args.points_to_use, + calibration_method="fisheye" +) + +logger.info(f"mapx: {mapx}, mapy: {mapy}") + +# Save the mappings to files +save_path = checkerboard_mp4_path.parent +np.save(save_path / 'mapx.npy', mapx) +np.save(save_path / 'mapy.npy', mapy) + +logger.info(f"Saved mapx and mapy to {save_path}") diff --git a/scripts/crop_image.py b/scripts/crop_image.py new file mode 100644 index 0000000..8ca355a --- /dev/null +++ b/scripts/crop_image.py @@ -0,0 +1,40 @@ +import argparse +from PIL import Image + +def crop_image(img_path, left_crop, top_crop, right_crop, bottom_crop): + # Open the image file + img = Image.open(img_path) + + # Get the original image size + original_width, original_height = img.size + + # Define the new edges by cropping the specified amount from each side + left = left_crop + top = top_crop + right = original_width - right_crop + bottom = original_height - bottom_crop + + # Crop the image + cropped_img = img.crop((left, top, right, bottom)) + + # Save the cropped image + cropped_img_path = img_path.replace('.jpg', '_cropped.jpg') + cropped_img.save(cropped_img_path) + + return cropped_img_path + +def main(): + parser = argparse.ArgumentParser(description='Crop an image by specified amounts from each side.') + parser.add_argument('img_path', type=str, help='Path to the image file') + parser.add_argument('--left', type=int, default=0, help='Amount to crop from the left side of the image') + parser.add_argument('--top', type=int, default=0, help='Amount to crop from the top of the image') + parser.add_argument('--right', type=int, default=0, help='Amount to crop from the right side of the image') + parser.add_argument('--bottom', type=int, default=0, help='Amount to crop from the bottom of the image') + + args = parser.parse_args() + + cropped_img_path = crop_image(args.img_path, args.left, args.top, args.right, args.bottom) + print(f'Cropped image saved to {cropped_img_path}') + +if __name__ == '__main__': + main() diff --git a/scripts/sample_frames_from_videos.py b/scripts/sample_frames_from_videos.py new file mode 100644 index 0000000..117a3b3 --- /dev/null +++ b/scripts/sample_frames_from_videos.py @@ -0,0 +1,41 @@ +import argparse +import os +import cv2 + +def extract_frames(video_dir, image_dir, num_frames): + os.makedirs(image_dir, exist_ok=True) + + for video_file in os.listdir(video_dir): + if video_file.endswith(".mp4"): + video_path = os.path.join(video_dir, video_file) + cap = cv2.VideoCapture(video_path) + + if not cap.isOpened(): + print(f"Error opening video file {video_file}") + continue + + length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + interval = length // num_frames + + base_name = os.path.splitext(video_file)[0] + + for i in range(num_frames): + frame_id = i * interval + cap.set(cv2.CAP_PROP_POS_FRAMES, frame_id) + ret, frame = cap.read() + if ret: + output_filename = os.path.join(image_dir, f"{base_name}_{i:03d}.png") + cv2.imwrite(output_filename, frame) + else: + print(f"Error reading frame {frame_id} from {video_file}") + + cap.release() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Extract frames from video files.") + parser.add_argument("--video_dir", help="Directory containing the videos") + parser.add_argument("--image_dir", help="Directory to store the images") + parser.add_argument("--num_frames", type=int, default=25, help="Number of frames to extract") + + args = parser.parse_args() + extract_frames(args.video_dir, args.image_dir, args.num_frames) diff --git a/scripts/set_permissions.sh b/scripts/set_permissions.sh new file mode 100755 index 0000000..dd5cbca --- /dev/null +++ b/scripts/set_permissions.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Check if a folder name is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " +fi + +FOLDER=$1 + +# Check if the specified folder exists +if [ ! -d "$FOLDER" ]; then + echo "Error: Directory '$FOLDER' does not exist." +fi + +# Change the group of the folder and its contents to gaa50073 +chgrp -R gaa50073 "$FOLDER" + +# Set read, write, and execute permissions for the group gaa50073 +chmod -R g+rwx "$FOLDER" + +echo "Permissions set for group gaa50073 on all files and directories in '$FOLDER'" + diff --git a/scripts/upload_to_rf.py b/scripts/upload_to_rf.py new file mode 100644 index 0000000..6e604c8 --- /dev/null +++ b/scripts/upload_to_rf.py @@ -0,0 +1,37 @@ +import os +import argparse +import glob +from dotenv import load_dotenv +from roboflow import Roboflow + +def main(workspace_id, project_id, image_files): + load_dotenv() + # Initialize the Roboflow object with your API key + rf = Roboflow(api_key=os.environ.get('RF_PRIVATE_KEY')) + + # Retrieve your current workspace and project name + print(rf.workspace()) + + # Access the specified project + project = rf.workspace(workspace_id).project(project_id) + + # Upload each image to your project + for image_file in image_files: + print(f"Uploading {image_file}...") + project.upload(image_file) + print(f"Uploaded {image_file}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Upload images to a Roboflow project.") + parser.add_argument("--workspace_id", default="atom-scott-ix3vx", help="ID of the workspace") + parser.add_argument("--project_id", default="soccertrack-v2-pbqrm", help="ID of the project within the workspace") + parser.add_argument("image_files", nargs="+", help="Path to the image file(s) to upload") + + args = parser.parse_args() + + # Expand wildcard inputs + expanded_files = [] + for file_pattern in args.image_files: + expanded_files.extend(glob.glob(file_pattern)) + + main(args.workspace_id, args.project_id, expanded_files) From dfaacfe1fcb15200e766fd7fca8809fa630c611d Mon Sep 17 00:00:00 2001 From: IkumaUchida Date: Tue, 26 Mar 2024 16:49:23 +0900 Subject: [PATCH 2/2] Add calibration notebook --- fish_eye_calibration.ipynb | 103 +++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 fish_eye_calibration.ipynb diff --git a/fish_eye_calibration.ipynb b/fish_eye_calibration.ipynb new file mode 100644 index 0000000..b5184da --- /dev/null +++ b/fish_eye_calibration.ipynb @@ -0,0 +1,103 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import numpy as np\n", + "import cv2\n", + "\n", + "# ファイルパス\n", + "json_file_path = \"./test_distortiuon_keypoints.json\"\n", + "\n", + "# ファイルを開き、JSONとして読み込む\n", + "with open(json_file_path, 'r') as f:\n", + " image_points_dict = json.load(f)\n", + "\n", + "# 画像ファイルのパス\n", + "image_file_path = \"./test_distortion.png\"\n", + "\n", + "# 画像を読み込む\n", + "img = cv2.imread(image_file_path)\n", + "\n", + "# 画像の幅と高さを取得\n", + "image_height, image_width = img.shape[:2]\n", + "\n", + "\n", + "# サッカーコートの寸法をメートル単位で指定 (例: 105m x 68m)\n", + "# ここで、ピッチの各点の実世界座標をメートル単位で計算します\n", + "world_points_dict = {}\n", + "for key, value in image_points_dict.items():\n", + " # キーから実世界の座標を抽出 (例: \"(0,0)\" -> (0.0, 0.0))\n", + " coord = tuple(map(float, key.strip(\"()\").split(\",\")))\n", + " world_points_dict[key] = [coord[0], coord[1], 0.0] # Z座標は0\n", + "\n", + "# numpy配列に変換\n", + "image_points = np.array(list(image_points_dict.values()), dtype=np.float32)\n", + "world_points = np.array(list(world_points_dict.values()), dtype=np.float32)\n", + "\n", + "# image_points should be of shape (n, 1, 2)\n", + "image_points = image_points.reshape(-1, 1, 2)\n", + "\n", + "# world_points should be of shape (n, 1, 3)\n", + "world_points = world_points.reshape(-1, 1, 3)\n", + "\n", + "# カメラ行列と歪み係数の初期値\n", + "K = np.zeros((3, 3))\n", + "D = np.zeros((4, 1))\n", + "\n", + "# フラグを設定 (魚眼カメラのモデルを使用)\n", + "flags = cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC + cv2.fisheye.CALIB_FIX_SKEW\n", + "\n", + "# キャリブレーション\n", + "retval, K, D, rvecs, tvecs = cv2.fisheye.calibrate(\n", + " [world_points], [image_points], (image_width, image_height), K, D, flags=flags\n", + ")\n", + "\n", + "# 新しいカメラ行列を取得(alphaを調整して歪み補正後の画像の見切れを調整)\n", + "new_K, roi = cv2.getOptimalNewCameraMatrix(K, D, (image_width, image_height), alpha=1.02, centerPrincipalPoint=1)\n", + "\n", + "# 歪み補正マップを初期化\n", + "map1, map2 = cv2.fisheye.initUndistortRectifyMap(K, D, np.eye(3), new_K, (image_width, image_height), cv2.CV_16SC2)\n", + "\n", + "# 歪み補正を適用\n", + "undistorted_img = cv2.remap(img, map1, map2, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT)\n", + "\n", + "# 補正された画像をファイルに保存\n", + "cv2.imwrite('./undistorted_image.png', undistorted_img)\n", + "\n", + "import matplotlib.pyplot as plt\n", + "# 表示する画像のサイズをインチ単位で設定(例: 幅15インチ、高さ5インチ)\n", + "plt.figure(figsize=(25, 10))\n", + "\n", + "# 補正された画像を表示\n", + "plt.imshow(cv2.cvtColor(undistorted_img, cv2.COLOR_BGR2RGB))\n", + "plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "soccertrack-test-env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}