From 9a11311e7e9a62413184144eabd76ba4eccac8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=81=E5=86=9C=E9=98=BF=E6=A5=A0?= Date: Thu, 16 Oct 2025 01:13:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=8F=AF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + .idea/compiler.xml | 13 + .idea/encodings.xml | 7 + .idea/jarRepositories.xml | 20 + .idea/misc.xml | 12 + .idea/vcs.xml | 6 + pom.xml | 36 ++ src/main/java/org/example/ImageDotMarker.java | 571 ++++++++++++++++++ src/main/java/org/example/Main.java | 21 + 9 files changed, 694 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 pom.xml create mode 100644 src/main/java/org/example/ImageDotMarker.java create mode 100644 src/main/java/org/example/Main.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..5379e33 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..be5f5c6 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d31b37a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3793a77 --- /dev/null +++ b/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.example + mark-tools-java + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + org.example.Main + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/example/ImageDotMarker.java b/src/main/java/org/example/ImageDotMarker.java new file mode 100644 index 0000000..d096a85 --- /dev/null +++ b/src/main/java/org/example/ImageDotMarker.java @@ -0,0 +1,571 @@ +package org.example; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ImageDotMarker extends JFrame { + private BufferedImage originalImage; + private BufferedImage displayImage; + private JLabel imageLabel; + private final List points = new ArrayList<>(); + private DefaultListModel pointListModel; + private JList pointList; + private static final int DOT_RADIUS = 5; + private static final Color DOT_COLOR = Color.RED; // 修复后的颜色常量 + private String currentImagePath; + private int selectedPointIndex = -1; + private double scaleFactor = 1.0; + private JLabel zoomLabel; + private JPanel statusPanel; + + public ImageDotMarker() { + setTitle("图片标点工具"); + setSize(1200, 800); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setLayout(new BorderLayout()); + + // 创建菜单栏 + createMenuBar(); + + // 创建工具栏 + createToolbar(); + + // 创建主面板 + createMainPanel(); + + // 状态栏 + createStatusBar(); + } + + private void createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + JMenu fileMenu = new JMenu("文件"); + JMenuItem openItem = new JMenuItem("打开图片"); + JMenuItem saveItem = new JMenuItem("保存图片"); + JMenuItem exitItem = new JMenuItem("退出"); + + openItem.addActionListener(e -> openImage()); + saveItem.addActionListener(e -> saveImage()); + exitItem.addActionListener(e -> System.exit(0)); + + fileMenu.add(openItem); + fileMenu.add(saveItem); + fileMenu.addSeparator(); + fileMenu.add(exitItem); + menuBar.add(fileMenu); + + JMenu pointMenu = new JMenu("标记点"); + JMenuItem clearItem = new JMenuItem("清除所有标记"); + JMenuItem exportTextItem = new JMenuItem("导出坐标(文本)"); + + clearItem.addActionListener(e -> clearPoints()); + exportTextItem.addActionListener(e -> exportPointsAsText()); + + pointMenu.add(clearItem); + pointMenu.add(exportTextItem); + menuBar.add(pointMenu); + + setJMenuBar(menuBar); + } + + private void createToolbar() { + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable(false); + + JButton openBtn = new JButton("打开"); + openBtn.setToolTipText("打开图片"); + openBtn.addActionListener(e -> openImage()); + + JButton saveBtn = new JButton("保存"); + saveBtn.setToolTipText("保存图片"); + saveBtn.addActionListener(e -> saveImage()); + + JButton clearBtn = new JButton("清除"); + clearBtn.setToolTipText("清除标记"); + clearBtn.addActionListener(e -> clearPoints()); + + JButton exportTextBtn = new JButton("导出文本"); + exportTextBtn.setToolTipText("导出坐标为文本"); + exportTextBtn.addActionListener(e -> exportPointsAsText()); + + // 缩放控件 + JButton zoomInBtn = new JButton("放大"); + zoomInBtn.addActionListener(e -> zoomImage(1.25)); + + JButton zoomOutBtn = new JButton("缩小"); + zoomOutBtn.addActionListener(e -> zoomImage(0.8)); + + JButton resetZoomBtn = new JButton("重置"); + resetZoomBtn.addActionListener(e -> resetZoom()); + + toolBar.add(openBtn); + toolBar.add(saveBtn); + toolBar.addSeparator(); + toolBar.add(clearBtn); + toolBar.add(exportTextBtn); + toolBar.addSeparator(); + toolBar.add(zoomInBtn); + toolBar.add(zoomOutBtn); + toolBar.add(resetZoomBtn); + + add(toolBar, BorderLayout.NORTH); + } + + private void createMainPanel() { + JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + splitPane.setDividerLocation(300); + splitPane.setResizeWeight(0.7); + + // 左侧图片面板 + JPanel imagePanel = new JPanel(new BorderLayout()); + imageLabel = new JLabel(); + imageLabel.setHorizontalAlignment(JLabel.CENTER); + + // 添加鼠标监听器 + imageLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (originalImage != null) { + Point imgPoint = getImagePoint(e.getPoint()); + + if (e.getClickCount() == 2) { + // 双击编辑点 + MarkPoint point = findPointAt(imgPoint); + if (point != null) { + editPointName(point); + } + } else { + // 单击添加点 + addNewPoint(imgPoint); + } + } + } + }); + + // 添加鼠标移动监听器以显示坐标 + imageLabel.addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + if (originalImage != null) { + Point imgPoint = getImagePoint(e.getPoint()); + updateStatusBar("当前位置: (" + imgPoint.x + ", " + imgPoint.y + ")"); + } + } + }); + + imagePanel.add(new JScrollPane(imageLabel), BorderLayout.CENTER); + + // 右侧标记点面板 + JPanel pointPanel = new JPanel(new BorderLayout()); + pointPanel.setBorder(BorderFactory.createTitledBorder("标记点管理")); + + // 点列表 + pointListModel = new DefaultListModel<>(); + pointList = new JList<>(pointListModel); + pointList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + pointList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + selectedPointIndex = pointList.getSelectedIndex(); + if (selectedPointIndex >= 0 && selectedPointIndex < points.size()) { + highlightSelectedPoint(); + } + } + }); + + // 添加双击编辑名称功能 + pointList.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + int index = pointList.locationToIndex(e.getPoint()); + if (index >= 0 && index < points.size()) { + editPointName(points.get(index)); + } + } + } + }); + + JScrollPane listScrollPane = new JScrollPane(pointList); + listScrollPane.setPreferredSize(new Dimension(280, 400)); + + // 点操作按钮 + JPanel buttonPanel = new JPanel(new GridLayout(1, 3, 5, 5)); + + JButton deleteBtn = new JButton("删除"); + deleteBtn.addActionListener(e -> deleteSelectedPoint()); + + JButton moveUpBtn = new JButton("上移"); + moveUpBtn.addActionListener(e -> movePointUp()); + + JButton moveDownBtn = new JButton("下移"); + moveDownBtn.addActionListener(e -> movePointDown()); + + buttonPanel.add(deleteBtn); + buttonPanel.add(moveUpBtn); + buttonPanel.add(moveDownBtn); + + pointPanel.add(listScrollPane, BorderLayout.CENTER); + pointPanel.add(buttonPanel, BorderLayout.SOUTH); + + splitPane.setLeftComponent(imagePanel); + splitPane.setRightComponent(pointPanel); + + add(splitPane, BorderLayout.CENTER); + } + + private void createStatusBar() { + this.statusPanel = new JPanel(new BorderLayout()); + this.statusPanel.setBorder(BorderFactory.createEtchedBorder()); + + JLabel statusLabel = new JLabel("就绪"); + statusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5)); + this.statusPanel.add(statusLabel, BorderLayout.WEST); + + zoomLabel = new JLabel("缩放: 100%"); + zoomLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5)); + this.statusPanel.add(zoomLabel, BorderLayout.EAST); + + add(this.statusPanel, BorderLayout.SOUTH); + } + + private void updateStatusBar() { + updateStatusBar("已添加标记点: " + points.size()); + } + + private void updateStatusBar(String message) { + // 直接使用类成员变量,避免查找和类型转换错误 + JLabel statusLabel = (JLabel) statusPanel.getComponent(0); + statusLabel.setText(message); + } + + private void openImage() { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileFilter(new FileNameExtensionFilter( + "图片文件 (*.jpg, *.jpeg, *.png, *.gif)", "jpg", "jpeg", "png", "gif")); + + int result = fileChooser.showOpenDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + try { + File file = fileChooser.getSelectedFile(); + currentImagePath = file.getAbsolutePath(); + originalImage = ImageIO.read(file); + resetZoom(); + points.clear(); + updatePointList(); + + updateStatusBar("已打开: " + file.getName()); + } catch (IOException ex) { + JOptionPane.showMessageDialog(this, "无法打开图片: " + ex.getMessage()); + } + } + } + + private void saveImage() { + if (originalImage == null) return; + + // 创建带标记点的图片 + BufferedImage markedImage = createMarkedImage(); + + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setSelectedFile(new File("marked_image.png")); + fileChooser.setFileFilter(new FileNameExtensionFilter("PNG图片 (*.png)", "png")); + + int result = fileChooser.showSaveDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + try { + File file = fileChooser.getSelectedFile(); + if (!file.getName().toLowerCase().endsWith(".png")) { + file = new File(file.getAbsolutePath() + ".png"); + } + + ImageIO.write(markedImage, "png", file); + + JOptionPane.showMessageDialog(this, "图片保存成功!"); + updateStatusBar("已保存: " + file.getName()); + } catch (IOException ex) { + JOptionPane.showMessageDialog(this, "保存失败: " + ex.getMessage()); + } + } + } + + private BufferedImage createMarkedImage() { + BufferedImage markedImage = new BufferedImage( + originalImage.getWidth(), + originalImage.getHeight(), + BufferedImage.TYPE_INT_ARGB + ); + + Graphics2D g = markedImage.createGraphics(); + g.drawImage(originalImage, 0, 0, null); + + // 绘制所有标记点 + for (MarkPoint point : points) { + drawPoint(g, point, 1.0); + } + + g.dispose(); + return markedImage; + } + + private void zoomImage(double factor) { + scaleFactor *= factor; + updateImageDisplay(); + zoomLabel.setText(String.format("缩放: %.0f%%", scaleFactor * 100)); + } + + private void resetZoom() { + scaleFactor = 1.0; + updateImageDisplay(); + zoomLabel.setText("缩放: 100%"); + } + + private void updateImageDisplay() { + if (originalImage == null) return; + + int newWidth = (int) (originalImage.getWidth() * scaleFactor); + int newHeight = (int) (originalImage.getHeight() * scaleFactor); + + displayImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = displayImage.createGraphics(); + + // 设置平滑渲染 + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 缩放并绘制原始图片 + AffineTransform transform = AffineTransform.getScaleInstance(scaleFactor, scaleFactor); + g.drawImage(originalImage, transform, this); + + // 绘制所有标记点 + for (MarkPoint point : points) { + drawPoint(g, point, scaleFactor); + } + + g.dispose(); + imageLabel.setIcon(new ImageIcon(displayImage)); + } + + private void drawPoint(Graphics2D g, MarkPoint point, double scale) { + int scaledX = (int) (point.x * scale); + int scaledY = (int) (point.y * scale); + int scaledRadius = (int) (DOT_RADIUS * scale); + + // 绘制点 + g.setColor(DOT_COLOR); + g.fillOval(scaledX - scaledRadius, scaledY - scaledRadius, scaledRadius * 2, scaledRadius * 2); + g.setColor(Color.WHITE); + g.drawOval(scaledX - scaledRadius, scaledY - scaledRadius, scaledRadius * 2, scaledRadius * 2); + + // 绘制点名称 + if (point.name != null && !point.name.isEmpty()) { + g.setColor(Color.BLACK); + g.setFont(new Font("Arial", Font.BOLD, (int)(12 * scale))); + g.drawString(point.name, scaledX + scaledRadius + 2, scaledY); + } + } + + private Point getImagePoint(Point mousePoint) { + Rectangle bounds = imageLabel.getBounds(); + int imgWidth = (int)(originalImage.getWidth() * scaleFactor); + int imgHeight = (int)(originalImage.getHeight() * scaleFactor); + + // 计算图片在标签中的实际显示位置 + int xOffset = (bounds.width - imgWidth) / 2; + int yOffset = (bounds.height - imgHeight) / 2; + + // 计算相对于图片的坐标 + int x = mousePoint.x - xOffset; + int y = mousePoint.y - yOffset; + + // 确保坐标在图片范围内 + x = Math.max(0, Math.min(x, imgWidth - 1)); + y = Math.max(0, Math.min(y, imgHeight - 1)); + + // 转换为原始图片坐标 + return new Point( + (int)(x / scaleFactor), + (int)(y / scaleFactor) + ); + } + + private void addNewPoint(Point imgPoint) { + // 提示用户输入点名称 + String name = JOptionPane.showInputDialog(this, "请输入标记点名称:", "添加标记点", JOptionPane.PLAIN_MESSAGE); + if (name == null) return; // 用户取消 + + MarkPoint point = new MarkPoint(imgPoint.x, imgPoint.y, name); + points.add(point); + updateImageDisplay(); + updatePointList(); + updateStatusBar(); + } + + private MarkPoint findPointAt(Point imgPoint) { + int tolerance = (int)(DOT_RADIUS / scaleFactor); + + for (MarkPoint point : points) { + double distance = Math.sqrt(Math.pow(point.x - imgPoint.x, 2) + Math.pow(point.y - imgPoint.y, 2)); + if (distance <= tolerance) { + return point; + } + } + return null; + } + + private void editPointName(MarkPoint point) { + String newName = JOptionPane.showInputDialog(this, "修改标记点名称:", point.name); + if (newName != null && !newName.equals(point.name)) { + point.name = newName; + updateImageDisplay(); + updatePointList(); + } + } + + private void clearPoints() { + if (originalImage == null) return; + + int confirm = JOptionPane.showConfirmDialog(this, + "确定要清除所有标记点吗?", "清除标记", + JOptionPane.YES_NO_OPTION); + + if (confirm == JOptionPane.YES_OPTION) { + points.clear(); + updateImageDisplay(); + updatePointList(); + updateStatusBar("已清除所有标记点"); + } + } + + private void exportPointsAsText() { + if (points.isEmpty()) { + JOptionPane.showMessageDialog(this, "当前没有标记点"); + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append("图片标记点坐标:\n"); + for (int i = 0; i < points.size(); i++) { + MarkPoint p = points.get(i); + sb.append(String.format("%d. %s: (%d, %d)\n", i + 1, p.name, p.x, p.y)); + } + + JTextArea textArea = new JTextArea(sb.toString()); + textArea.setEditable(false); + JScrollPane scrollPane = new JScrollPane(textArea); + scrollPane.setPreferredSize(new Dimension(300, 200)); + + JOptionPane.showMessageDialog(this, scrollPane, "标记点坐标", JOptionPane.INFORMATION_MESSAGE); + } + + private void updatePointList() { + pointListModel.clear(); + for (int i = 0; i < points.size(); i++) { + MarkPoint p = points.get(i); + pointListModel.addElement(String.format("%d. %s: (%d, %d)", i + 1, p.name, p.x, p.y)); + } + } + + private void deleteSelectedPoint() { + if (selectedPointIndex == -1) { + JOptionPane.showMessageDialog(this, "请先选择一个标记点"); + return; + } + + points.remove(selectedPointIndex); + updateImageDisplay(); + updatePointList(); + selectedPointIndex = -1; + updateStatusBar("已删除标记点"); + } + + private void movePointUp() { + if (selectedPointIndex == -1 || selectedPointIndex == 0) { + return; + } + + MarkPoint point = points.get(selectedPointIndex); + points.remove(selectedPointIndex); + points.add(selectedPointIndex - 1, point); + selectedPointIndex--; + + updateImageDisplay(); + updatePointList(); + pointList.setSelectedIndex(selectedPointIndex); + } + + private void movePointDown() { + if (selectedPointIndex == -1 || selectedPointIndex == points.size() - 1) { + return; + } + + MarkPoint point = points.get(selectedPointIndex); + points.remove(selectedPointIndex); + points.add(selectedPointIndex + 1, point); + selectedPointIndex++; + + updateImageDisplay(); + updatePointList(); + pointList.setSelectedIndex(selectedPointIndex); + } + + private void highlightSelectedPoint() { + if (selectedPointIndex >= 0 && selectedPointIndex < points.size()) { + updateImageDisplay(); + + // 高亮显示选中的点 + MarkPoint p = points.get(selectedPointIndex); + Graphics2D g = displayImage.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int scaledX = (int) (p.x * scaleFactor); + int scaledY = (int) (p.y * scaleFactor); + int scaledRadius = (int) (DOT_RADIUS * scaleFactor); + + // 绘制高亮点 + g.setColor(Color.YELLOW); + g.fillOval(scaledX - scaledRadius, scaledY - scaledRadius, scaledRadius * 2, scaledRadius * 2); + g.setColor(Color.BLACK); + g.drawOval(scaledX - scaledRadius, scaledY - scaledRadius, scaledRadius * 2, scaledRadius * 2); + + // 绘制名称 + if (p.name != null && !p.name.isEmpty()) { + g.setColor(Color.BLUE); + g.setFont(new Font("Arial", Font.BOLD, (int)(12 * scaleFactor))); + g.drawString(p.name, scaledX + scaledRadius + 2, scaledY); + } + + g.dispose(); + imageLabel.setIcon(new ImageIcon(displayImage)); + } + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + ImageDotMarker app = new ImageDotMarker(); + app.setVisible(true); + }); + } + + // 标记点类,包含坐标和名称 + private static class MarkPoint { + int x; + int y; + String name; + + public MarkPoint(int x, int y, String name) { + this.x = x; + this.y = y; + this.name = name; + } + } +} diff --git a/src/main/java/org/example/Main.java b/src/main/java/org/example/Main.java new file mode 100644 index 0000000..f250c86 --- /dev/null +++ b/src/main/java/org/example/Main.java @@ -0,0 +1,21 @@ +package org.example; + +import javax.swing.SwingUtilities; + +/** + * 应用程序主入口类 + * 用于启动图片标点工具应用程序 + */ +public class Main { + /** + * 程序入口方法 + * @param args 命令行参数 + */ + public static void main(String[] args) { + // 使用SwingUtilities.invokeLater确保UI组件在事件调度线程中创建 + SwingUtilities.invokeLater(() -> { + ImageDotMarker app = new ImageDotMarker(); + app.setVisible(true); + }); + } +}