要判断一个东西是否在一个三维区域的内部还是外部是很简单的,但是要直观地展示出来这种三维空间的内外关系,需要一些技巧。这里举个例子。

先看结果:

2025-05-21T08:14:39.png

如上图所示,目标分子使用散点表示,如果在四面体内部则标记为绿色,如果在外部则标记为红色。这样能够已经能够比较好地表示内外的情况了。但是pdf不像html支持三维查看,所以三维只能看到某一个视角,就容易产生视觉上的误差。

因此更准确详细的做法,可以是计算这些点在四面体上各个面上的投影情况。即以四面体每个面作为新的局部坐标系的xy平面,然后坐标系z轴正放下给指向四面体内部,x轴方向和指定棱保持一直,局部坐标系的原点为这个面三角形的几何中心。然后完成点坐标在不同局部坐标系的映射即可。此外二维作图的时候,还可以使用折线表示点的轨迹,折线的颜色表示点的时间。

然后三维和二维的作图相结合,就可以把时空关系充分地直观地展示出来。

把上述需求稍微整理细化,丢给grok:

写一个python函数:
函数输入是一个pd.DataFrame,包含这些列:Frame,O_x,O_y,O_z,A_x,A_y,A_z,B_x,B_y,B_z,C_x,C_y,C_z,D_x,D_y,D_z。每一行代表O、A、B、C、D五个点在特定时间帧的空间坐标。ABCD四个点组成了一个四面体。
以下是函数内部要做的事情:
1. 对所有时间点的A,B,C,D四个点的坐标求平均,从而得到平均四面体ABCD,这个平均四面体的坐标可使用字典存储其四个点坐标,是返回值之一。
2. 以平均四面体ABCD上的面ABC创建一个局部直角坐标系,这个局部直角坐标系的原点是面的几何中心,局部直角坐标系的z轴方向就是这个面的朝向四面体内部的方向,局部直角坐标系的x轴与AB平行且方向相同,局部直角坐标系的y轴的方向则朝向C点一侧。
3. 建立好这样一个局部直角坐标系后,计算五个点在所有时间点上的坐标在这个新的局部直角坐标系的投影坐标,请注意规范命名变量以存储新的坐标,以O点为例,新的坐标为 O_x_ABC, 后缀ABC表示局部直角坐标系的xy平面。
4. 以平均四面体ABCD上的面ABD创建一个局部直角坐标系,这个局部直角坐标系的原点是面的几何中心,局部直角坐标系的z轴方向就是这个面的朝向四面体内部的方向,局部直角坐标系的x轴与AB平行且方向相同,局部直角坐标系的y轴的方向则朝向D点一侧。
5. 建立好这样一个局部直角坐标系后,计算五个点在所有时间点上的坐标在这个新的局部直角坐标系的投影坐标,请注意规范命名变量以存储新的坐标,以O点为例,新的坐标为 O_x_ABD, 后缀ABD表示局部直角坐标系的xy平面。
6. 以平均四面体ABCD上的面BCD创建一个局部直角坐标系,这个局部直角坐标系的原点是面的几何中心,局部直角坐标系的z轴方向就是这个面的朝向四面体内部的方向,局部直角坐标系的x轴与BC平行且方向相同,局部直角坐标系的y轴的方向则朝向D点一侧。
7. 建立好这样一个局部直角坐标系后,计算五个点在所有时间点上的坐标在这个新的局部直角坐标系的投影坐标,请注意规范命名变量以存储新的坐标,以O点为例,新的坐标为 O_x_BCD, 后缀BCD表示局部直角坐标系的xy平面。
8. 以平均四面体ABCD上的面CAD创建一个局部直角坐标系,这个局部直角坐标系的原点是面的几何中心,局部直角坐标系的z轴方向就是这个面的朝向四面体内部的方向,局部直角坐标系的x轴与CA平行且方向相同,局部直角坐标系的y轴的方向则朝向D点一侧。
9. 建立好这样一个局部直角坐标系后,计算五个点在所有时间点上的坐标在这个新的局部直角坐标系的投影坐标,请注意规范命名变量以存储新的坐标,以O点为例,新的坐标为 O_x_CAD, 后缀CAD表示局部直角坐标系的xy平面。
10. 将上述计算得到的所有坐标作为新增列添加到输入的 pd.DataFrame,作为函数的返回值之一。

通过少量测试和修改,就实现了这样的坐标系变化。接下来就是对结果进行可视化。这个迭代修改的次数比较多,所以就不放记录了,感兴趣的大家自己看我分享的grok聊天记录吧

该功能的输入(数据表)是之前一篇文章的输出。

代码如下:

import pandas as pd
import numpy as np
from glob import glob
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection


def process_tetrahedron_coordinates(df):
    """
    Process tetrahedron coordinates and compute projections in local coordinate systems.
    
    Parameters:
    df (pd.DataFrame): Input DataFrame with columns Frame, O_x, O_y, O_z, A_x, A_y, A_z, 
                      B_x, B_y, B_z, C_x, C_y, C_z, D_x, D_y, D_z
    
    Returns:
    tuple: (mean_tetrahedron, result_df)
        - mean_tetrahedron: dict with mean coordinates of points A, B, C, D
        - result_df: DataFrame with original data and new projected coordinates
    """
    # 1. Calculate mean coordinates for points A, B, C, D
    mean_tetrahedron = {
        'A': np.array([df['A_x'].mean(), df['A_y'].mean(), df['A_z'].mean()]),
        'B': np.array([df['B_x'].mean(), df['B_y'].mean(), df['B_z'].mean()]),
        'C': np.array([df['C_x'].mean(), df['C_y'].mean(), df['C_z'].mean()]),
        'D': np.array([df['D_x'].mean(), df['D_y'].mean(), df['D_z'].mean()])
    }
    
    # Initialize result DataFrame
    result_df = df.copy()
    
    # Define faces and their corresponding configurations
    faces = [
        ('ABC', ['A', 'B', 'C'], ['A', 'B'], 'C'),
        ('ABD', ['A', 'B', 'D'], ['A', 'B'], 'D'),
        ('BCD', ['B', 'C', 'D'], ['B', 'C'], 'D'),
        ('CAD', ['C', 'A', 'D'], ['C', 'A'], 'D')
    ]
    
    def create_local_coordinate_system(face_points, x_axis_points, y_side_point, mean_tetrahedron):
        """Create local coordinate system for a face."""
        # Get mean coordinates
        p1, p2, p3 = [mean_tetrahedron[p] for p in face_points]
        
        # Calculate face center (origin)
        origin = (p1 + p2 + p3) / 3
        
        # Calculate normal vector (z-axis) pointing inside tetrahedron
        v1 = p2 - p1
        v2 = p3 - p1
        normal = np.cross(v1, v2)
        # Ensure normal points inside tetrahedron
        to_center = mean_tetrahedron[list(set(['A', 'B', 'C', 'D']) - set(face_points))[0]] - origin
        if np.dot(normal, to_center) > 0:
            normal = -normal
        z_axis = normal / np.linalg.norm(normal)
        
        # x-axis along first edge
        x_axis = mean_tetrahedron[x_axis_points[1]] - mean_tetrahedron[x_axis_points[0]]
        x_axis = x_axis / np.linalg.norm(x_axis)
        
        # y-axis perpendicular to x and z, towards y_side_point
        y_axis = np.cross(z_axis, x_axis)
        to_y_point = mean_tetrahedron[y_side_point] - origin
        if np.dot(y_axis, to_y_point) < 0:
            y_axis = -y_axis
        y_axis = y_axis / np.linalg.norm(y_axis)
        
        # Create transformation matrix
        transform_matrix = np.vstack([x_axis, y_axis, z_axis]).T
        
        return origin, transform_matrix
    
    def project_coordinates(coordinates, origin, transform_matrix):
        """Project coordinates to local coordinate system."""
        # Translate to origin and project
        translated = coordinates - origin
        projected = np.dot(translated, transform_matrix)
        return projected[:, 0], projected[:, 1], projected[:, 2]
    
    # Process each face
    for face_name, face_points, x_axis_points, y_side_point in faces:
        # Create local coordinate system
        origin, transform_matrix = create_local_coordinate_system(
            face_points, x_axis_points, y_side_point, mean_tetrahedron
        )
        
        # Project coordinates for all points (O, A, B, C, D)
        for point in ['O', 'A', 'B', 'C', 'D']:
            coords = df[[f'{point}_x', f'{point}_y', f'{point}_z']].values
            x_new, y_new, z_new = project_coordinates(coords, origin, transform_matrix)
            
            # Add new coordinates to result DataFrame
            result_df[f'{point}_x_{face_name}'] = x_new
            result_df[f'{point}_y_{face_name}'] = y_new
            result_df[f'{point}_z_{face_name}'] = z_new
    
    return mean_tetrahedron, result_df


def visualize_tetrahedron_projections(results_df, sample):
    """
    Create a 4x2 grid of plots for visualizing tetrahedron point projections.
    For each face, plot the mean coordinates of the three face points as a triangle
    with labeled vertices in xy-plane, and a different triangle in xz-plane.
    Point O is plotted as a colored line based on Frame order.
    All axes have equal scale. XZ-plane z-coordinates are transformed to positive.
    Each subplot's axis range is adjusted to center the triangle.
    No legend is shown. Colorbar is displayed at the bottom.
    
    Parameters:
    results_df (pd.DataFrame): DataFrame containing projected coordinates with columns
                              like O_x_ABC, O_y_ABC, O_z_ABC, etc.
    
    Returns:
    None (saves figure as 'tetrahedron_projections.png')
    """
    # Define face suffixes, titles, and points for each face (xy and xz planes)
    faces = [
        ('ABC', 'Face ABC', ['A', 'B', 'C'], ['A', 'B', 'D']),
        ('ABD', 'Face ABD', ['A', 'B', 'D'], ['A', 'B', 'C']),
        ('BCD', 'Face BCD', ['B', 'C', 'D'], ['B', 'C', 'A']),
        ('CAD', 'Face CAD', ['C', 'A', 'D'], ['A', 'B', 'C'])
    ]
    
    # Create 4x2 subplot grid
    fig, axes = plt.subplots(4, 2, figsize=(12, 16))
    plt.subplots_adjust(hspace=0.4, wspace=0.3, bottom=0.15)  # Adjust bottom for colorbar
    
    # Get colormap for point O's line
    cmap = cm.get_cmap('viridis')
    
    for row, (face_suffix, face_title, xy_points, xz_points) in enumerate(faces):
        # Get axes for this row
        ax_xy = axes[row, 0]
        ax_xz = axes[row, 1]
        
        # Calculate mean coordinates for xy-plane points
        mean_coords_xy = {
            point: {
                'x': results_df[f'{point}_x_{face_suffix}'].mean(),
                'y': results_df[f'{point}_y_{face_suffix}'].mean()
            } for point in xy_points
        }
        
        # Calculate mean coordinates for xz-plane points (z transformed to positive)
        mean_coords_xz = {
            point: {
                'x': results_df[f'{point}_x_{face_suffix}'].mean(),
                'z': -results_df[f'{point}_z_{face_suffix}'].mean()  # Negate z
            } for point in xz_points
        }
        
        # Calculate axis ranges for xy-plane to center the triangle
        xy_x_coords = [mean_coords_xy[p]['x'] for p in xy_points] + list(results_df[f'O_x_{face_suffix}'])
        xy_y_coords = [mean_coords_xy[p]['y'] for p in xy_points] + list(results_df[f'O_y_{face_suffix}'])
        xy_x_min, xy_x_max = np.min(xy_x_coords), np.max(xy_x_coords)
        xy_y_min, xy_y_max = np.min(xy_y_coords), np.max(xy_y_coords)
        xy_x_range = xy_x_max - xy_x_min
        xy_y_range = xy_y_max - xy_y_min
        xy_range = max(xy_x_range, xy_y_range) * 1.2  # Add 20% padding
        xy_x_center = (xy_x_max + xy_x_min) / 2
        xy_y_center = (xy_y_max + xy_y_min) / 2
        
        # Calculate axis ranges for xz-plane to center the triangle
        xz_x_coords = [mean_coords_xz[p]['x'] for p in xz_points] + list(results_df[f'O_x_{face_suffix}'])
        xz_z_coords = [mean_coords_xz[p]['z'] for p in xz_points] + list(-results_df[f'O_z_{face_suffix}'])
        xz_x_min, xz_x_max = np.min(xz_x_coords), np.max(xz_x_coords)
        xz_z_min, xz_z_max = np.min(xz_z_coords), np.max(xz_z_coords)
        xz_x_range = xz_x_max - xz_x_min
        xz_z_range = xz_z_max - xz_z_min
        xz_range = max(xz_x_range, xz_z_range) * 1.2  # Add 20% padding
        xz_x_center = (xz_x_max + xz_x_min) / 2
        xz_z_center = (xz_z_max + xz_z_min) / 2
        
        # Plot triangle for xy-plane
        triangle_xy = np.array([
            [mean_coords_xy[p]['x'] for p in xy_points + [xy_points[0]]],
            [mean_coords_xy[p]['y'] for p in xy_points + [xy_points[0]]]
        ]).T
        ax_xy.plot(triangle_xy[:, 0], triangle_xy[:, 1], 'gray', alpha=0.5)
        
        # Plot triangle for xz-plane (z transformed to positive)
        triangle_xz = np.array([
            [mean_coords_xz[p]['x'] for p in xz_points + [xz_points[0]]],
            [mean_coords_xz[p]['z'] for p in xz_points + [xz_points[0]]]
        ]).T
        ax_xz.plot(triangle_xz[:, 0], triangle_xz[:, 1], 'gray', alpha=0.5)
        
        # Plot and label mean points for xy-plane
        for point in xy_points:
            ax_xy.scatter(
                mean_coords_xy[point]['x'], mean_coords_xy[point]['y'],
                c='gray', alpha=0.7
            )
            ax_xy.text(
                mean_coords_xy[point]['x'], mean_coords_xy[point]['y'],
                point, fontsize=8, ha='right', va='bottom'
            )
        
        # Plot and label mean points for xz-plane (z transformed)
        for point in xz_points:
            ax_xz.scatter(
                mean_coords_xz[point]['x'], mean_coords_xz[point]['z'],
                c='gray', alpha=0.7
            )
            ax_xz.text(
                mean_coords_xz[point]['x'], mean_coords_xz[point]['z'],
                point, fontsize=8, ha='right', va='bottom'
            )
        
        # Plot point O as a colored line based on Frame order
        norm = plt.Normalize(results_df['Frame'].min(), results_df['Frame'].max())
        points_xy = np.array([
            results_df[f'O_x_{face_suffix}'],
            results_df[f'O_y_{face_suffix}']
        ]).T
        points_xz = np.array([
            results_df[f'O_x_{face_suffix}'],
            -results_df[f'O_z_{face_suffix}']  # Negate z for O
        ]).T
        segments_xy = np.array([points_xy[:-1], points_xy[1:]]).transpose(1, 0, 2)
        segments_xz = np.array([points_xz[:-1], points_xz[1:]]).transpose(1, 0, 2)
        
        # Create line collections for colored lines
        lc_xy = LineCollection(
            segments_xy,
            cmap=cmap,
            norm=norm
        )
        lc_xz = LineCollection(
            segments_xz,
            cmap=cmap,
            norm=norm
        )
        
        # Set line colors based on Frame
        lc_xy.set_array(results_df['Frame'][:-1])
        lc_xz.set_array(results_df['Frame'][:-1])
        
        # Add line collections to plots
        ax_xy.add_collection(lc_xy)
        ax_xz.add_collection(lc_xz)
        
        # Set titles and labels
        ax_xy.set_title(f'{face_title} - XY Plane')
        ax_xz.set_title(f'{face_title} - XZ Plane')
        ax_xy.set_xlabel('X' if row == 3 else '')
        ax_xz.set_xlabel('X' if row == 3 else '')
        ax_xy.set_ylabel('Y')
        ax_xz.set_ylabel('Z')
        
        # Set equal aspect ratio and centered axis ranges
        ax_xy.set_aspect('equal')
        ax_xz.set_aspect('equal')
        ax_xy.set_xlim(xy_x_center - xy_range / 2, xy_x_center + xy_range / 2)
        ax_xy.set_ylim(xy_y_center - xy_range / 2, xy_y_center + xy_range / 2)
        ax_xz.set_xlim(xz_x_center - xz_range / 2, xz_x_center + xz_range / 2)
        ax_xz.set_ylim(xz_z_center - xz_range / 2, xz_z_center + xz_range / 2)
        
        # Add grid
        ax_xy.grid(True)
        ax_xz.grid(True)
        
    # Add colorbar at the bottom
    cbar = fig.colorbar(
        cm.ScalarMappable(norm=norm, cmap=cmap),
        ax=axes,
        location='bottom',
        fraction=0.05,
        pad=0.04,
        label='Frame'
    )
    
    # Save the figure
    plt.savefig(f'{sample}_2d_projections.png', dpi=300, bbox_inches='tight')
    plt.close()


def visualize_mean_tetrahedron(mean_tetrahedron, results_df, sample):
    """
    Create a 3D plot of the mean tetrahedron.
    Points A, B, C, D are plotted as gray scatter points with labels including coordinates.
    Tetrahedron surfaces are gray with transparency (alpha=0.7).
    Edges are solid gray lines.
    Point O is plotted as scatter points, green if inside the tetrahedron, red if outside.
    
    Parameters:
    mean_tetrahedron (dict): Dictionary with mean coordinates for points A, B, C, D
                            (e.g., {'A': [x, y, z], 'B': [x, y, z], ...})
    results_df (pd.DataFrame): DataFrame containing O_x, O_y, O_z coordinates
    
    Returns:
    None (saves figure as 'mean_tetrahedron_3d.png')
    """
    # Create figure and 3D axes
    fig = plt.figure(figsize=(8, 8))
    ax = fig.add_subplot(111, projection='3d')
    
    # Extract coordinates for A, B, C, D
    points = np.array([mean_tetrahedron[p] for p in ['A', 'B', 'C', 'D']])
    x, y, z = points[:, 0], points[:, 1], points[:, 2]
    
    # Define tetrahedron faces (vertices for each face)
    faces = [
        [points[0], points[1], points[2]],  # ABC
        [points[0], points[1], points[3]],  # ABD
        [points[1], points[2], points[3]],  # BCD
        [points[2], points[0], points[3]]   # CAD
    ]
    
    # Define edges (all unique pairs of points)
    edges = [
        (0, 1),  # A-B
        (0, 2),  # A-C
        (0, 3),  # A-D
        (1, 2),  # B-C
        (1, 3),  # B-D
        (2, 3)   # C-D
    ]
    
    # Function to compute barycentric coordinates
    def barycentric_coordinates(p, tetra_points):
        a, b, c, d = tetra_points
        T = np.array([b - a, c - a, d - a]).T
        try:
            bary = np.linalg.solve(T, p - a)
            # Append the first coordinate (1 - sum of others)
            bary = np.append(bary, 1 - np.sum(bary))
            return bary
        except np.linalg.LinAlgError:
            # If singular matrix, return invalid coordinates
            return np.array([-1, -1, -1, -1])
    
    # Extract O coordinates
    o_points = np.array([results_df['O_x'], results_df['O_y'], results_df['O_z']]).T
    
    # Determine if O points are inside or outside
    inside = []
    for o in o_points:
        # Compute barycentric coordinates
        bary = barycentric_coordinates(o, points)
        # Point is inside if all barycentric coordinates are in [0, 1] and sum to ~1
        is_inside = np.all(bary >= -1e-10) and np.all(bary <= 1 + 1e-10) and abs(np.sum(bary) - 1) < 1e-10
        inside.append(is_inside)
    
    inside = np.array(inside)
    
    # Plot scatter points for A, B, C, D
    ax.scatter(x, y, z, c='gray', s=50)
    
    # Add labels for points with coordinates (rounded to 1 decimal place)
    for i, point in enumerate(['A', 'B', 'C', 'D']):
        coords = mean_tetrahedron[point]
        label = f"{point}({coords[0]:.1f}, {coords[1]:.1f}, {coords[2]:.1f})"
        ax.text(points[i][0], points[i][1], points[i][2], label, fontsize=8, ha='right', va='bottom')
    
    # Plot scatter points for O (green for inside, red for outside)
    ax.scatter(o_points[inside][:, 0], o_points[inside][:, 1], o_points[inside][:, 2], 
               c='green', s=15, label='O (inside)', alpha=0.1)
    ax.scatter(o_points[~inside][:, 0], o_points[~inside][:, 1], o_points[~inside][:, 2], 
               c='red', s=15, label='O (outside)', alpha=0.1)
    
    # Plot tetrahedron surfaces with transparency
    poly = Poly3DCollection(faces, facecolors='gray', alpha=0.2, edgecolors='none')
    ax.add_collection3d(poly)
    
    # Plot edges
    for edge in edges:
        edge_points = points[list(edge)]
        ax.plot(edge_points[:, 0], edge_points[:, 1], edge_points[:, 2], 'gray', linewidth=1.5)
    
    # Set labels
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    
    # Set equal aspect ratio
    ax.set_box_aspect([1, 1, 1])
    
    # Calculate axis limits to center the tetrahedron and O points
    all_points = np.vstack([points, o_points])
    all_coords = [all_points[:, i] for i in range(3)]
    min_vals = np.min(all_coords, axis=1)
    max_vals = np.max(all_coords, axis=1)
    ranges = max_vals - min_vals
    max_range = np.max(ranges) * 1.2  # Add 20% padding
    centers = (max_vals + min_vals) / 2
    ax.set_xlim(centers[0] - max_range / 2, centers[0] + max_range / 2)
    ax.set_ylim(centers[1] - max_range / 2, centers[1] + max_range / 2)
    ax.set_zlim(centers[2] - max_range / 2, centers[2] + max_range / 2)
    
    # Add grid
    ax.grid(True)
    
    # Add legend for O points
    ax.legend(loc='upper right')
    
    # Save the figure
    plt.savefig(f'{sample}_3d-view-in-out.png', dpi=300, bbox_inches='tight')
    plt.close()


def main():
    fps = glob("results/*.csv")
    for fp in fps:
        df = pd.read_csv(fp)
        sample = fp.split(".")[0]
        tdn, df2 = process_tetrahedron_coordinates(df)
        visualize_mean_tetrahedron(tdn, df2, sample)
        visualize_tetrahedron_projections(df2, sample)
        df2.to_csv(f'{sample}_results.csv', index=None)


if __name__=='__main__':
    main()
最后修改:2025 年 05 月 21 日
请大力赞赏以支持本站持续运行!