
在工程测量、生物医学影像、地质勘探甚至日常摄影中,我们常常需要从图片中提取真实的尺寸信息。当一张图片没有提供比例尺时,如何准确计算图片中物体的实际尺寸?今天我将带你打造一款图片比例尺计算工具,只需点击两下鼠标,输入一个已知距离,就能精确计算出图片的比例尺。
比例尺计算的核心思想非常简单却极其强大:在图片上选择两个已知实际距离的点,通过计算这两个点之间的像素距离,建立像素空间到物理空间的映射关系。这个关系可以用一个简单的公式表示:
其中比例尺的单位是 (毫米/像素),这意味着每个像素代表的实际毫米数。例如,如果比例尺为 ,那么图片中一个 像素的线段就对应现实中的 。
这个工具的实现涉及计算机图形学中的坐标变换、Qt框架的事件处理和Python的高效计算,下面让我们一探究竟。
以下是完整的图片比例尺计算工具代码,整合在一个文件中,无需外部依赖:
import sys
import math
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QLabel,
QPushButton, QLineEdit, QVBoxLayout, QHBoxLayout,
QFileDialog, QMessageBox, QGroupBox, QFrame)
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor, QFont
classImageScaleCalculator(QMainWindow):
"""图片比例尺计算器主窗口类"""
def__init__(self):
super().__init__()
# 初始化变量
self.image_path = None# 图片路径
self.original_pixmap = None# 原始图片
self.display_pixmap = None# 显示用的缩放后图片
self.points = [] # 存储点击的点
self.pixel_distance = 0# 像素距离
self.scale_value = None# 比例尺值
self.init_ui() # 初始化界面
definit_ui(self):
"""初始化用户界面"""
# 窗口基本设置
self.setWindowTitle('图片比例尺计算工具')
self.setGeometry(100, 100, 1000, 700)
# 设置窗口样式
self.setStyleSheet("""
QMainWindow { background-color: #f5f7fa; }
QLabel { font-size: 14px; color: #333; }
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 14px;
font-weight: bold;
transition: background-color 0.3s;
}
QPushButton:hover { background-color: #45a049; }
QPushButton:disabled { background-color: #cccccc; }
QLineEdit {
padding: 8px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
QLineEdit:focus { border-color: #4CAF50; }
QGroupBox {
font-size: 14px;
font-weight: bold;
border: 2px solid #4CAF50;
border-radius: 8px;
margin-top: 10px;
padding-top: 15px;
background-color: white;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 10px 0 10px;
color: #2E7D32;
}
""")
# 创建中央部件和主布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 20, 20, 20)
# 标题区域
title_label = QLabel("📏 图片比例尺计算工具")
title_label.setAlignment(Qt.AlignCenter)
title_font = QFont("Arial", 22, QFont.Bold)
title_label.setFont(title_font)
title_label.setStyleSheet("color: #2E7D32; padding: 15px;")
main_layout.addWidget(title_label)
# 分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
separator.setStyleSheet("background-color: #4CAF50; height: 2px;")
main_layout.addWidget(separator)
# 控制面板
control_group = self.create_control_panel()
main_layout.addWidget(control_group)
# 图像显示区域
image_group = self.create_image_display()
main_layout.addWidget(image_group)
# 计算区域
calculation_group = self.create_calculation_panel()
main_layout.addWidget(calculation_group)
# 使用说明
instructions = QLabel("📝 使用说明: 1) 打开图片 2) 点击两个点 3) 输入实际距离 4) 计算比例尺")
instructions.setAlignment(Qt.AlignCenter)
instructions.setStyleSheet("color: #666; padding: 12px; font-size: 13px;")
main_layout.addWidget(instructions)
# 状态栏
self.statusBar().showMessage("就绪 - 等待打开图片")
defcreate_control_panel(self):
"""创建控制面板"""
group = QGroupBox("🎛️ 控制面板")
layout = QHBoxLayout()
self.open_button = QPushButton("📂 打开图片")
self.open_button.clicked.connect(self.open_image)
layout.addWidget(self.open_button)
self.clear_button = QPushButton("🗑️ 清除标记")
self.clear_button.clicked.connect(self.clear_points)
self.clear_button.setEnabled(False)
layout.addWidget(self.clear_button)
group.setLayout(layout)
return group
defcreate_image_display(self):
"""创建图像显示区域"""
group = QGroupBox("🖼️ 图像显示")
layout = QVBoxLayout()
self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setStyleSheet("""
QLabel {
background-color: white;
border: 2px solid #ddd;
border-radius: 8px;
padding: 5px;
}
""")
self.image_label.setMinimumHeight(450)
self.image_label.mousePressEvent = self.handle_image_click
self.image_status = QLabel("请打开一张图片,然后在图片上点击两个点")
self.image_status.setAlignment(Qt.AlignCenter)
self.image_status.setStyleSheet("font-style: italic; color: #777; padding: 8px;")
layout.addWidget(self.image_label)
layout.addWidget(self.image_status)
group.setLayout(layout)
return group
defcreate_calculation_panel(self):
"""创建计算面板"""
group = QGroupBox("🧮 比例尺计算")
layout = QVBoxLayout()
# 实际距离输入
distance_layout = QHBoxLayout()
distance_label = QLabel("📏 实际距离 (mm):")
distance_label.setFixedWidth(150)
self.distance_input = QLineEdit()
self.distance_input.setPlaceholderText("请输入两点之间的实际距离(毫米)")
distance_layout.addWidget(distance_label)
distance_layout.addWidget(self.distance_input)
layout.addLayout(distance_layout)
# 像素距离显示
pixel_layout = QHBoxLayout()
pixel_label = QLabel("🖥️ 像素距离:")
pixel_label.setFixedWidth(150)
self.pixel_distance_display = QLabel("0 像素")
self.pixel_distance_display.setStyleSheet("font-weight: bold; color: #2196F3; font-size: 15px;")
pixel_layout.addWidget(pixel_label)
pixel_layout.addWidget(self.pixel_distance_display)
layout.addLayout(pixel_layout)
# 计算按钮
self.calculate_button = QPushButton("🧠 计算比例尺")
self.calculate_button.clicked.connect(self.calculate_scale)
self.calculate_button.setEnabled(False)
layout.addWidget(self.calculate_button)
# 比例尺结果显示
result_layout = QHBoxLayout()
result_label = QLabel("📐 比例尺:")
result_label.setFixedWidth(150)
self.scale_result = QLabel("未计算")
self.scale_result.setStyleSheet("font-size: 18px; font-weight: bold; color: #F44336;")
result_layout.addWidget(result_label)
result_layout.addWidget(self.scale_result)
layout.addLayout(result_layout)
# 比例尺解释
explanation = QLabel("📊 比例尺单位: mm/pixel (毫米/像素)")
explanation.setStyleSheet("font-style: italic; color: #666;")
layout.addWidget(explanation)
group.setLayout(layout)
return group
defopen_image(self):
"""打开图片文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择图片文件", "",
"图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.tiff *.webp)"
)
if file_path:
self.image_path = file_path
self.original_pixmap = QPixmap(file_path)
if self.original_pixmap.isNull():
QMessageBox.warning(self, "错误", "无法加载图片文件")
return
# 重置状态
self.reset_calculation_state()
self.display_image()
# 更新界面状态
self.image_status.setText("图片已加载,请在图片上点击选择两个点")
self.statusBar().showMessage(f"已加载图片: {file_path}")
self.clear_button.setEnabled(True)
self.calculate_button.setEnabled(False)
defreset_calculation_state(self):
"""重置计算状态"""
self.points = []
self.pixel_distance = 0
self.scale_value = None
self.distance_input.clear()
self.pixel_distance_display.setText("0 像素")
self.scale_result.setText("未计算")
defdisplay_image(self):
"""显示图片,保持宽高比"""
if self.original_pixmap:
label_size = self.image_label.size()
display_width = label_size.width() - 20
display_height = label_size.height() - 20
self.display_pixmap = self.original_pixmap.scaled(
display_width, display_height,
Qt.KeepAspectRatio, Qt.SmoothTransformation
)
self.image_label.setPixmap(self.display_pixmap)
defhandle_image_click(self, event):
"""处理图片点击事件"""
ifnot self.original_pixmap ornot self.display_pixmap:
return
# 获取点击位置
click_pos = event.pos()
pixmap = self.image_label.pixmap()
if pixmap:
# 计算图片在标签中的偏移量(居中显示)
pixmap_size = pixmap.size()
label_size = self.image_label.size()
x_offset = (label_size.width() - pixmap_size.width()) // 2
y_offset = (label_size.height() - pixmap_size.height()) // 2
# 转换为图片坐标系
image_x = click_pos.x() - x_offset
image_y = click_pos.y() - y_offset
# 确保点击在图片范围内
if0 <= image_x < pixmap_size.width() and0 <= image_y < pixmap_size.height():
# 添加点(最多两个)
self.points.append(QPoint(int(image_x), int(image_y)))
if len(self.points) > 2:
self.points = self.points[-2:]
# 更新显示和状态
self.update_image_display()
self.update_ui_state()
defupdate_image_display(self):
"""更新图片显示,绘制点和线"""
ifnot self.display_pixmap:
return
# 创建图片副本用于绘制
pixmap_copy = self.display_pixmap.copy()
painter = QPainter(pixmap_copy)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制点
for i, point in enumerate(self.points):
# 设置点颜色:第一个点红色,第二个点绿色
color = QColor(255, 50, 50) if i == 0else QColor(50, 220, 50)
painter.setPen(QPen(color, 3))
painter.setBrush(color)
painter.drawEllipse(point, 7, 7)
# 添加点标签
painter.setPen(Qt.black)
painter.setFont(QFont("Arial", 11, QFont.Bold))
label = f"P{i+1}"
painter.drawText(point.x() + 12, point.y() - 12, label)
# 绘制连接线(如果有两个点)
if len(self.points) == 2:
point1, point2 = self.points[0], self.points[1]
# 绘制虚线连接线
painter.setPen(QPen(QColor(30, 120, 220), 2, Qt.DashLine))
painter.drawLine(point1, point2)
# 计算并显示像素距离
self.calculate_pixel_distance()
if self.pixel_distance > 0:
# 在线段中点显示距离
mid_point = QPoint(
(point1.x() + point2.x()) // 2,
(point1.y() + point2.y()) // 2
)
painter.setPen(QPen(QColor(220, 120, 30), 2))
painter.setFont(QFont("Arial", 11, QFont.Bold))
distance_text = f"{self.pixel_distance:.2f} 像素"
text_width = painter.fontMetrics().width(distance_text)
painter.drawText(mid_point.x() - text_width//2, mid_point.y() - 15, distance_text)
painter.end()
self.image_label.setPixmap(pixmap_copy)
defcalculate_pixel_distance(self):
"""计算两点之间的像素距离"""
if len(self.points) == 2:
point1, point2 = self.points[0], self.points[1]
dx = point2.x() - point1.x()
dy = point2.y() - point1.y()
self.pixel_distance = math.sqrt(dx*dx + dy*dy)
self.pixel_distance_display.setText(f"{self.pixel_distance:.2f} 像素")
defupdate_ui_state(self):
"""根据当前状态更新UI"""
if len(self.points) == 1:
self.image_status.setText("已选择第一个点,请点击选择第二个点")
self.statusBar().showMessage("已选择第一个点")
elif len(self.points) == 2:
self.image_status.setText("已选择两个点,请输入实际距离并点击计算按钮")
self.statusBar().showMessage("已选择两个点")
self.calculate_button.setEnabled(True)
defcalculate_scale(self):
"""计算比例尺"""
if len(self.points) != 2or self.pixel_distance == 0:
QMessageBox.warning(self, "错误", "请先选择两个点")
return
# 验证并获取实际距离
try:
actual_distance = float(self.distance_input.text())
if actual_distance <= 0:
raise ValueError("距离必须大于零")
except ValueError as e:
QMessageBox.warning(self, "输入错误", f"请输入有效的实际距离(毫米)\n错误: {str(e)}")
return
# 计算比例尺
self.scale_value = actual_distance / self.pixel_distance
# 更新显示
self.scale_result.setText(f"{self.scale_value:.8f} mm/pixel")
self.statusBar().showMessage(f"比例尺计算完成: {self.scale_value:.8f} mm/pixel")
# 显示详细结果
self.show_detailed_results(actual_distance)
defshow_detailed_results(self, actual_distance):
"""显示详细计算结果"""
info_text = (
f"🔍 比例尺计算结果详情\n\n"
f"📏 实际距离: {actual_distance} mm\n"
f"🖥️ 像素距离: {self.pixel_distance:.2f} 像素\n"
f"📐 比例尺: {self.scale_value:.8f} mm/pixel\n\n"
f"💡 这意味着:\n"
f" • 图片中每个像素代表 {self.scale_value:.8f} 毫米\n"
f" • 100像素的线段 ≈ {100 * self.scale_value:.4f} 毫米\n"
f" • 1毫米 ≈ {1 / self.scale_value:.2f} 像素"
)
QMessageBox.information(self, "计算完成", info_text)
defclear_points(self):
"""清除所有标记点"""
self.points = []
self.pixel_distance = 0
self.pixel_distance_display.setText("0 像素")
self.scale_result.setText("未计算")
self.distance_input.clear()
self.calculate_button.setEnabled(False)
self.image_status.setText("标记已清除,请在图片上点击两个点")
self.statusBar().showMessage("标记已清除")
# 重新显示原始图片
if self.original_pixmap:
self.display_image()
defresizeEvent(self, event):
"""处理窗口大小改变事件"""
super().resizeEvent(event)
if self.original_pixmap:
self.display_image()
if self.points:
self.update_image_display()
defmain():
"""程序入口函数"""
app = QApplication(sys.argv)
app.setStyle('Fusion') # 使用现代化样式
# 设置全局字体
font = QFont("Microsoft YaHei", 10)
app.setFont(font)
# 创建并显示主窗口
calculator = ImageScaleCalculator()
calculator.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
这个工具的核心挑战在于正确处理屏幕坐标、标签坐标和图片坐标之间的转换。当用户点击图片时,我们需要将屏幕坐标 转换为图片坐标 。
转换过程可以表示为:
其中 是图片在标签中的偏移量,由图片居中算法计算得出:
这里 和 是标签的宽度和高度, 和 是图片显示时的宽度和高度。
两点之间的像素距离计算基于欧几里得距离公式:
其中 和 是两个点的坐标。这个距离 是图片空间中的像素数量。
比例尺计算的核心公式是:
其中 是比例尺(单位:), 是实际距离(单位:), 是像素距离(单位:)。
一旦获得比例尺 ,我们就可以计算图片中任意线段的实际长度:
其中 是图片中任意线段的像素长度。
工具使用双缓冲技术避免闪烁:首先将图形绘制到临时的 QPixmap 副本中,然后一次性显示到标签上。点、线和文本的绘制都启用了抗锯齿(QPainter.Antialiasing),确保图形边缘平滑。
点标记使用贝塞尔曲线绘制,通过 drawEllipse 方法实现。第一个点用红色表示,第二个点用绿色表示,这种颜色编码遵循工程制图中的常见约定。
连接线使用虚线样式(Qt.DashLine),这种设计使得测量线在复杂背景图片上仍然清晰可见,同时不会完全遮挡背景细节。
这款比例尺计算工具在实际工作中有广泛的应用价值:
科研图像分析:在生物学研究中,测量显微镜图像中的细胞尺寸;在材料科学中,分析SEM/TEM图像中的微观结构。
工程测量:从工程图纸或现场照片中提取尺寸信息,用于逆向工程或质量控制。
地图制图:计算地图或航拍图像的比例尺,用于距离测量和面积计算。
医学影像:在CT、MRI或X光图像中测量解剖结构的大小。
未来可以扩展的功能包括:
通过这个项目,我们不仅实现了一个实用的图片比例尺计算工具,还深入理解了计算机图形坐标系统、事件处理机制和比例尺计算原理。从像素到现实世界的精确转换,本质上是建立数字世界与物理世界之间桥梁的过程。
这个工具的价值不仅在于其功能本身,更在于它展示了如何将数学原理、编程技术和用户界面设计完美结合,解决实际问题。无论你是科研人员、工程师还是开发者,都可以在此基础上进行定制和扩展,打造更适合自己工作流程的专业工具。
技术的本质是延伸人类的能力,而这款比例尺计算工具正是人类视觉测量能力的数字延伸。在数字时代,掌握这样的工具开发能力,意味着你能在虚拟与现实的交界处创造更多可能性。


陪伴是最长情的告白
为你推送最实用的资讯

识别二维码 关注我们