compare python_深入 Python —— == 和 is 的区别

== 和 is 的区别这个问题对于使用过 Python 一段时间开发人员的来说相信不是一个困难的问题。本文将剖析 Python3.6 源码,旨在从实现细节层面把这个问题说清楚,

从字节码看起

我们先来看看 == 和 is 编译后字节码的区别:

In [1]: def test():

...: a = 1

...: b = 1

...: a == b

...: a is b

...:

In [2]: import dis

In [3]: dis.dis(test)

2 0 LOAD_CONST 1 (1)

2 STORE_FAST 0 (a)

3 4 LOAD_CONST 1 (1)

6 STORE_FAST 1 (b)

4 8 LOAD_FAST 0 (a)

10 LOAD_FAST 1 (b)

12 COMPARE_OP 2 (==)

14 POP_TOP

5 16 LOAD_FAST 0 (a)

18 LOAD_FAST 1 (b)

20 COMPARE_OP 8 (is)

22 POP_TOP

24 LOAD_CONST 0 (None)

26 RETURN_VALUE

从字节码可以看出来,is 和 == 都是交给 COMPARE_OP 来执行的,通过 oparg(== 是 2,is 是 8) 参数执行不同的处理,顺藤摸瓜,我们来到 COMPARE_OP:

TARGET(COMPARE_OP) {

PyObject *right = POP();

PyObject *left = TOP();

PyObject *res = cmp_outcome(oparg, left, right);

Py_DECREF(left);

Py_DECREF(right);

SET_TOP(res);

if (res == NULL)

goto error;

PREDICT(POP_JUMP_IF_FALSE);

PREDICT(POP_JUMP_IF_TRUE);

DISPATCH();

}

COMPARE_OP 将待比较的对象和参数又传入到 cmp_outcome:

static PyObject * cmp_outcome(int op, PyObject *v, PyObject *w);

is 比较的本质

先看 cmp_outcome 函数中处理 is 和 is not 的部分:

static PyObject *

cmp_outcome(int op, register PyObject *v, register PyObject *w) {

int res = 0;

switch (op) {

case PyCmp_IS:

res = (v == w);

break;

case PyCmp_IS_NOT:

res = (v != w);

break;

...

}

可以看出,is 和 is not 比较的就是 v 和 w 这俩个指针变量!而指针变量本质上是一个内存地址,它在 32 位系统中就是一个 32 整数,在 64 位系统中就是一个 64 位整数。由此我们可以得出一个结论:is 比较的是俩个对象在内存中是否是同一个地址,换句话说,它们是否是同一个对象。

richcompare

继续往下之前,先来了解下 Python 中 richcompare 的概念。其实不单单是 Python,编程语言应该都会提供 ,>= 这六中比较,在 Python 源码中,它们统称为 richcompare。每一个比较,Python 都提供了一个重载方法和一个参数码,对应关系如下

符号 重载方法 参数码

< __lt__ 0

<= __le__ 1

== __eq__ 2

!= __le__ 3

> __gt__ 4

>= __ge__ 5

可以通过重写上面任意一个或多个方法来重载对应的操作符号,

Python 中每个对象都关联一个类型,类型中有一个 tp_richcompare 函数指针来决定对象之间做 rich compare 时的行为,所有对象的基类型提供了一个默认的实现,我们将在后面介绍。

== 比较的本质

== 比较只是 richcompare 的一种,所有 richcompare 的比较最终都是要交给这个对象关联类型的 tp_richcompare。大部分内置类型,如 int,list,dict 都重写了这个函数,对于用户自建的类型,会优先调用用户重载的方法,没有再调用默认的 tp_richcompare。这只是大概的逻辑,具体到细节,相同类型对象之间、不同类型对象之间、对象与其基类对象之间的比较又有所差异。

继续往下看 cmp_outcome 是怎么处理 richcompare 的:

static PyObject *

cmp_outcome(int op, register PyObject *v, register PyObject *w)

{

...

default:

return PyObject_RichCompare(v, w, op);

}

v = res ? Py_True : Py_False;

Py_INCREF(v);

return v;

}

richcompare 会进入到 PyObject_RichCompare:

PyObject *

PyObject_RichCompare(PyObject *v, PyObject *w, int op)

{

PyObject *res;

assert(Py_LT <= op && op <= Py_GE);

if (v == NULL || w == NULL) {

if (!PyErr_Occurred())

PyErr_BadInternalCall();

return NULL;

}

if (Py_EnterRecursiveCall(" in comparison"))

return NULL;

res = do_richcompare(v, w, op);

Py_LeaveRecursiveCall();

return res;

}

这个函数主要是对参数的检查,真正做事的是 do_richcompare :

/* Perform a rich comparison, raising TypeError when the requested comparisonoperator is not supported. */

static PyObject *

do_richcompare(PyObject *v, PyObject *w, int op)

{

richcmpfunc f;

PyObject *res;

int checked_reverse_op = 0;

/* 第一种情况 */

...

/* 第二种情况 */

...

/* 第三种情况 */

...

/* 第四种情况 */

...

Py_INCREF(res);

return res;

}

这里分为几种情况:v 和 w 类型不同,w 是 v 的子类,w 如果重载了某个 richcompare 方法,则调用 w 中的 richcompare 方法:

if (v->ob_type != w->ob_type &&

PyType_IsSubtype(w->ob_type, v->ob_type) &&

(f = w->ob_type->tp_richcompare) != NULL) {

checked_reverse_op = 1;

res = (*f)(w, v, _Py_SwappedOp[op]);

if (res != Py_NotImplemented)

return res;

Py_DECREF(res);

}

例子:

In [1]: class A:

...: pass

...:

In [2]: class B(A):

...: def __eq__(self, o):

...: print('eq richcompare in B')

...: return True

...:

In [3]: a = A()

In [4]: b = B()

In [5]: a == b

eq richcompare in B

Out[5]: Truev 和 w 类型不同,或者 w 不是 v 的子类,或者 w 没有相应的 richcompare 方法,如果 v 定义了相应的 richcompare 方法,就调用 v 中相应的 richcompare 方法:

if ((f = v->ob_type->tp_richcompare) != NULL) {

res = (*f)(v, w, op);

if (res != Py_NotImplemented)

return res;

Py_DECREF(res);

}

例子:

In [1]: class A:

...: def __eq__(self, o):

...: print('eq richcompare in A')

...:

In [2]: class B:

...: pass

...:

In [3]: class C(A):

...: pass

...:

In [4]: class D(B):

...: def __eq__(self, o):

...: print('eq richcompare in D')

...:

In [5]: a = A()

In [6]: b = B()

In [7]: c = C()

In [8]: d = D()

In [9]: a == b

eq richcompare in A

In [10]: a == c

eq richcompare in A

In [11]: a == d

eq richcompare in Aw 不是 v 的子类,v 中没有定义或者继承相应的 richcompare 方法而 w 中定义了相应的 richcompare 方法,就调用 w 中相应的 richcompare 方法:

if (!checked_reverse_op && (f = w->ob_type->tp_richcompare) != NULL) {

res = (*f)(w, v, _Py_SwappedOp[op]);

if (res != Py_NotImplemented)

return res;

Py_DECREF(res);

}

例子,接第二种情况的例子:

In [12]: c == d

eq richcompare in A

In [13]: b == d

eq richcompare in D

以上三种情况总结起来就是:如果 w 是 v 的子类对象,优先调用 w 相应的 richcompare 方法,否则,v 和和 w 中谁有就调用谁的。如果 v 和 w 都没有相应的 richcompare 方法,那么默认的处理是:

switch (op) {

case Py_EQ:

res = (v == w) ? Py_True : Py_False;

break;

case Py_NE:

res = (v != w) ? Py_True : Py_False;

break;

default:

PyErr_Format(PyExc_TypeError,

"'%s' not supported between instances of '%.100s' and '%.100s'",

opstrings[op],

v->ob_type->tp_name,

w->ob_type->tp_name);

return NULL;

}

可以看到如果比较的是 == 和 !=,结果又回到 v 和 w 指针变量的直接比较,和 is 比较的结果相同,否则会引发一个类型错误。

例子:

In [1]: class A:

...: pass

...:

In [2]: class B:

...: pass

...:

In [3]: a = A()

In [4]: b = B()

In [5]: a == b

Out[5]: False

In [6]: a is b

Out[6]: False

In [7]: a != b

Out[7]: True

In [8]: a is not b

Out[8]: True

In [9]: a > b

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 a > b

TypeError: '>' not supported between instances of 'A' and 'B'

object 的默认 richcompare

所有的类的基类object 提供了一个默认的 richcompare 函数:

PyTypeObject PyBaseObject_Type = {

...

object_richcompare, /* tp_richcompare */

...

}

实现如下:

static PyObject *

object_richcompare(PyObject *self, PyObject *other, int op)

{

PyObject *res;

switch (op) {

case Py_EQ:

/* Return NotImplemented instead of False, so if twoobjects are compared, both get a chance at thecomparison. See issue #1393. */

res = (self == other) ? Py_True : Py_NotImplemented;

Py_INCREF(res);

break;

case Py_NE:

/* By default, __ne__() delegates to __eq__() and inverts the result,unless the latter returns NotImplemented. */

if (self->ob_type->tp_richcompare == NULL) {

res = Py_NotImplemented;

Py_INCREF(res);

break;

}

res = (*self->ob_type->tp_richcompare)(self, other, Py_EQ);

if (res != NULL && res != Py_NotImplemented) {

int ok = PyObject_IsTrue(res);

Py_DECREF(res);

if (ok < 0)

res = NULL;

else {

if (ok)

res = Py_False;

else

res = Py_True;

Py_INCREF(res);

}

}

break;

default:

res = Py_NotImplemented;

Py_INCREF(res);

break;

}

return res;

}

可以看出,对于俩个相同类型的对象而言,== 默认比较的内存地址是否相同,即会否是同一个对象。对于 !=,如果类没有重载 !=(实现 ne),返回 Py_NotImplemented,这时候又回到上面的第 4 种情况,继续比较内存地址。其他比较也是回到上述第 4 中情况,引发类型错误。

例子:

In [1]: class A:

...: pass

...:

In [2]: class B:

...: def __ne__(self, o):

...: print('ne richcompare in B')

...: return False

...:

In [3]: a1 = A()

In [4]: a2 = A()

In [5]: a3 = a1

In [6]: a1 == a2

Out[6]: False

In [7]: a1 == a3

Out[7]: True

In [8]: a1 != a2

Out[8]: True

In [9]: b1 = B()

In [10]: b2 = B()

In [11]: b1 == b2

Out[11]: False

In [12]: b1 != b2

ne richcompare in B

Out[12]: False

In [13]: a1 <= a2

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

in ()

----> 1 a1 <= a2

TypeError: '<=' not supported between instances of 'A' and 'A'

总结

本文深入源码,剖析了 is 和 == 的区别和联系,总的来说就是:is 比较的是俩个对象内存地址是不是一样,即是否是同一个对象

== 是 richcompare 的一种,除非对象的类型重写了 tp_richcompare,否则默认的 == 比较的也是俩个对象的内存地址,和 is 一致

Python 的常用内置类型如 int,string,list,dict 都有默认实现的 tp_richcompare 实现,这个可以另写一篇文章介绍了。

此外,与 Python2 相比,整个比较的逻辑是做了简化的,这里就不剖析 Python2 了,只提一点,Python2 中用户是可以通过重写 cmp 方法来决定对象之间的比较逻辑的,从 Python 3.0.1 版本后,这个方法被移除了。

import sys from collections import Counter from PyQt5.QtWidgets import ( QApplication, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QFileDialog, QSplitter, QTableWidget, QTableWidgetItem, QGridLayout, QSpinBox, QComboBox, QMessageBox, QLineEdit ) from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor from function import extract_text_from_pdf, extract_total_pages import os class PDFCompareWindow(QMainWindow): HIGHLIGHT_ROLE = Qt.UserRole PERMANENT_HIGHLIGHT = "permanent_highlight" def __init__(self): super().__init__() self.setWindowTitle("成绩单校验工具——Made By 716") self.resize(1600, 900) main_widget = QWidget() self.setCentralWidget(main_widget) main_layout = QVBoxLayout() main_layout.setSpacing(10) main_layout.setContentsMargins(20, 20, 20, 20) # ========== 上半部分:按钮 + 文件名显示区域 ========== top_layout = QHBoxLayout() # 左边:按钮组(使用网格布局) button_layout = QVBoxLayout() grid_layout = QGridLayout() self.template_button = QPushButton("选择模板PDF") self.template_button.clicked.connect(self.load_template_pdf) self.compare_button = QPushButton("选择比对PDF") self.compare_button.clicked.connect(self.load_compare_pdf) # 新增按钮 self.highlight_same_button = QPushButton("一键比对") self.highlight_same_button.clicked.connect(self.highlight_all_same_first_column) self.export_result_button = QPushButton("导出比对结果") self.export_result_button.clicked.connect(self.export_comparison_result) self.clear_highlight_button = QPushButton("清除高亮") self.clear_highlight_button.clicked.connect(self.clear_all_green_highlights) # 设置按钮样式 for btn in [ self.template_button, self.compare_button, self.highlight_same_button, self.export_result_button, self.clear_highlight_button ]: btn.setStyleSheet("font-size: 20px; padding: 10px;") grid_layout.addWidget(btn, *{ self.template_button: (0, 0), self.compare_button: (0, 1), self.highlight_same_button: (1, 0), self.clear_highlight_button: (1, 1), self.export_result_button: (1, 2) }[btn]) # 单独为下方三个按钮设置浅蓝色背景 for btn in [self.highlight_same_button, self.export_result_button, self.clear_highlight_button]: btn.setStyleSheet(btn.styleSheet() + "background-color: skyblue;") # 浅蓝色 button_layout.addLayout(grid_layout) # 中间:文件名显示 file_info_layout = QVBoxLayout() self.template_file_label = QLabel("模板PDF:未选择") self.compare_file_label = QLabel("比对PDF:未选择") self.template_file_label.setStyleSheet("font-size: 24px;") self.compare_file_label.setStyleSheet("font-size: 24px;") file_info_layout.addWidget(self.template_file_label) file_info_layout.addWidget(self.compare_file_label) file_info_layout.addStretch() # 右边:页码输入框 page_input_layout = QVBoxLayout() self.page_template_label = QLabel("模板:第 01 页 / 共 xx 页") self.page_compare_label = QLabel("比对:第 01 页 / 共 xx 页") # 限选课程学分合计标签 self.limited_credits_label = QLabel("限选课程学分合计: 0.00") self.limited_credits_label.setStyleSheet("font-size: 20px; color: blue;") # 创建页码选择控件 self.template_page_spinbox, self.template_page_combo = self.create_page_control() self.compare_page_spinbox, self.compare_page_combo = self.create_page_control() # 模板PDF页码布局 template_page_layout = QHBoxLayout() template_page_layout.addWidget(self.page_template_label) template_page_layout.addWidget(self.template_page_spinbox) template_page_layout.addWidget(self.template_page_combo) # 比对PDF页码布局 compare_page_layout = QHBoxLayout() compare_page_layout.addWidget(self.page_compare_label) compare_page_layout.addWidget(self.compare_page_spinbox) compare_page_layout.addWidget(self.compare_page_combo) page_input_layout.addLayout(template_page_layout) page_input_layout.addLayout(compare_page_layout) page_input_layout.addWidget(self.limited_credits_label) # 添加学分合计显示 page_input_layout.addStretch() # 添加到 top_layout top_layout.addLayout(page_input_layout, 2) top_layout.addSpacing(50) top_layout.addLayout(button_layout, 3) top_layout.addSpacing(50) top_layout.addLayout(file_info_layout, 5) # ========== 下半部分:表格对比区域 ========== bottom_splitter = QSplitter(Qt.Horizontal) # 左侧:模板 PDF 表格 self.template_text = QTableWidget() self.template_text.setStyleSheet("QTableWidget { border: 1px solid #ccc; }") # 右侧:比对 PDF 表格 self.compare_text = QTableWidget() self.compare_text.setStyleSheet("QTableWidget { border: 1px solid #ccc; }") bottom_splitter.addWidget(self.template_text) bottom_splitter.addWidget(self.compare_text) # ========== 组合整体布局 ========== main_layout.addLayout(top_layout, 1) main_layout.addWidget(bottom_splitter, 9) main_widget.setLayout(main_layout) # ========== 事件绑定(新增右侧点击支持)========== self.template_text.cellClicked.connect(self.on_table_cell_clicked) self.compare_text.cellClicked.connect(self.on_table_cell_clicked) # 存储原始数据 self.template_data = None self.compare_data = None # 设置样式美化 self.setStyleSheet(""" QMainWindow { background-color: #FFF5E4; /* 米黄色背景 */ } QPushButton { background-color: #4CAF50; color: white; padding: 12px; border-radius: 5px; font-size: 20px; } QPushButton:hover { background-color: #45a049; } QLabel { margin-top: 5px; font-size: 24px; } QTableWidget { border: 1px solid #ccc; font-size: 16px; } QLineEdit { font-size: 20px; padding: 5px; } """) def calculate_limited_credits(self, widget): total = 0.0 for i in range(widget.rowCount()): item_2nd_col = widget.item(i, 2) # 第三列(课程类型) item_3rd_col = widget.item(i, 3) # 第四列(学分) if item_2nd_col and item_2nd_col.text() == "限选" and item_3rd_col: try: total += float(item_3rd_col.text()) except ValueError: continue return round(total, 2) def create_page_control(self): spinbox = QSpinBox() combobox = QComboBox() spinbox.setRange(1, 9999) spinbox.setValue(1) spinbox.setFixedWidth(60) combobox.addItems([str(i) for i in range(1, 101)]) combobox.setFixedWidth(200) # 同步 SpinBox ComboBox spinbox.valueChanged.connect(combobox.setCurrentIndex) combobox.currentIndexChanged.connect(spinbox.setValue) return spinbox, combobox def load_template_pdf(self): file_path, _ = QFileDialog.getOpenFileName(self, "选择模板PDF", "", "PDF Files (*.pdf)") if file_path: self._template_file_path = file_path try: self.template_page_spinbox.valueChanged.disconnect() except TypeError: pass self._load_table_data(file_path, is_template=True) self.template_page_spinbox.valueChanged.connect( lambda v: self._load_table_data(file_path, is_template=True) ) def load_compare_pdf(self): file_path, _ = QFileDialog.getOpenFileName(self, "选择待比对PDF", "", "PDF Files (*.pdf)") if file_path: self._compare_file_path = file_path try: self.compare_page_spinbox.valueChanged.disconnect() except TypeError: pass self._load_table_data(file_path, is_template=False) self.compare_page_spinbox.valueChanged.connect( lambda v: self._load_table_data(file_path, is_template=False) ) def _load_table_data(self, file_path, is_template=True): page_number = self.template_page_spinbox.value() - 1 if is_template else self.compare_page_spinbox.value() - 1 try: data = extract_text_from_pdf(file_path, page_number=page_number) if not data or not data[0]: QMessageBox.warning(self, "警告", f"无法读取 {file_path} 第 {page_number + 1} 页内容") return total_pages = extract_total_pages(file_path) if is_template: self.template_page_spinbox.setRange(1, total_pages) self.template_page_combo.clear() self.template_page_combo.addItems([str(i) for i in range(1, total_pages + 1)]) self.page_template_label.setText(f"模板:第 {page_number + 1:02d} 页 / 共 {total_pages:02d} 页") self.template_file_label.setText(f"模板PDF:{file_path.split('/')[-1]}") self.display_table(self.template_text, [data[0]]) else: self.compare_page_spinbox.setRange(1, total_pages) self.compare_page_combo.clear() self.compare_page_combo.addItems([str(i) for i in range(1, total_pages + 1)]) self.page_compare_label.setText(f"比对:第 {page_number + 1:02d} 页 / 共 {total_pages:02d} 页") self.compare_file_label.setText(f"比对PDF:{file_path.split('/')[-1]}") self.display_table(self.compare_text, [data[0]]) # 更新限选课程学分合计 template_credits = self.calculate_limited_credits(self.template_text) compare_credits = self.calculate_limited_credits(self.compare_text) self.limited_credits_label.setText(f"限选课程学分合计: 模板 {template_credits}, 比对 {compare_credits}") except Exception as e: QMessageBox.critical(self, "错误", f"加载PDF时发生严重错误:\n{str(e)}") print("加载PDF异常:", e) def on_table_cell_clicked(self, row, col): widget = self.sender() if widget in [self.template_text, self.compare_text] and col == 0: item = widget.item(row, col) if item: # 检查是否是永久高亮(红色/灰色等),如果是则不处理 if item.data(self.HIGHLIGHT_ROLE) == self.PERMANENT_HIGHLIGHT: return # 判断当前是否为绿色高亮 is_green = item.background().color() == Qt.green # 单独对该单元格设置背景色 item.setBackground(Qt.white if is_green else Qt.green) def display_table(self, widget, data): if not data or not data[0]: widget.setRowCount(0) widget.setColumnCount(0) return first_page = data[0] rows = len(first_page) cols = max(len(row) for row in first_page) if first_page else 0 widget.setRowCount(rows) widget.setColumnCount(cols) total_width = widget.width() ratios = [5, 2, 2, 2, 2] total_ratio = sum(ratios[:cols]) if cols <= 5 else sum(ratios[:5]) + (cols - 5) * 1 for col in range(cols): ratio = ratios[col] if col < 5 else 1 width = int(widget.width() * ratio / total_ratio) widget.setColumnWidth(col, width) for i, row in enumerate(first_page): for j, cell in enumerate(row): item = QTableWidgetItem(cell if cell else "") item.setFlags(item.flags() | Qt.ItemIsSelectable | Qt.ItemIsEnabled) if 1 <= j <= 4: item.setTextAlignment(Qt.AlignCenter) widget.setItem(i, j, item) if j == 4 and self._should_highlight_red(cell): item.setBackground(Qt.red) item.setData(self.HIGHLIGHT_ROLE, self.PERMANENT_HIGHLIGHT) elif j == 2: color = self._get_highlight_color(cell) if color: item.setBackground(color) item.setData(self.HIGHLIGHT_ROLE, self.PERMANENT_HIGHLIGHT) def _should_highlight_red(self, cell_text): if not cell_text: return False if "不" in cell_text: return True try: value = float(cell_text) return value < 60 except ValueError: return False def _get_highlight_color(self, cell_text): if not cell_text: return None if cell_text == "任选": return Qt.gray elif cell_text == "必修": return Qt.yellow elif cell_text == "限选": return QColor(255, 165, 0) return None def clear_all_highlights(self): widgets = [self.template_text, self.compare_text] for widget in widgets: for i in range(widget.rowCount()): for j in range(widget.columnCount()): item = widget.item(i, j) if item and item.background().color() == Qt.green: item.setBackground(Qt.white) def clear_all_green_highlights(self): if not self.check_pdfs_loaded("请先选择模板PDF比对PDF"): return self.clear_all_highlights() def get_filtered_first_column(self, widget, include_red=False): first_col_texts = [] for i in range(widget.rowCount()): item_5th_col = widget.item(i, 4) if widget.columnCount() > 4 else None is_red_highlighted = item_5th_col and item_5th_col.background().color() == Qt.red if include_red or not is_red_highlighted: item_1st_col = widget.item(i, 0) if item_1st_col: first_col_texts.append(item_1st_col.text()) return first_col_texts def mark_text_green_in_widget(self, widget, text, max_count): count = 0 for i in range(widget.rowCount()): item = widget.item(i, 0) if item and item.text() == text: count += 1 if count <= max_count: item.setBackground(Qt.green) else: item.setBackground(Qt.white) def highlight_all_same_first_column(self): if not self.check_pdfs_loaded("请先选择模板PDF比对PDF"): return template_texts = self.get_filtered_first_column(self.template_text) compare_texts = self.get_filtered_first_column(self.compare_text, include_red=False) template_counter = Counter(template_texts) compare_counter = Counter(compare_texts) self.clear_all_green_highlights() matched_count = 0 for text, count_in_template in template_counter.items(): count_in_compare = compare_counter.get(text, 0) mark_count = min(count_in_template, count_in_compare) if mark_count > 0: self.mark_text_green_in_widget(self.template_text, text, mark_count) self.mark_text_green_in_widget_with_condition(self.compare_text, text, mark_count) # 必修课程匹配判断 required_required_rows = [] highlighted_required_rows = [] for i in range(self.template_text.rowCount()): item_2nd_col = self.template_text.item(i, 2) item_1st_col = self.template_text.item(i, 0) if item_2nd_col and item_1st_col: course_type = item_2nd_col.text() if course_type == '必修': required_required_rows.append(item_1st_col.text()) if item_1st_col.background().color() == Qt.green: highlighted_required_rows.append(item_1st_col.text()) missing_required_count = len(required_required_rows) - len(highlighted_required_rows) # 限选学分匹配判断 try: template_credits = float(self.limited_credits_label.text().split("模板")[1].split(",")[0].strip()) compare_credits = float(self.limited_credits_label.text().split("比对")[1].strip()) except (IndexError, ValueError): QMessageBox.warning(self, "警告", "无法获取限选课程学分,请确保已加载有效成绩单") return # 构建统一提示内容 result_title = "整体比对结果" result_messages = [] if missing_required_count == 0: result_messages.append("✅ 所有必修课程已匹配") else: result_messages.append(f"❌ 必修课程未匹配 {missing_required_count} 门") if template_credits == compare_credits: result_messages.append("✅ 限选课程学分已匹配") elif template_credits > compare_credits: diff = round(template_credits - compare_credits, 2) result_messages.append(f"❌ 限选课程学分缺少 {diff} 分") else: result_messages.append("⚠️ 比对文件限选学分高于模板") msg_box = QMessageBox(self) msg_box.setWindowTitle(result_title) msg_box.setText("\n".join(result_messages)) msg_box.setIcon(QMessageBox.Warning if any("❌" in m for m in result_messages) else QMessageBox.Information) msg_box.setStandardButtons(QMessageBox.Ok) msg_box.exec_() def mark_text_green_in_widget_with_condition(self, widget, text, max_count): count = 0 for i in range(widget.rowCount()): item = widget.item(i, 0) if item and item.text() == text: # 检查第五列是否存在且背景为红色 if widget.columnCount() > 4: fifth_item = widget.item(i, 4) if fifth_item and fifth_item.background().color() == Qt.red: continue # 跳过被红色标记的行 count += 1 if count <= max_count: item.setBackground(Qt.green) else: item.setBackground(Qt.white) def export_comparison_result(self): if not self.check_pdfs_loaded("请先选择模板PDF比对PDF"): return current_page = self.compare_page_spinbox.value() default_file_name = f"比对结果_第{current_page}页.csv" file_path, _ = QFileDialog.getSaveFileName( self, "保存比对结果", default_file_name, "CSV 文件 (*.csv);;文本文件 (*.txt)" ) if not file_path: return # 检查文件是否打开 try: with open(file_path, 'a', encoding='utf-8'): pass except IOError: QMessageBox.critical(self, "错误", "目标文件已被其他程序打开,请关闭后再试。") return unmatched_rows = [] # 获取模板中绿色高亮的课程名称 green_template_texts = set() for i in range(self.template_text.rowCount()): item = self.template_text.item(i, 0) if item and item.background().color() == Qt.green: green_template_texts.add(item.text()) # 获取比对中绿色高亮的课程名称 green_compare_texts = set() for i in range(self.compare_text.rowCount()): item = self.compare_text.item(i, 0) if item and item.background().color() == Qt.green: green_compare_texts.add(item.text()) # 找出未被高亮的行 template_unmatched_rows = [] for i in range(self.template_text.rowCount()): item = self.template_text.item(i, 0) if item and item.text() not in green_template_texts: row_data = [ self.template_text.item(i, j).text() if self.template_text.item(i, j) else "" for j in range(self.template_text.columnCount()) ] template_unmatched_rows.append(row_data) compare_unmatched_rows = [] for i in range(self.compare_text.rowCount()): item = self.compare_text.item(i, 0) if item and item.text() not in green_compare_texts: row_data = [ self.compare_text.item(i, j).text() if self.compare_text.item(i, j) else "" for j in range(self.compare_text.columnCount()) ] compare_unmatched_rows.append(row_data) # 对齐行数 max_len = max(len(template_unmatched_rows), len(compare_unmatched_rows)) while len(template_unmatched_rows) < max_len: template_unmatched_rows.append([""] * self.template_text.columnCount()) while len(compare_unmatched_rows) < max_len: compare_unmatched_rows.append([""] * self.compare_text.columnCount()) # 合并成左右格式 for t_row, c_row in zip(template_unmatched_rows, compare_unmatched_rows): merged_row = t_row + [""] + c_row unmatched_rows.append(merged_row) # 写入文件 try: with open(file_path, 'w', encoding='utf-8', newline='') as f: f.write("=== 左侧未匹配项 ===,,,,,,=== 右侧未匹配项 ===\n") header = ["模板列" + str(i + 1) for i in range(self.template_text.columnCount())] + \ [""] + ["比对列" + str(i + 1) for i in range(self.compare_text.columnCount())] f.write(",".join(header) + "\n") for row in unmatched_rows: f.write(",".join(row) + "\n") QMessageBox.information(self, "成功", "文件已成功导出") except Exception as e: QMessageBox.critical(self, "错误", f"\n{str(e)}") def resizeEvent(self, event): super().resizeEvent(event) widgets = [self.template_text, self.compare_text] for widget in widgets: if widget.rowCount() > 0: cols = min(widget.columnCount(), 5) ratios = [5, 2, 2, 2, 2] total_ratio = sum(ratios[:cols]) for col in range(cols): ratio = ratios[col] if col < 5 else 1 width = int(widget.width() * ratio / total_ratio) widget.setColumnWidth(col, width) def check_pdfs_loaded(self, message="请先选择模板PDF比对PDF文件"): if not hasattr(self, '_template_file_path') or not hasattr(self, '_compare_file_path'): QMessageBox.warning(self, "警告", message) return False return True if __name__ == "__main__": app = QApplication(sys.argv) window = PDFCompareWindow() window.show() sys.exit(app.exec_()) QcomboboxQspinbox的值不能互相关联,要求修改其中一个,另一个同步更改
06-10
代码1: import cv2 import numpy as np import matplotlib.pyplot as plt import json import os from matplotlib import font_manager import warnings # ======================== # 设置中文字体 # ======================== def setup_chinese_font(): chinese_fonts = [ 'SimHei', 'Microsoft YaHei', 'FangSong', 'KaiTi', 'Arial Unicode MS', 'PingFang SC', 'WenQuanYi Micro Hei' ] available_fonts = set(f.name for f in font_manager.fontManager.ttflist) matched_font = None for font in chinese_fonts: if font in available_fonts: matched_font = font break if matched_font: plt.rcParams['font.sans-serif'] = [matched_font] print(f"✅ 使用中文字体: {matched_font}") else: plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Arial'] print("⚠️ 未检测到中文字体,使用英文替代") plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 150 setup_chinese_font() # ======================== # 安全转换 NumPy 类型为 Python 原生类型(用于 JSON) # ======================== def convert_types(obj): if isinstance(obj, np.ndarray): return obj.tolist() elif isinstance(obj, (np.float32, np.float64)): return float(obj) elif isinstance(obj, (np.int32, np.int64)): return int(obj) elif isinstance(obj, dict): return {k: convert_types(v) for k, v in obj.items()} elif isinstance(obj, list): return [convert_types(i) for i in obj] else: return obj # ======================== # 图像分析函数(单次对比) # ======================== def analyze_pair(ref_img_path, test_img_path, output_match_image=None): """ 分析测试图像相对于参考图像的差异 返回包含畸变率、像素误差等指标的结果字典 """ ref_img = cv2.imread(ref_img_path) test_img = cv2.imread(test_img_path) if ref_img is None or test_img is None: raise ValueError(f"无法加载图像: {ref_img_path} 或 {test_img_path}") gray_ref = cv2.cvtColor(ref_img, cv2.COLOR_BGR2GRAY) gray_test = cv2.cvtColor(test_img, cv2.COLOR_BGR2GRAY) h, w = gray_ref.shape diagonal = np.sqrt(w ** 2 + h ** 2) def calculate_distortion(gray1, gray2): sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(gray1, None) kp2, des2 = sift.detectAndCompute(gray2, None) if len(kp1) < 8 or len(kp2) < 8: return None, None, [], [], [], None # 匹配失败 bf = cv2.BFMatcher() matches = bf.knnMatch(des1, des2, k=2) good_matches = [m for m, n in matches if m.distance < 0.75 * n.distance] if len(good_matches) < 8: return None, None, [], [], [], None src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) reprojected = cv2.perspectiveTransform(src_pts, H) errors = np.linalg.norm(dst_pts - reprojected, axis=2) mean_error_px = np.mean(errors) distortion_rate_percent = (mean_error_px / diagonal) * 100 return distortion_rate_percent, mean_error_px, good_matches, kp1, kp2, mask def calculate_contrast(gray): return np.std(gray) def calculate_sharpness(gray): return cv2.Laplacian(gray, cv2.CV_64F).var() # 执行计算 dr_percent, mean_px_err, matches, kp_ref, kp_test, mask = calculate_distortion(gray_ref, gray_test) if dr_percent is None: print(f"⚠️ 特征匹配不足,跳过: {os.path.basename(test_img_path)}") return None contrast_ref = calculate_contrast(gray_ref) contrast_test = calculate_contrast(gray_test) sharpness_ref = calculate_sharpness(gray_ref) sharpness_test = calculate_sharpness(gray_test) # 百分制评分 MAX_DR = 5.0 MAX_CONTRAST_DIFF = 50.0 MAX_SHARPNESS_DIFF = 1000.0 def score(v, max_v, inv=True): s = min(float(v) / max_v, 1.0) return round((1 - s) * 100, 2) if inv else round(s * 100, 2) scores = { "distortion": score(dr_percent, MAX_DR), "contrast": score(abs(contrast_ref - contrast_test), MAX_CONTRAST_DIFF), "sharpness": score(abs(sharpness_ref - sharpness_test), MAX_SHARPNESS_DIFF), } weights = {"distortion": 0.4, "contrast": 0.3, "sharpness": 0.3} overall = round(sum(scores[k] * weights[k] for k in scores), 2) # 可视化并保存匹配图 if output_match_image: title = f"与基准图对比\n畸变率: {dr_percent:.3f}% | 平均误差: {mean_px_err:.3f}px" draw_params = dict(matchColor=(0, 255, 0), matchesMask=mask.ravel().tolist(), flags=2) matched_img = cv2.drawMatches(ref_img, kp_ref, test_img, kp_test, matches, None, **draw_params) matched_img_rgb = cv2.cvtColor(matched_img, cv2.COLOR_BGR2RGB) plt.figure(figsize=(16, 9)) plt.imshow(matched_img_rgb) plt.title(title, fontsize=16, weight='bold') plt.axis('off') plt.savefig(output_match_image, bbox_inches='tight', pad_inches=0.1) plt.close() print(f"✅ 匹配图已保存: {output_match_image}") # 构建结果(注意:数值仍是 np 类型,后续会转换) result = { "reference_image": os.path.basename(ref_img_path), "test_image": os.path.basename(test_img_path), "distortion_rate_percent": dr_percent, "mean_reprojection_error_pixels": mean_px_err, "image_diagonal_pixels": diagonal, "metrics": { "reference": { "contrast": contrast_ref, "sharpness": sharpness_ref }, "test": { "contrast": contrast_test, "sharpness": sharpness_test } }, "scores": { "distortion": scores["distortion"], "contrast": scores["contrast"], "sharpness": scores["sharpness"], "overall": overall } } return result # ======================== # 批量处理所有图像(第一张为基准) # ======================== def batch_compare_images(image_paths, output_dir="comparison_results"): """ 批量对比:以第一张图为基准,与其他图一一比较 """ if len(image_paths) < 2: raise ValueError("至少需要两张图像进行对比") # 创建输出目录 os.makedirs(output_dir, exist_ok=True) ref_img_path = image_paths[0] results = [] print(f"\n🎯 开始批量分析,基准图像: {os.path.basename(ref_img_path)}") print(f"共需对比 {len(image_paths) - 1} 张图像...\n") for i, test_img_path in enumerate(image_paths[1:], start=1): test_basename = os.path.basename(test_img_path) print(f"🔍 正在对比第 {i}/{len(image_paths)-1}: {test_basename}") # 修复文件扩展名重复问题(原代码可能生成 .jpg.png) name_part = os.path.splitext(test_basename)[0] match_img = os.path.join(output_dir, f"match_{i:02d}_{name_part}.png") json_file = os.path.join(output_dir, f"result_{i:02d}_{name_part}.json") result = analyze_pair(ref_img_path, test_img_path, output_match_image=match_img) if result: # ✅ 关键:先转换数据类型再保存 safe_result = convert_types(result) results.append(safe_result) with open(json_file, 'w', encoding='utf-8') as f: json.dump(safe_result, f, ensure_ascii=False, indent=4) print(f" 保存结果: {json_file}") else: print(f" ❌ 对比失败,跳过") # 保存汇总报告 summary = { "summary": { "reference_image": os.path.basename(ref_img_path), "total_compared": len(results), "failed_count": (len(image_paths) - 1) - len(results) }, "details": results } summary_file = os.path.join(output_dir, "summary_report.json") safe_summary = convert_types(summary) with open(summary_file, 'w', encoding='utf-8') as f: json.dump(safe_summary, f, ensure_ascii=False, indent=4) print(f"\n📈 汇总报告已保存: {summary_file}") return summary # ======================== # 打印汇总结果 # ======================== def print_summary_report(summary): print("\n" + "=" * 60) print("📊 批量对比汇总报告") print("=" * 60) print(f"基准图像: {summary['summary']['reference_image']}") print(f"成功对比: {summary['summary']['total_compared']} 张") print(f"失败数量: {summary['summary']['failed_count']} 张\n") for idx, r in enumerate(summary['details']): print(f"--- [{idx+1}] {r['test_image']} ---") print(f" 几何畸变: {r['scores']['distortion']}% → 畸变率: {r['distortion_rate_percent']:.3f}%") print(f" 对比度分: {r['scores']['contrast']}% (Ref: {r['metrics']['reference']['contrast']:.3f}, " f"Test: {r['metrics']['test']['contrast']:.3f})") print(f" 锐度得分: {r['scores']['sharpness']}%") print(f" 综合评分: {r['scores']['overall']}%\n") # ======================== # 主程序入口 # ======================== if __name__ == "__main__": # 📌 修改为你自己的图像路径列表(第一张为基准图) image_files = [ "ref.jpg", # 基准图 "test1.jpg", "test2.jpg", "test3.jpg", ] # 检查文件是否存在 missing = [f for f in image_files if not os.path.exists(f)] if missing: print("❌ 缺失文件:", missing) else: try: summary_result = batch_compare_images(image_files, output_dir="comparison_results") print_summary_report(summary_result) except Exception as e: print("❌ 错误:", str(e)) 代码2: import cv2 import numpy as np import matplotlib.pyplot as plt import json import os import glob import pandas as pd # 用于导出 Excel from matplotlib import font_manager # ======================== # 设置常规中文字体(优先选择 SimHei、微软雅黑) # ======================== def setup_chinese_font(): # 常见中文字体列表 chinese_fonts = [ 'SimHei', # 黑体(最常用) 'Microsoft YaHei', # 微软雅黑 'FangSong', # 仿宋 'KaiTi', # 楷体 'Arial Unicode MS', 'PingFang SC', 'WenQuanYi Micro Hei' ] available_fonts = set(f.name for f in font_manager.fontManager.ttflist) matched_font = None for font in chinese_fonts: if font in available_fonts: matched_font = font break if matched_font: plt.rcParams['font.sans-serif'] = [matched_font] plt.rcParams['axes.unicode_minus'] = False # 正常显示负号 print(f"✅ 成功加载中文字体: {matched_font}") else: plt.rcParams['font.sans-serif'] = ['DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False print("⚠️ 未找到中文字体,使用英文替代") setup_chinese_font() # ======================== # 安全转换 NumPy 类型为原生 Python 类型 # ======================== def convert_types(obj): if isinstance(obj, np.ndarray): return obj.tolist() elif isinstance(obj, (np.float32, np.float64)): return float(obj) elif isinstance(obj, (np.int32, np.int64)): return int(obj) elif isinstance(obj, dict): return {k: convert_types(v) for k, v in obj.items()} elif isinstance(obj, list): return [convert_types(i) for i in obj] else: return obj # ======================== # 发现所有有效图像文件 # ======================== def discover_images(directory=".", recursive=False): patterns = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.tiff', '*.tif'] found_files = [] abs_dir = os.path.abspath(directory) print(f"\n🔍 扫描目录: {abs_dir}") for pattern in patterns: search_path = os.path.join(directory, pattern) matches = glob.glob(search_path, recursive=recursive) found_files.extend(matches) unique_files = sorted(set(os.path.normpath(f) for f in found_files)) valid_images = [] print(f"📄 候选文件数: {len(unique_files)}") for f in unique_files: if not os.path.isfile(f): continue try: img = cv2.imread(f) if img is not None and img.size > 0: valid_images.append(f) print(f" ✅ 加载成功: {os.path.basename(f)} | 尺寸: {img.shape[1]}x{img.shape[0]}") else: print(f" ❌ 跳过损坏图像: {f}") except Exception as e: print(f" ❌ 读取异常: {f} -> {str(e)}") print(f"\n🎉 共发现有效图像: {len(valid_images)} 张") for i, f in enumerate(valid_images): print(f" [{i+1:02d}] {os.path.basename(f)}") return valid_images # ======================== # 单次图像对比分析函数 # ======================== def analyze_pair(ref_img_path, test_img_path, output_match_image=None): ref_img = cv2.imread(ref_img_path) test_img = cv2.imread(test_img_path) if ref_img is None or test_img is None: print(f"❌ 无法加载图像 -> Ref: {ref_img_path}, Test: {test_img_path}") return None gray_ref = cv2.cvtColor(ref_img, cv2.COLOR_BGR2GRAY) gray_test = cv2.cvtColor(test_img, cv2.COLOR_BGR2GRAY) h, w = gray_ref.shape diagonal = float(np.sqrt(w ** 2 + h ** 2)) # --- 1. 计算几何畸变率 --- def calculate_distortion(): sift = cv2.SIFT_create(nfeatures=200) kp1, des1 = sift.detectAndCompute(gray_ref, None) kp2, des2 = sift.detectAndCompute(gray_test, None) if len(kp1) < 8 or len(kp2) < 8: return None, None, [], [], [], None bf = cv2.BFMatcher() matches = bf.knnMatch(des1, des2, k=2) good_matches = [m for m, n in matches if m.distance < 0.75 * n.distance] if len(good_matches) < 8: return None, None, [], [], [], None src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) reprojected = cv2.perspectiveTransform(src_pts, H) errors = np.linalg.norm(dst_pts - reprojected, axis=2) mean_error_px = np.mean(errors) distortion_rate_percent = (mean_error_px / diagonal) * 100 return distortion_rate_percent, mean_error_px, good_matches, kp1, kp2, mask dr_percent, mean_px_err, matches, kp_ref, kp_test, mask = calculate_distortion() if dr_percent is None: print(f" ⚠️ 特征匹配不足,跳过: {os.path.basename(test_img_path)}") return None # --- 2. 对比度与锐度 --- def calculate_contrast(gray): return np.std(gray) def calculate_sharpness(gray): return cv2.Laplacian(gray, cv2.CV_64F).var() contrast_ref = calculate_contrast(gray_ref) contrast_test = calculate_contrast(gray_test) sharpness_ref = calculate_sharpness(gray_ref) sharpness_test = calculate_sharpness(gray_test) # --- 3. 百分制评分 --- MAX_DR = 5.0 MAX_CONTRAST_DIFF = 50.0 MAX_SHARPNESS_DIFF = 1000.0 def score(value, max_val, inverse=True): normalized = min(float(abs(value)) / max_val, 1.0) return round((1 - normalized) * 100, 2) if inverse else round(normalized * 100, 2) scores = { "distortion": score(dr_percent, MAX_DR), "contrast": score(contrast_ref - contrast_test, MAX_CONTRAST_DIFF), "sharpness": score(sharpness_ref - sharpness_test, MAX_SHARPNESS_DIFF), } weights = {"distortion": 0.4, "contrast": 0.3, "sharpness": 0.3} overall_score = round(sum(scores[k] * weights[k] for k in scores), 2) # --- 4. 可视化特征点匹配 --- if output_match_image: title = f"特征点匹配可视化\n畸变率: {dr_percent:.3f}% | 重投影误差: {mean_px_err:.3f}px" draw_params = dict(matchColor=(0, 255, 0), singlePointColor=None, matchesMask=mask.ravel().tolist(), flags=2) matched_img = cv2.drawMatches(ref_img, kp_ref, test_img, kp_test, matches, None, **draw_params) matched_rgb = cv2.cvtColor(matched_img, cv2.COLOR_BGR2RGB) plt.figure(figsize=(16, 9)) plt.imshow(matched_rgb) plt.title(title, fontsize=16, fontweight='bold') plt.axis('off') plt.tight_layout() plt.savefig(output_match_image, dpi=120, bbox_inches='tight', pad_inches=0.1) plt.close() print(f" 🖼️ 匹配图已保存: {output_match_image}") # --- 5. 构建结果 --- result = { "参考图像": os.path.basename(ref_img_path), "测试图像": os.path.basename(test_img_path), "成像畸变率_百分比": round(dr_percent, 4), "平均重投影误差_像素": round(mean_px_err, 4), "对角线长度_像素": round(diagonal, 2), "参考图对比度_std": round(contrast_ref, 4), "测试图对比度_std": round(contrast_test, 4), "对比度差值": round(abs(contrast_ref - contrast_test), 4), "参考图锐度_LaplacianVar": round(sharpness_ref, 4), "测试图锐度_LaplacianVar": round(sharpness_test, 4), "锐度差值": round(abs(sharpness_ref - sharpness_test), 4), "畸变评分": scores["distortion"], "对比度评分": scores["contrast"], "锐度评分": scores["sharpness"], "综合得分": overall_score } return result # ======================== # 批量处理主函数 + Excel 导出 # ======================== def batch_compare_images(image_paths, output_dir="comparison_results"): if len(image_paths) < 2: raise ValueError("至少需要两张图像进行对比") if len(image_paths) > 100: print(f"⚠️ 图像数量超过100,仅处理前100张") image_paths = image_paths[:100] os.makedirs(output_dir, exist_ok=True) ref_img_path = image_paths[0] results = [] print(f"\n🎯 开始分析,基准图像: {os.path.basename(ref_img_path)}") print(f"共需对比 {len(image_paths) - 1} 张图像...\n") for i, test_img_path in enumerate(image_paths[1:], start=1): test_name = os.path.basename(test_img_path) name_no_ext = os.path.splitext(test_name)[0] match_img = os.path.join(output_dir, f"match_{i:02d}_{name_no_ext}.png") json_file = os.path.join(output_dir, f"result_{i:02d}_{name_no_ext}.json") print(f"🔍 [{i:02d}/{len(image_paths)-1:02d}] 正在对比: {test_name}") result = analyze_pair(ref_img_path, test_img_path, output_match_image=match_img) if result: safe_result = convert_types(result) results.append(safe_result) with open(json_file, 'w', encoding='utf-8') as f: json.dump(safe_result, f, ensure_ascii=False, indent=4) print(f" ✅ JSON 结果已保存: {json_file}") else: print(f" ❌ 分析失败,跳过该图像") # === 🔽 新增:导出 Excel 汇总表 === excel_file = os.path.join(output_dir, "analysis_summary.xlsx") try: df = pd.DataFrame(results) df.to_excel(excel_file, index=False, sheet_name="图像对比汇总") print(f"📊 Excel 汇总表已生成: {excel_file}") except Exception as e: print(f"❌ Excel 导出失败: {e}") # 保存 summary_report.json summary = { "summary": { "reference_image": os.path.basename(ref_img_path), "total_compared": len(results), "failed_count": (len(image_paths) - 1) - len(results), "output_directory": os.path.abspath(output_dir) }, "details": results } summary_file = os.path.join(output_dir, "summary_report.json") with open(summary_file, 'w', encoding='utf-8') as f: json.dump(convert_types(summary), f, ensure_ascii=False, indent=4) print(f"📈 JSON 汇总报告已生成: {summary_file}") return summary # ======================== # 打印简洁摘要 # ======================== def print_summary_report(summary): s = summary['summary'] print("\n" + "=" * 80) print("📊 图像质量与一致性分析报告") print("=" * 80) print(f"基准图像 : {s['reference_image']}") print(f"对比图像总数 : {s['total_compared']} 张") print(f"失败数量 : {s['failed_count']} 张") print(f"输出目录 : {s['output_directory']}") print("-" * 80) for idx, r in enumerate(summary['details']): print(f"[{idx+1:02d}] {r['测试图像']}") print(f" 畸变率: {r['成像畸变率_百分比']:>6.3f}% " f"| 误差: {r['平均重投影误差_像素']:>6.3f}px") print(f" 对比度: Ref={r['参考图对比度_std']:>6.2f} → " f"Test={r['测试图对比度_std']:>6.2f} | 评分: {r['对比度评分']}/100") print(f" 锐度: Ref={r['参考图锐度_LaplacianVar']:>6.2f} → " f"Test={r['测试图锐度_LaplacianVar']:>6.2f} | 评分: {r['锐度评分']}/100") print(f" 综合评分: {r['综合得分']}/100") print("=" * 80) # ======================== # 主程序入口 # ======================== if __name__ == "__main__": # 方法一:手动指定图像列表 image_files = [ # "ref.jpg", # "test1.jpg" ] # 方法二:自动扫描当前目录下所有图像 if not image_files: IMAGE_DIR = "." # 可改为 "./images" image_files = discover_images(directory=IMAGE_DIR, recursive=False) if len(image_files) < 2: print("❌ 至少需要两个有效的图像文件!") else: try: summary = batch_compare_images(image_files, output_dir="comparison_results") print_summary_report(summary) except Exception as e: print(f"💥 程序异常: {e}") 为何两个代码,输出的畸变率数值不同,请检查产生的原因
最新发布
12-13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值