在grok的帮助下利用plotly打造了一个简单的双通道成像预览函数。这样可以方便集成到数据分析工作流程中,以便实时交互以快速检验情况。

效果如下:

2602291766.png

需要注意的是:

  1. 图像数据会自动转换为 8-bit
  2. 第一个通道默认使用红色,第二个默认绿色伪彩
  3. 对比度方面仅支持通过滑块调整显示的最大值,并且按百分比来
  4. 可以对任意通道或merge子图区域进行同步的zoom in/out

代码如下:

from skimage import io
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

def visualize_channels(ch1, ch2, output_file="channel_visualization.html"):
    """
    Visualize two grayscale images (ch1 and ch2) and their RGB merged image in a single row with three subplots.
    ch1 uses red colormap, ch2 uses green colormap, merged image uses ch1 as R, ch2 as G, B=0.
    Non-8-bit inputs are linearly converted to 8-bit based on dtype.
    A single slider controls zmax for all subplots (mapped from 1-100% to 0-255), with synchronized zooming and 1:1 scale.
    Subplots are displayed with the same aspect ratio and dimensions as the input images.
    Tight layout and no axis background for clean display.
    
    Parameters:
    ch1 (np.array): Grayscale image for channel 1 (R in merged image)
    ch2 (np.array): Grayscale image for channel 2 (G in merged image)
    output_file (str): Path to save the output HTML file
    """
    # Convert inputs to 8-bit if not already
    def to_uint8(arr):
        arr = np.asarray(arr)
        if arr.dtype == np.uint8:
            return arr
        # Linearly scale to 0-255 based on dtype min and max
        dtype_info = np.iinfo(arr.dtype) if np.issubdtype(arr.dtype, np.integer) else np.finfo(arr.dtype)
        arr_min, arr_max = dtype_info.min, dtype_info.max
        arr = (arr - arr_min) / (arr_max - arr_min) * 255
        return np.clip(arr, 0, 255).astype(np.uint8)
    
    ch1 = to_uint8(ch1)
    ch2 = to_uint8(ch2)
    
    # Get image dimensions
    img_height, img_width = ch1.shape
    
    # Calculate aspect ratio (width / height)
    aspect_ratio = img_width / img_height
    
    # Create subplot figure with shared x and y axes for synchronized zooming
    fig = make_subplots(
        rows=1, cols=3, 
        subplot_titles=("Channel 1 (Red)", "Channel 2 (Green)", "Merged (RGB)"),
        shared_xaxes=True, shared_yaxes=True,
        horizontal_spacing=0.02  # Tight layout: minimize spacing between subplots
    )
    
    # Define custom colormaps starting from black
    red_cmap = [[0, "rgb(0,0,0)"], [1, "rgb(255,0,0)"]]
    green_cmap = [[0, "rgb(0,0,0)"], [1, "rgb(0,255,0)"]]
    
    # Add heatmaps for ch1 (red) and ch2 (green)
    fig.add_trace(
        go.Heatmap(z=ch1, colorscale=red_cmap, zmin=0, zmax=255, showscale=False),
        row=1, col=1
    )
    fig.add_trace(
        go.Heatmap(z=ch2, colorscale=green_cmap, zmin=0, zmax=255, showscale=False),
        row=1, col=2
    )
    
    # Create RGB merged image (ch1 as R, ch2 as G, B=0)
    rgb = np.stack([ch1, ch2, np.zeros_like(ch1)], axis=-1).astype(np.uint8)
    
    # Add RGB image for merged subplot
    fig.add_trace(
        go.Image(z=rgb, zmin=[0, 0, 0, 0], zmax=[255, 255, 255, 255], hoverinfo="none"),
        row=1, col=3
    )
    
    # Update axes for synchronized zooming and matching image aspect ratio
    fig.update_xaxes(
        matches='x', 
        scaleanchor='y', 
        scaleratio=1,
        constrain='domain'
    )
    fig.update_yaxes(
        matches='y', 
        scaleanchor='x', 
        scaleratio=1,
        constrain='domain'
    )
    
    # Calculate figure dimensions to match image aspect ratio
    # Base width per subplot, adjusted for three subplots and minimal margins
    subplot_width = 600  # Base width per subplot in pixels
    total_width = subplot_width * 3 + 50  # Account for margins and spacing
    total_height = subplot_width / aspect_ratio + 150  # Adjust height for aspect ratio + margins
    
    # Add single slider for zmax adjustment for all subplots
    fig.update_layout(
        sliders=[
            {
                "active": 99,  # Default to 100% (zmax=255)
                "yanchor": "top",
                "xanchor": "left",
                "currentvalue": {
                    "font": {"size": 16},
                    "prefix": "Contrast (zmax %): ",
                    "visible": True,
                    "xanchor": "right"
                },
                "pad": {"b": 10, "t": 50},
                "len": 0.9,
                "x": 0.05,
                "y": 0,
                "steps": [
                    {
                        "method": "restyle",
                        "label": str(i),
                        "value": str(i),
                        "args": [
                            {
                                "zmax": [
                                    int((i-1)/99*255),  # ch1 (Heatmap)
                                    int((i-1)/99*255),  # ch2 (Heatmap)
                                    [int((i-1)/99*255), int((i-1)/99*255), int((i-1)/99*255)]  # RGB (Image)
                                ]
                            },
                            [0, 1, 2]  # Apply to all three traces
                        ]
                    } for i in range(1, 101, 1)
                ]
            }
        ],
        # Tight layout: minimize margins
        margin=dict(l=10, r=10, t=50, b=100),
        # title="Channel Visualization with Contrast Adjustment",
        width=total_width,
        height=total_height,
        showlegend=False
    )
    
    # Save to HTML
    fig.write_html(output_file, include_plotlyjs="cdn")
    
    return fig


def test():
    ch1 = io.imread('ch1.tif', plugin='pil')
    ch2 = io.imread('ch2.tif', plugin='pil')
    frame1 = ch1[5]
    frame2 = ch2[4]
    visualize_channels(frame1, frame2)

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