Please use the following script and update my script so that the individually calculated timestamp_offset is used throughout all calculations (e.g., in filtering_data)and offset (which is used in match_images_dataframes), we need to modify the way timestamps are handled in the data matching process. The main challenge here is that timestamp_offset depends on the line_number (which we obtain from tip_2d_y), but tip_2d_y comes from the coordinate data that hasn't been matched yet. This creates a circular dependency. Here is the script: import os
import re
import imageio
import pandas as pd
import numpy as np
import cv2
import matplotlib.pyplot as plt
import argparse
import glob

from src.camera_param import CameraParameters
from src.transformation.projective_geometry import project_points

from src.transformation.coordinates_loader import (
add_fiducial_coordinates,
filter_blurry,
filter_quality_recorded_trackers,
match_images_dataframes,
pre_filter_unrecorded_nir,
)

from src.selector_configs import SelectedData, SelectorParameters
from src.raw_dataset_configs import MetadataFile, RawDataFolder
from src.transformation.camera_loader import load_camera_parameters
from src.transformation.projective_geometry import add_tip_coordinates
from src.data_loader import _join_2d_positions
from src.raw_dataset_configs import CoordinateDataKeys

TOOL_TIP_ESTIMATION = np.array([300, 110, 0])

def create_csv(filename, image_id, x1, y1, x2, y2):
columns = ['image_id', 'tip_x', 'tip_y', 'center_x', 'center_y']
df = pd.DataFrame(columns=columns)
df.loc[0] = [image_id, x1, y1, x2, y2]


csvFilePath = filename
if not os.path.isfile(csvFilePath):
    df.to_csv(csvFilePath, sep=',', index=False)
else:
    df.to_csv(csvFilePath, mode='a', sep=',', header=False, index=False)
import glob

def load_raw_data(
calibration_data,
data_folder: str,
offset: float = 0.0,
verbose: bool = True,
image_list: list[str] | None = None,
) -> SelectedData:
image_folder = os.path.join(data_folder, RawDataFolder.IMAGE_FOLDER)
camera_configuration_file = os.path.join(
data_folder, RawDataFolder.CAMERA_CONFIGURATION_FILE
)


# Define the path to the logs folder where the coordinate files are located
logs_folder = os.path.join(data_folder, "logs")

# Find all coordinate files within the 'logs' subfolder
coordinate_files = glob.glob(os.path.join(logs_folder, "coord_log_led_based_*.txt"))

# Check if any files were found
if not coordinate_files:
    raise FileNotFoundError(f"No coordinate files found in {logs_folder} with pattern 'coord_log_led_based_*.txt'.")

# Load each file, skip if empty
raw_coordinates_dataframe_list = []
for coord_file in coordinate_files:
    df = pd.read_csv(coord_file, delimiter=r"\s+")
    if not df.empty:
        raw_coordinates_dataframe_list.append(df)
    else:
        print(f"Warning: {coord_file} is empty and was skipped.")

# Check if any data was loaded
if not raw_coordinates_dataframe_list:
    raise ValueError("No data loaded from any coordinate files; all files are empty.")

# Concatenate all dataframes into one
raw_coordinates_dataframe = pd.concat(raw_coordinates_dataframe_list, ignore_index=True)

if image_list is None:
    image_list = os.listdir(image_folder)
    image_list = pre_filter_unrecorded_nir(image_list, raw_coordinates_dataframe)

metadata = MetadataFile.parse_obj({
    'date': '2023-10-19:14-45-32', 
    'bur_id': '107-030', 
    'bur_name': 'match head 3.0', 
    'bur_level': '1', 
    'attachment_id': '284', 
    'tracker_id': '0', 
    'bur_offset_mm': [0, 0, -1.5]
})

tip_coordinates = np.array([
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [calibration_data[2], calibration_data[0], calibration_data[1], 1]
])
camera_parameters = load_camera_parameters(camera_configuration_file)

selected_data = SelectedData(
    data_folder=data_folder,
    image_folder=image_folder,
    coordinate_dataframe=match_images_dataframes(
        image_list, raw_coordinates_dataframe, offset=offset
    ),
    camera_parameters=camera_parameters,
    metadata=metadata,
    tip_coordinates=tip_coordinates,
)

if verbose:
    print(
        f"Found {selected_data.coordinate_dataframe.shape[0]} matches in {data_folder} across {len(coordinate_files)} files."
    )

return selected_data
def filtering_data(
calibration_data,
data_folder: str,
selector_parameters: SelectorParameters,
verbose: bool = True,
image_list: list[str] | None = None,
) -> SelectedData:
selected_data = load_raw_data(
calibration_data,
data_folder,
offset=selector_parameters.timestamp_offset,
verbose=verbose,
image_list=image_list,
)


if selector_parameters.quality_threshold is not None:
    selected_data.coordinate_dataframe = filter_quality_recorded_trackers(
        selected_data.coordinate_dataframe,
        quality_threshold=selector_parameters.quality_threshold,
        verbose=verbose,
    )

if selector_parameters.movement_threshold_mm is not None:
    selected_data.coordinate_dataframe = filter_blurry(
        selected_data.coordinate_dataframe,
        movement_threshold_mm=selector_parameters.movement_threshold_mm,
        verbose=verbose,
    )

if selector_parameters.join_fiducials:
    add_fiducial_coordinates(selected_data, verbose=verbose)

if selector_parameters.join_tip_coordinates:
    if verbose:
        print("Joining tip coordinates...")

    add_tip_coordinates(selected_data, corrected=False)

    if selector_parameters.join_tip_correction:
        add_tip_coordinates(selected_data, corrected=True)

if selector_parameters.join_2d_positions:
    _join_2d_positions(
        selected_data,
        selector_parameters,
        verbose=verbose,
    )

return selected_data
def plot_metrics(timestamps, metrics, metric_name, output_filename):
# Convert timestamps to seconds from the first timestamp
reference_time = min(timestamps)
x_values = [(t - reference_time) / 1000.0 for t in timestamps] # Convert ms to seconds


plt.figure(figsize=(10, 6))
plt.plot(x_values, metrics, marker='o', linestyle='-', color='b')
plt.title(f'{metric_name} over Time')
plt.xlabel('Time (seconds from start)')
plt.ylabel(metric_name)
plt.grid(True)

# Save the plot as a PNG image file
plt.savefig(output_filename)
plt.close()
print(f"{metric_name} plot saved to {output_filename}")
def extract_timestamp(filename: str) -> int:
"""
Extracts the timestamp from the filename. Assumes the filename contains a large numeric timestamp in milliseconds.
Example: '106840846.png' -> 106840846 (milliseconds)
"""
pattern = re.compile(r'(\d+)') # Adjust this regex pattern to match the timestamp format
match = pattern.search(filename)
if match:
return int(match.group(1)) # Return timestamp as integer
return None

def get_blur_score(grey_image):


return cv2.Laplacian(grey_image, cv2.CV_64F).var()
def detect_blur_fft(image,size: int = 60):
height, width = image.shape
center_x, center_y = int(width / 2.0), int(height / 2.0)
fft = np.fft.fft2(image)
fft_shift = np.fft.fftshift(fft)
fft_shift[center_y - size : center_y + size, center_x - size : center_x + size] = 0
fft_shift = np.fft.ifftshift(fft_shift)
recon = np.fft.ifft2(fft_shift)
magnitude = 20 * np.log(np.abs(recon))


return np.mean(magnitude)                                                
def get_2d_position_estimation(
tool_center_3d: np.ndarray,
tool_rotation_matrix: np.ndarray,
translation_vector: np.ndarray,
camera_parameters: CameraParameters,
) -> np.ndarray:
translation_vector = translation_vector[[1, 2, 0]]


tip_coordinates = tool_rotation_matrix @ translation_vector + tool_center_3d

return project_points(tip_coordinates, camera_parameters)
The 10 pixel margin for cropping the ROI. Should be adapted based on the resolution of images.
margin = 10

def get_bounding_box_estimation_center(
tool_center_2d: np.ndarray,
tool_center_3d: np.ndarray,
tool_rotation_matrix: np.ndarray,
width: int,
height: int,
camera_parameters: CameraParameters,
TOOL_TIP_ESTIMATION,
) -> tuple[int, int, int, int]:
tip_2d_position_estimation = get_2d_position_estimation(
tool_center_3d, tool_rotation_matrix, TOOL_TIP_ESTIMATION, camera_parameters
)


points = np.stack(
    (
        np.array([tip_2d_position_estimation[0], tip_2d_position_estimation[1]]),
        np.array([tool_center_2d[0], tool_center_2d[1]]),
    )
)

x1 = int(np.min(points[:, 0]))
y1 = int(np.min(points[:, 1]))
x2 = int(np.max(points[:, 0]))
y2 = int(np.max(points[:, 1]))

h = y2 - y1
w = x2 - x1

if w > h:
    x1 = x1 - margin
    x2 = x2 + margin
    ym = int((y2 + y1)*0.5)
    y1 = ym - int(w/2) - margin
    y2 = ym + int(w / 2) + margin
else:
    y1 = y1 - margin
    y2 = y2 + margin
    xm = int((x2 + x1) * 0.5)
    x1 = xm - int(h / 2) - margin
    x2 = xm + int(h / 2) + margin

return (
         max(int(x1), 0),
         max(int(y1), 0),
         min(int(x2), width),
         min(int(y2), height)), (tip_2d_position_estimation[0], tip_2d_position_estimation[1])
def crop_frame(frame_array, bounding_box):
return frame_array[
bounding_box[1] : bounding_box[3], bounding_box[0] : bounding_box[2]
], (bounding_box[0], bounding_box[1])

def resize_frame(
frame: np.ndarray, size: tuple[int, int]
) -> tuple[np.ndarray, tuple[float, float]]:
return cv2.resize(frame, size), (size[1] / frame.shape[1], size[0] / frame.shape[0])

def _reduce_point_scale(
point: np.ndarray | None,
x_min: int,
y_min: int,
scale: tuple[float, float]
) -> np.ndarray | None:
return (point - np.array([x_min, y_min])) * np.array(scale) if point is not None else None

def reduce_labels_scale(point: np.ndarray, bouding_box: tuple[int, int, int, int], scale: tuple[float, float]):
x_min, y_min, _, _ = bouding_box
return _reduce_point_scale(point, x_min, y_min, scale)

import cv2

def save_cropped_frame(size, width, height, tool_center_2d, tool_center_3d, tool_center_rotation, bur_tip_x, bur_tip_y, camera_parameters, frame_array, image_name, quality, movement, output_subfolder):
bounding_box, tip_2d_position_estimation = get_bounding_box_estimation_center(
tool_center_2d, tool_center_3d, tool_center_rotation, width, height, camera_parameters, TOOL_TIP_ESTIMATION
)
input_size = (size, size)
cropped_frame, offset = crop_frame(frame_array, bounding_box)
resized_frame, scale = resize_frame(cropped_frame, input_size)


# Paint tip_2d_position_estimation and tool_center_2d on the resized frame
tip_color = (0, 255, 0)  # Green for tip
center_color = (255, 0, 0)  # Blue for center
radius = 3
thickness = 2

# Adjust coordinates for cropping and resizing
tip_point = reduce_labels_scale(np.array(tip_2d_position_estimation), bounding_box, scale)
center_point = reduce_labels_scale(tool_center_2d, bounding_box, scale)

# Draw circles for tip and center
cv2.circle(resized_frame, tuple(map(int, tip_point)), radius, tip_color, thickness)
cv2.circle(resized_frame, tuple(map(int, center_point)), radius, center_color, thickness)

grey_image = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2GRAY)
laplacian_blur = cv2.Laplacian(grey_image, cv2.CV_64F).var()
fft_blur = detect_blur_fft(grey_image)
print(f"Laplacian Blur: {laplacian_blur:.2f}, FFT Blur: {fft_blur:.2f}")

# Prepare output path with subfolder
os.makedirs(output_subfolder, exist_ok=True)
new_image_name = f"{os.path.splitext(image_name)[0]}_q{quality:.2f}_m{movement:.2f}_lap{laplacian_blur:.2f}_fft{fft_blur:.2f}{os.path.splitext(image_name)[1]}"
imageio.v2.imwrite(os.path.join(output_subfolder, new_image_name), resized_frame)

return laplacian_blur, fft_blur
selector_parameters = SelectorParameters(
timestamp_offset= 0.059,
quality_threshold=0.5,
movement_threshold_mm=10000,
join_fiducials=False,
join_tip_coordinates=True,
join_tip_correction=True,
join_2d_positions=True,
interest_region_radius=400,
tip_inclusion_radius=200,
final_tool_radius=100,
small_component_threshold=0,
cropping_margin=5,
pixel_margin=50,
)

def main(file_path, UHD, output_size, output_folder, framerate, exposure_time):
file_list = os.listdir(file_path)
file_list.sort(key=lambda f: extract_timestamp(f))
timestamps = []
laplacian_blurs = []
fft_blurs = []
movements = []


total_lines = 2160 if UHD else 1080  # Set total lines based on UHD flag

for file in file_list:
    data_folder = os.path.join(file_path, file)
    raw_data_folder = data_folder
    camera_parameters = load_camera_parameters(
        os.path.join(data_folder, RawDataFolder.CAMERA_CONFIGURATION_FILE)
    )
    solution = [149.421361, 0, 200.342416]
    width, height = (3840, 2160) if UHD else (1920, 1080)
    selected_data = filtering_data(solution, raw_data_folder, selector_parameters, verbose=True)

    max_row = selected_data.coordinate_dataframe.shape[0]
    for row in range(max_row):
        image_name = selected_data.coordinate_dataframe.iloc[row]["image_name"]
        image_path = os.path.join(selected_data.image_folder, image_name)

        try:
            image = imageio.v2.imread(image_path)
        except Exception as e:
            print(f"Failed to read {image_name}: {e}")
            continue

        timestamp = extract_timestamp(image_name)
        if timestamp is not None:
            timestamps.append(timestamp)
            quality_value = selected_data.coordinate_dataframe.iloc[row]["quality"]
            movement_value = selected_data.coordinate_dataframe.iloc[row]["estimated_movement"]
            movements.append(movement_value)

            # Get line_number from y-coordinate of tool tip position
            line_number = int(selected_data.coordinate_dataframe.iloc[row]["tip_2d_y"])

            # Calculate timestamp_offset for this image
            timestamp_offset = (1000.0 / framerate - exposure_time / 1000.0) * (line_number / total_lines) + (exposure_time / 1000.0 / 2.0)

            tool_center_x = selected_data.coordinate_dataframe.iloc[row]["center_x"]
            tool_center_y = selected_data.coordinate_dataframe.iloc[row]["center_y"]
            bur_tip_x = selected_data.coordinate_dataframe.iloc[row]["tip_2d_x"]
            bur_tip_y = selected_data.coordinate_dataframe.iloc[row]["tip_2d_y"]
            tool_center_3d = selected_data.coordinate_dataframe.iloc[row][CoordinateDataKeys.TOOL_CENTER_3D].to_numpy()
            rotation_matrix = (
                selected_data.coordinate_dataframe.iloc[row][CoordinateDataKeys.TOOL_ROTATION_MATRIX].to_numpy().reshape(-1, 3, 3)
            ).transpose(0, 2, 1)[0]
            tool_center_2d = selected_data.coordinate_dataframe.iloc[row][CoordinateDataKeys.TOOL_CENTER_2D].to_numpy()

            # Get relative path to maintain folder structure
            relative_path = os.path.relpath(image_path, file_path)
            output_subfolder = os.path.join(output_folder, os.path.dirname(relative_path))

            try:
                print(f"Processed row {row+1}/{max_row}: {image_path}")
                laplacian_blur, fft_blur = save_cropped_frame(
                    output_size,
                    width,
                    height,
                    tool_center_2d,
                    tool_center_3d,
                    rotation_matrix,
                    bur_tip_x,
                    bur_tip_y,
                    camera_parameters,
                    image,
                    image_name,
                    quality_value,
                    movement_value,
                    output_subfolder
                )
                laplacian_blurs.append(laplacian_blur)
                fft_blurs.append(fft_blur)

                # Store the calculated timestamp_offset if needed for further processing
                print(f"Timestamp Offset for {image_name}: {timestamp_offset:.4f}")

            except Exception as e:
                print(f"Failed to write {image_name}: {e}")

results_df = pd.DataFrame({
    'timestamp': timestamps,
    'laplacian_blur': laplacian_blurs,
    'fft_blur': fft_blurs,
    'movement': movements
})
output_csv_path = os.path.join(output_folder, 'blur_analysis_results.csv')
results_df.to_csv(output_csv_path, index=False)
print(f"Results saved to {output_csv_path}")

if timestamps:
    plot_metrics(timestamps, laplacian_blurs, "Laplacian Blur", os.path.join(output_folder, "laplacian_blur_plot.png"))
    plot_metrics(timestamps, fft_blurs, "FFT Blur", os.path.join(output_folder, "fft_blur_plot.png"))
    plot_metrics(timestamps, movements, "Movement", os.path.join(output_folder, "movement_plot.png"))
if name == 'main':
parser = argparse.ArgumentParser(description="Process parameters")
parser.add_argument('--file_path', type=str, required=True, help='Path to the data files.')
parser.add_argument('--output_size', type=int, required=True, help='Input size for the MV model.')
parser.add_argument('--UHD', action='store_true', help='Flag to set UHD (4K resolution).')
parser.add_argument('--output_folder', type=str, required=True, help='Path to save output images and CSV with the same subfolder structure as file_path.')
parser.add_argument('--framerate', type=float, required=True, help='Framerate input from user.')
parser.add_argument('--exposure_time', type=float, required=True, help='Exposure time input from user.')


args = parser.parse_args()
main(args.file_path, args.UHD, args.output_size, args.output_folder, args.framerate, args.exposure_time)