Tuesday, July 22, 2025

Java Anatomy

# Java Swing Anatomy Image Animation with Frame Time Limit

Here's a complete Java Swing application that demonstrates how to create an anatomy image animation with frame time limits:

```java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.util.*;
import java.io.*;
import javax.imageio.*;

public class AnatomyAnimation extends JFrame {
    private AnimationPanel animationPanel;
    private ArrayList<BufferedImage> anatomyLayers = new ArrayList<>();
    private int currentLayerIndex = 0;
    private Timer animationTimer;
    private int frameDelay = 100; // milliseconds between frames
    private int totalFrames = 10; // Number of frames per layer
    private int currentFrame = 0;
    private JSlider frameSlider, speedSlider;
    private JButton loadButton, playButton, saveButton;

    public AnatomyAnimation() {
        setTitle("Anatomy Image Animation");
        setSize(1000, 800);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout());

        // Create animation panel
        animationPanel = new AnimationPanel();
        add(animationPanel, BorderLayout.CENTER);

        // Create control panel
        JPanel controlPanel = new JPanel(new GridLayout(1, 5));

        // Frame limit control
        frameSlider = new JSlider(1, 30, totalFrames);
        frameSlider.setMajorTickSpacing(5);
        frameSlider.setMinorTickSpacing(1);
        frameSlider.setPaintTicks(true);
        frameSlider.setPaintLabels(true);
        frameSlider.addChangeListener(e -> {
            totalFrames = frameSlider.getValue();
        });

        // Animation speed control
        speedSlider = new JSlider(10, 500, frameDelay);
        speedSlider.setInverted(true);
        speedSlider.setMajorTickSpacing(100);
        speedSlider.setMinorTickSpacing(50);
        speedSlider.setPaintTicks(true);
        speedSlider.setPaintLabels(true);
        speedSlider.addChangeListener(e -> {
            frameDelay = speedSlider.getValue();
            if (animationTimer != null) {
                animationTimer.setDelay(frameDelay);
            }
        });

        // Load button
        loadButton = new JButton("Load Layers");
        loadButton.addActionListener(e -> loadAnatomyLayers());

        // Play button
        playButton = new JButton("Play");
        playButton.addActionListener(e -> toggleAnimation());

        // Save button
        saveButton = new JButton("Save Animation");
        saveButton.addActionListener(e -> saveAnimation());

        controlPanel.add(new JLabel("Frames per layer:"));
        controlPanel.add(frameSlider);
        controlPanel.add(new JLabel("Speed:"));
        controlPanel.add(speedSlider);
        controlPanel.add(loadButton);
        controlPanel.add(playButton);
        controlPanel.add(saveButton);

        add(controlPanel, BorderLayout.SOUTH);

        // Initialize timer
        animationTimer = new Timer(frameDelay, e -> {
            currentFrame++;
            if (currentFrame >= totalFrames) {
                currentFrame = 0;
                currentLayerIndex = (currentLayerIndex + 1) % anatomyLayers.size();
            }
            animationPanel.repaint();
        });
    }

    private void loadAnatomyLayers() {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setMultiSelectionEnabled(true);
        fileChooser.setDialogTitle("Select Anatomy Layer Images");
        
        if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            anatomyLayers.clear();
            for (File file : fileChooser.getSelectedFiles()) {
                try {
                    BufferedImage img = ImageIO.read(file);
                    anatomyLayers.add(img);
                } catch (IOException ex) {
                    JOptionPane.showMessageDialog(this, "Error loading image: " + ex.getMessage(),
                        "Error", JOptionPane.ERROR_MESSAGE);
                }
            }
            if (!anatomyLayers.isEmpty()) {
                currentLayerIndex = 0;
                currentFrame = 0;
                animationPanel.repaint();
            }
        }
    }

    private void toggleAnimation() {
        if (anatomyLayers.isEmpty()) {
            JOptionPane.showMessageDialog(this, "Please load anatomy layers first");
            return;
        }

        if (animationTimer.isRunning()) {
            animationTimer.stop();
            playButton.setText("Play");
        } else {
            animationTimer.start();
            playButton.setText("Stop");
        }
    }

    private void saveAnimation() {
        if (anatomyLayers.isEmpty()) {
            JOptionPane.showMessageDialog(this, "No animation to save");
            return;
        }

        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setDialogTitle("Save Animation as GIF");
        fileChooser.setSelectedFile(new File("anatomy_animation.gif"));
        
        if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
            File outputFile = fileChooser.getSelectedFile();
            try {
                // Create a GIF writer
                ImageWriter writer = ImageIO.getImageWritersByFormatName("gif").next();
                ImageOutputStream ios = ImageIO.createImageOutputStream(outputFile);
                writer.setOutput(ios);
                
                // Configure GIF metadata
                writer.prepareWriteSequence(null);
                
                // Create each frame
                for (int i = 0; i < anatomyLayers.size(); i++) {
                    BufferedImage frame = createFrame(i);
                    
                    // Write frame with delay
                    IIOMetadataNode node = new IIOMetadataNode("javax_imageio_gif_image_1.0");
                    IIOMetadataNode extension = new IIOMetadataNode("GraphicControlExtension");
                    extension.setAttribute("delayTime", String.valueOf(frameDelay / 10)); // in 1/100ths of a second
                    extension.setAttribute("disposalMethod", "none");
                    extension.setAttribute("userInputFlag", "FALSE");
                    extension.setAttribute("transparentColorIndex", "0");
                    node.appendChild(extension);
                    
                    IIOMetadata metadata = writer.getDefaultImageMetadata(new ImageTypeSpecifier(frame), null);
                    metadata.mergeTree("javax_imageio_gif_image_1.0", node);
                    
                    writer.writeToSequence(new IIOImage(frame, null, metadata), null);
                }
                
                writer.endWriteSequence();
                ios.close();
                
                JOptionPane.showMessageDialog(this, "Animation saved successfully!");
            } catch (IOException ex) {
                JOptionPane.showMessageDialog(this, "Error saving animation: " + ex.getMessage(),
                    "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    private BufferedImage createFrame(int layerIndex) {
        BufferedImage frame = new BufferedImage(
            animationPanel.getWidth(), animationPanel.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = frame.createGraphics();
        
        // Draw all layers up to the current one
        for (int i = 0; i <= layerIndex; i++) {
            BufferedImage layer = anatomyLayers.get(i);
            
            // Calculate alpha (transparency) based on position in sequence
            float alpha = (i == layerIndex) ? 1.0f : 0.7f;
            g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
            
            // Draw layer centered
            int x = (frame.getWidth() - layer.getWidth()) / 2;
            int y = (frame.getHeight() - layer.getHeight()) / 2;
            g2d.drawImage(layer, x, y, null);
        }
        
        g2d.dispose();
        return frame;
    }

    class AnimationPanel extends JPanel {
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g;
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            
            // Draw background
            g2d.setColor(Color.WHITE);
            g2d.fillRect(0, 0, getWidth(), getHeight());
            
            if (!anatomyLayers.isEmpty()) {
                // Calculate current alpha based on frame progress
                float alpha = (float) currentFrame / totalFrames;
                
                // Draw all layers up to current one
                for (int i = 0; i <= currentLayerIndex; i++) {
                    BufferedImage layer = anatomyLayers.get(i);
                    
                    // Set transparency (full for previous layers, animated for current)
                    float layerAlpha = (i < currentLayerIndex) ? 0.7f : alpha;
                    g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, layerAlpha));
                    
                    // Draw layer centered
                    int x = (getWidth() - layer.getWidth()) / 2;
                    int y = (getHeight() - layer.getHeight()) / 2;
                    g2d.drawImage(layer, x, y, null);
                }
                
                // Draw frame info
                g2d.setColor(Color.BLACK);
                g2d.drawString("Layer: " + (currentLayerIndex + 1) + "/" + anatomyLayers.size(), 20, 30);
                g2d.drawString("Frame: " + currentFrame + "/" + totalFrames, 20, 50);
            } else {
                // Show instructions if no layers loaded
                g2d.setColor(Color.BLACK);
                g2d.drawString("Click 'Load Layers' to add anatomy images", getWidth() / 2 - 150, getHeight() / 2);
            }
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            AnatomyAnimation animation = new AnatomyAnimation();
            animation.setVisible(true);
        });
    }
}
```

## Key Features of This Implementation:

1. **Layered Anatomy Visualization**:
   - Load multiple anatomy layer images (e.g., skeletal, muscular, nervous systems)
   - Smooth transitions between layers with alpha blending

2. **Frame Time Control**:
   - Adjustable frames per layer (1-30)
   - Configurable animation speed (10-500ms delay between frames)
   - Real-time preview of animation

3. **Animation Export**:
   - Save animation as GIF with proper frame timing
   - Preserves transparency and layer ordering

4. **User Interface**:
   - Clean visualization panel with centered images
   - Control panel with intuitive sliders
   - Status display showing current layer and frame

5. **Image Handling**:
   - Supports standard image formats (PNG, JPG, etc.)
   - Maintains aspect ratio of original images
   - Handles different image sizes gracefully

## How to Enhance This Application:

1. **Add Layer Labeling**:
```java
// Create a class to hold both image and label
class AnatomyLayer {
    BufferedImage image;
    String label;
    
    public AnatomyLayer(BufferedImage image, String label) {
        this.image = image;
        this.label = label;
    }
}

// Then modify the drawing code to display labels
g2d.drawString(layer.label, x, y - 10);
```

2. **Add Zoom/Pan Functionality**:
```java
// Add these fields to AnimationPanel
private double scale = 1.0;
private Point2D.Double viewPosition = new Point2D.Double(0, 0);

// Modify drawing code to account for zoom/pan
AffineTransform tx = new AffineTransform();
tx.translate(viewPosition.x, viewPosition.y);
tx.scale(scale, scale);
g2d.drawImage(layer, tx, null);

// Add mouse listeners for panning and zooming
addMouseListener(new MouseAdapter() {
    // Implement panning
});

addMouseWheelListener(e -> {
    // Implement zooming
});
```

3. **Add Annotation Tools**:
```java
// Create annotation tools for marking up anatomy
private String currentTool = "pointer"; // or "text", "arrow", etc.

// Add mouse listeners to handle annotations
addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent e) {
        if (currentTool.equals("text")) {
            // Add text annotation
        } else if (currentTool.equals("arrow")) {
            // Start drawing arrow
        }
    }
});
```

4. **Add Layer Reordering**:
```java
// Add buttons to move layers up/down in the stack
JButton upButton = new JButton("Move Up");
upButton.addActionListener(e -> {
    if (currentLayerIndex > 0) {
        Collections.swap(anatomyLayers, currentLayerIndex, currentLayerIndex - 1);
        repaint();
    }
});
```

5. **Add Cross-Section Views**:
```java
// Implement cross-section views by processing images
private BufferedImage createCrossSection(BufferedImage original, int slicePosition) {
    // Create a cross-section view of the anatomy
    BufferedImage crossSection = new BufferedImage(
        original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
    
    // Process pixels to show only the slice
    for (int y = 0; y < original.getHeight(); y++) {
        for (int x = 0; x < original.getWidth(); x++) {
            if (Math.abs(y - slicePosition) < 2) { // Show thin slice
                crossSection.setRGB(x, y, original.getRGB(x, y));
            }
        }
    }
    
    return crossSection;
}
```

This implementation provides a solid foundation for creating anatomy animations with frame time limits in Java Swing. The architecture is designed to be easily extensible with additional features like annotations, cross-sections, and more sophisticated layer management.

No comments: