Browse Source

添加上传身份证识别页面

liuyuqi-cnb 2 weeks ago
parent
commit
668dda9f1a
3 changed files with 631 additions and 7 deletions
  1. 30 0
      routes.py
  2. 525 0
      static/id_verify.html
  3. 76 7
      utils/ocr_utils.py

+ 30 - 0
routes.py

@@ -75,6 +75,36 @@ def register_user():
 
 
     except Exception as e:
     except Exception as e:
         return jsonify({'error': str(e)}), 500
         return jsonify({'error': str(e)}), 500
+    finally:
+        # 清理临时文件
+        if os.path.exists(image_path):
+            os.remove(image_path)
+
+@user_bp.route('/id_verify', methods=['GET'])
+def id_verify_page():
+    """身份证验证页面"""
+    return render_template_string(open('static/id_verify.html').read())
+
+@user_bp.route('/api/verify_id_card', methods=['POST'])
+def verify_id_card():
+    """身份证验证 API"""
+    image_file = request.files.get('id_card_image')
+
+    if not image_file:
+        return jsonify({'error': '请上传身份证图片'}), 400
+
+    # 保存上传的图片到临时目录
+    image_path = os.path.join('/tmp', image_file.filename)
+    image_file.save(image_path)
+
+    try:
+        # 使用 PaddleOCR 识别身份证信息
+        id_card_info = extract_id_card_info(image_path)
+
+        return jsonify(id_card_info), 200
+
+    except Exception as e:
+        return jsonify({'error': f'识别失败: {str(e)}'}), 500
     finally:
     finally:
         # 清理临时文件
         # 清理临时文件
         if os.path.exists(image_path):
         if os.path.exists(image_path):

+ 525 - 0
static/id_verify.html

@@ -0,0 +1,525 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>身份证验证</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: 'Microsoft YaHei', Arial, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            padding: 15px;
+        }
+
+        .container {
+            background: white;
+            border-radius: 15px;
+            box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
+            padding: 25px 30px;
+            max-width: 1400px;
+            width: 100%;
+            max-height: 90vh;
+            overflow: hidden;
+        }
+
+        h1 {
+            text-align: center;
+            color: #333;
+            margin-bottom: 20px;
+            font-size: 24px;
+        }
+
+        .message-area {
+            min-height: 45px;
+            margin-bottom: 15px;
+        }
+
+        .error-message,
+        .success-message {
+            display: none;
+            padding: 10px 15px;
+            border-radius: 6px;
+            margin-bottom: 10px;
+            font-size: 14px;
+        }
+
+        .error-message {
+            background: #fee;
+            color: #c33;
+            border-left: 3px solid #c33;
+        }
+
+        .success-message {
+            background: #efe;
+            color: #3c3;
+            border-left: 3px solid #3c3;
+        }
+
+        .error-message.show,
+        .success-message.show {
+            display: block;
+        }
+
+        .upload-section {
+            background: #f8f9fa;
+            border: 2px dashed #667eea;
+            border-radius: 10px;
+            padding: 30px;
+            text-align: center;
+            transition: all 0.3s;
+        }
+
+        .upload-section:hover {
+            border-color: #764ba2;
+            background: #f0f0f5;
+        }
+
+        .upload-section.dragover {
+            border-color: #764ba2;
+            background: #e8e8f5;
+        }
+
+        .upload-label {
+            cursor: pointer;
+            display: inline-block;
+        }
+
+        .upload-icon {
+            font-size: 40px;
+            color: #667eea;
+            margin-bottom: 8px;
+        }
+
+        .upload-text {
+            color: #666;
+            font-size: 15px;
+            margin-bottom: 5px;
+        }
+
+        .upload-hint {
+            color: #999;
+            font-size: 13px;
+        }
+
+        #id_card_image {
+            display: none;
+        }
+
+        .btn-upload {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            border: none;
+            padding: 10px 35px;
+            border-radius: 20px;
+            font-size: 15px;
+            cursor: pointer;
+            margin-top: 12px;
+            transition: transform 0.2s;
+        }
+
+        .btn-upload:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+        }
+
+        .btn-upload:disabled {
+            background: #ccc;
+            cursor: not-allowed;
+            transform: none;
+        }
+
+        .main-content {
+            display: none;
+        }
+
+        .main-content.show {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 25px;
+            align-items: start;
+        }
+
+        .left-panel {
+            display: flex;
+            flex-direction: column;
+            gap: 15px;
+        }
+
+        .image-preview {
+            text-align: center;
+            background: #f8f9fa;
+            border-radius: 10px;
+            padding: 15px;
+        }
+
+        .image-preview img {
+            max-width: 100%;
+            max-height: 450px;
+            border-radius: 8px;
+            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
+        }
+
+        .loading {
+            display: none;
+            text-align: center;
+            padding: 30px;
+        }
+
+        .loading.show {
+            display: block;
+        }
+
+        .spinner {
+            border: 3px solid #f3f3f3;
+            border-top: 3px solid #667eea;
+            border-radius: 50%;
+            width: 35px;
+            height: 35px;
+            animation: spin 1s linear infinite;
+            margin: 0 auto 10px;
+        }
+
+        @keyframes spin {
+            0% {
+                transform: rotate(0deg);
+            }
+
+            100% {
+                transform: rotate(360deg);
+            }
+        }
+
+        .right-panel {
+            background: #f8f9fa;
+            border-radius: 10px;
+            padding: 20px;
+            max-height: 520px;
+            overflow-y: auto;
+        }
+
+        .result-title {
+            font-size: 18px;
+            color: #333;
+            margin-bottom: 15px;
+            font-weight: bold;
+            text-align: center;
+        }
+
+        .result-grid {
+            display: grid;
+            gap: 10px;
+        }
+
+        .result-item {
+            display: grid;
+            grid-template-columns: 90px 1fr;
+            padding: 12px 15px;
+            background: white;
+            border-radius: 6px;
+            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+            align-items: center;
+        }
+
+        .result-label {
+            font-weight: bold;
+            color: #667eea;
+            font-size: 14px;
+        }
+
+        .result-value {
+            color: #333;
+            font-size: 14px;
+            word-break: break-all;
+        }
+
+        .result-value.empty {
+            color: #999;
+            font-style: italic;
+        }
+
+        .button-group {
+            margin-top: 15px;
+            text-align: center;
+        }
+
+        .btn-reset {
+            background: #6c757d;
+            color: white;
+            border: none;
+            padding: 8px 25px;
+            border-radius: 20px;
+            font-size: 14px;
+            cursor: pointer;
+            transition: all 0.2s;
+        }
+
+        .btn-reset:hover {
+            background: #5a6268;
+            transform: translateY(-2px);
+        }
+
+        @media (max-width: 1200px) {
+            .main-content.show {
+                grid-template-columns: 1fr;
+            }
+
+            .image-preview img {
+                max-height: 350px;
+            }
+        }
+
+        /* 滚动条样式 */
+        .right-panel::-webkit-scrollbar {
+            width: 6px;
+        }
+
+        .right-panel::-webkit-scrollbar-track {
+            background: #f1f1f1;
+            border-radius: 3px;
+        }
+
+        .right-panel::-webkit-scrollbar-thumb {
+            background: #667eea;
+            border-radius: 3px;
+        }
+
+        .right-panel::-webkit-scrollbar-thumb:hover {
+            background: #5568d3;
+        }
+    </style>
+</head>
+
+<body>
+    <div class="container">
+        <h1>🪪 身份证信息验证</h1>
+
+        <div class="message-area">
+            <div class="error-message" id="errorMessage"></div>
+            <div class="success-message" id="successMessage"></div>
+        </div>
+
+        <div class="upload-section" id="uploadSection">
+            <label for="id_card_image" class="upload-label">
+                <div class="upload-icon">📤</div>
+                <div class="upload-text">点击或拖拽上传身份证图片</div>
+                <div class="upload-hint">支持 JPG、PNG 格式,建议文件小于 5MB</div>
+            </label>
+            <input type="file" id="id_card_image" accept="image/*">
+            <br>
+            <button class="btn-upload" id="btnUpload" disabled>开始识别</button>
+        </div>
+
+        <div class="loading" id="loading">
+            <div class="spinner"></div>
+            <div>正在识别中,请稍候...</div>
+        </div>
+
+        <div class="main-content" id="mainContent">
+            <div class="left-panel">
+                <div class="image-preview">
+                    <img id="imagePreview" src="" alt="身份证预览">
+                </div>
+            </div>
+
+            <div class="right-panel">
+                <div class="result-title">识别结果</div>
+                <div class="result-grid">
+                    <div class="result-item">
+                        <div class="result-label">姓名</div>
+                        <div class="result-value" id="resultName">-</div>
+                    </div>
+                    <div class="result-item">
+                        <div class="result-label">性别</div>
+                        <div class="result-value" id="resultGender">-</div>
+                    </div>
+                    <div class="result-item">
+                        <div class="result-label">民族</div>
+                        <div class="result-value" id="resultNation">-</div>
+                    </div>
+                    <div class="result-item">
+                        <div class="result-label">出生日期</div>
+                        <div class="result-value" id="resultBirth">-</div>
+                    </div>
+                    <div class="result-item">
+                        <div class="result-label">地址</div>
+                        <div class="result-value" id="resultAddress">-</div>
+                    </div>
+                    <div class="result-item">
+                        <div class="result-label">身份证号</div>
+                        <div class="result-value" id="resultIdNumber">-</div>
+                    </div>
+                </div>
+                <div class="button-group">
+                    <button class="btn-reset" id="btnReset">重新识别</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        const fileInput = document.getElementById('id_card_image');
+        const btnUpload = document.getElementById('btnUpload');
+        const btnReset = document.getElementById('btnReset');
+        const uploadSection = document.getElementById('uploadSection');
+        const mainContent = document.getElementById('mainContent');
+        const imagePreview = document.getElementById('imagePreview');
+        const loading = document.getElementById('loading');
+        const errorMessage = document.getElementById('errorMessage');
+        const successMessage = document.getElementById('successMessage');
+
+        let selectedFile = null;
+
+        // 文件选择事件
+        fileInput.addEventListener('change', function (e) {
+            const file = e.target.files[0];
+            if (file) {
+                handleFile(file);
+            }
+        });
+
+        // 拖拽上传
+        uploadSection.addEventListener('dragover', function (e) {
+            e.preventDefault();
+            uploadSection.classList.add('dragover');
+        });
+
+        uploadSection.addEventListener('dragleave', function (e) {
+            e.preventDefault();
+            uploadSection.classList.remove('dragover');
+        });
+
+        uploadSection.addEventListener('drop', function (e) {
+            e.preventDefault();
+            uploadSection.classList.remove('dragover');
+            const file = e.dataTransfer.files[0];
+            if (file && file.type.startsWith('image/')) {
+                fileInput.files = e.dataTransfer.files;
+                handleFile(file);
+            } else {
+                showError('请上传图片文件');
+            }
+        });
+
+        // 处理文件
+        function handleFile(file) {
+            if (file.size > 5 * 1024 * 1024) {
+                showError('文件大小不能超过 5MB');
+                return;
+            }
+
+            selectedFile = file;
+            btnUpload.disabled = false;
+
+            // 显示图片预览
+            const reader = new FileReader();
+            reader.onload = function (e) {
+                imagePreview.src = e.target.result;
+            };
+            reader.readAsDataURL(file);
+
+            hideMessages();
+        }
+
+        // 上传并识别
+        btnUpload.addEventListener('click', async function () {
+            if (!selectedFile) {
+                showError('请先选择图片');
+                return;
+            }
+
+            const formData = new FormData();
+            formData.append('id_card_image', selectedFile);
+
+            uploadSection.style.display = 'none';
+            loading.classList.add('show');
+            hideMessages();
+
+            try {
+                const response = await fetch('/api/verify_id_card', {
+                    method: 'POST',
+                    body: formData
+                });
+
+                const data = await response.json();
+
+                if (response.ok) {
+                    displayResult(data);
+                    showSuccess('识别成功!');
+                } else {
+                    showError(data.error || '识别失败,请重试');
+                    uploadSection.style.display = 'block';
+                }
+            } catch (error) {
+                showError('网络错误,请重试');
+                uploadSection.style.display = 'block';
+            } finally {
+                loading.classList.remove('show');
+            }
+        });
+
+        // 显示识别结果
+        function displayResult(data) {
+            document.getElementById('resultName').textContent = data.name || '未识别';
+            document.getElementById('resultGender').textContent = data.gender || '未识别';
+            document.getElementById('resultNation').textContent = data.nation || '未识别';
+            document.getElementById('resultBirth').textContent = data.birth || '未识别';
+            document.getElementById('resultAddress').textContent = data.address || '未识别';
+            document.getElementById('resultIdNumber').textContent = data.id_number || '未识别';
+
+            // 标记未识别的字段
+            document.querySelectorAll('.result-value').forEach(el => {
+                if (el.textContent === '未识别') {
+                    el.classList.add('empty');
+                } else {
+                    el.classList.remove('empty');
+                }
+            });
+
+            mainContent.classList.add('show');
+        }
+
+        // 重置
+        btnReset.addEventListener('click', function () {
+            selectedFile = null;
+            fileInput.value = '';
+            imagePreview.src = '';
+            mainContent.classList.remove('show');
+            uploadSection.style.display = 'block';
+            btnUpload.disabled = true;
+            hideMessages();
+        });
+
+        // 显示错误信息
+        function showError(message) {
+            errorMessage.textContent = message;
+            errorMessage.classList.add('show');
+            successMessage.classList.remove('show');
+        }
+
+        // 显示成功信息
+        function showSuccess(message) {
+            successMessage.textContent = message;
+            successMessage.classList.add('show');
+            errorMessage.classList.remove('show');
+        }
+
+        // 隐藏所有消息
+        function hideMessages() {
+            errorMessage.classList.remove('show');
+            successMessage.classList.remove('show');
+        }
+    </script>
+</body>
+
+</html>

+ 76 - 7
utils/ocr_utils.py

@@ -1,15 +1,26 @@
 
 
 import os
 import os
+# 禁用所有可能导致 "could not execute a primitive" 错误的优化
 os.environ["FLAGS_use_mkldnn"] = "0"
 os.environ["FLAGS_use_mkldnn"] = "0"
 os.environ["FLAGS_prim_skip_onednn"] = "1"
 os.environ["FLAGS_prim_skip_onednn"] = "1"
 os.environ["FLAGS_enable_pir_api"] = "0"
 os.environ["FLAGS_enable_pir_api"] = "0"
+os.environ["FLAGS_use_cuda_managed_memory"] = "0"
+os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
+os.environ["OMP_NUM_THREADS"] = "1"
 
 
 import cv2
 import cv2
 import re
 import re
 from paddleocr import PaddleOCR
 from paddleocr import PaddleOCR
 
 
 # 初始化 PaddleOCR(只需初始化一次,可复用)
 # 初始化 PaddleOCR(只需初始化一次,可复用)
-ocr = PaddleOCR(use_angle_cls=True, lang='ch')
+# 使用 CPU 模式并禁用可能的优化
+ocr = PaddleOCR(
+    use_angle_cls=True,
+    lang='ch',
+    use_gpu=False,
+    enable_mkldnn=False,
+    cpu_threads=1
+)
 
 
 def extract_text_from_image(image_path):
 def extract_text_from_image(image_path):
     """使用 PaddleOCR 从图片中提取文本"""
     """使用 PaddleOCR 从图片中提取文本"""
@@ -32,46 +43,104 @@ def extract_text_from_image(image_path):
 
 
 def extract_id_card_info(image_path):
 def extract_id_card_info(image_path):
     """
     """
-    从身份证图片中提取姓名和身份证号
-    返回: {'name': 姓名, 'id_number': 身份证号}
+    从身份证图片中提取完整信息
+    返回: {
+        'name': 姓名,
+        'gender': 性别,
+        'nation': 民族,
+        'birth': 出生日期,
+        'address': 地址,
+        'id_number': 身份证号
+    }
     """
     """
     try:
     try:
         # 使用 PaddleOCR 识别身份证
         # 使用 PaddleOCR 识别身份证
         result = ocr.ocr(image_path)
         result = ocr.ocr(image_path)
 
 
         name = None
         name = None
+        gender = None
+        nation = None
+        birth = None
+        address = None
         id_number = None
         id_number = None
 
 
         if result and result[0]:
         if result and result[0]:
+            all_text = []
             for line in result[0]:
             for line in result[0]:
                 text = line[1][0]
                 text = line[1][0]
                 confidence = line[1][1]
                 confidence = line[1][1]
+                all_text.append(text)
 
 
                 # 识别身份证号(18位或15位)
                 # 识别身份证号(18位或15位)
                 id_match = re.search(r'\d{17}[\dxX]|\d{15}', text)
                 id_match = re.search(r'\d{17}[\dxX]|\d{15}', text)
                 if id_match and confidence > 0.8:
                 if id_match and confidence > 0.8:
                     id_number = id_match.group(0).upper()
                     id_number = id_match.group(0).upper()
 
 
-                # 识别姓名(通常在"姓名"关键字后面)
+                # 识别姓名
                 if '姓名' in text:
                 if '姓名' in text:
-                    # 获取姓名字段
                     name_match = re.search(r'姓名[:\s]*([^\s]+)', text)
                     name_match = re.search(r'姓名[:\s]*([^\s]+)', text)
                     if name_match:
                     if name_match:
                         name = name_match.group(1)
                         name = name_match.group(1)
-                # 或者直接识别中文姓名(2-4个汉字)
                 elif not name and confidence > 0.9:
                 elif not name and confidence > 0.9:
                     name_match = re.search(r'^[一-龥]{2,4}$', text)
                     name_match = re.search(r'^[一-龥]{2,4}$', text)
-                    if name_match:
+                    if name_match and not any(kw in name_match.group(0) for kw in ['姓名', '性别', '民族', '出生', '住址', '公民']):
                         name = name_match.group(0)
                         name = name_match.group(0)
 
 
+                # 识别性别
+                if '性别' in text:
+                    gender_match = re.search(r'性别[:\s]*(男|女)', text)
+                    if gender_match:
+                        gender = gender_match.group(1)
+                elif text in ['男', '女'] and confidence > 0.9:
+                    gender = text
+
+                # 识别民族
+                if '民族' in text:
+                    nation_match = re.search(r'民族[:\s]*([^\s]+)', text)
+                    if nation_match:
+                        nation = nation_match.group(1)
+                elif confidence > 0.9 and re.match(r'^[一-龥]{1,4}族$', text):
+                    nation = text
+
+                # 识别出生日期
+                if '出生' in text:
+                    birth_match = re.search(r'(\d{4})年(\d{1,2})月(\d{1,2})日', text)
+                    if birth_match:
+                        birth = f"{birth_match.group(1)}-{birth_match.group(2).zfill(2)}-{birth_match.group(3).zfill(2)}"
+                elif not birth:
+                    birth_match = re.search(r'(\d{4})年(\d{1,2})月(\d{1,2})日', text)
+                    if birth_match:
+                        birth = f"{birth_match.group(1)}-{birth_match.group(2).zfill(2)}-{birth_match.group(3).zfill(2)}"
+
+                # 识别住址
+                if '住址' in text or '址' in text:
+                    address_match = re.search(r'住?址[:\s]*(.+)', text)
+                    if address_match:
+                        address = address_match.group(1)
+
+            # 如果住址没有完整识别,尝试合并多行文本
+            if not address:
+                full_text = '\n'.join(all_text)
+                address_match = re.search(r'住?址[:\s]*([^\n]+(?:\n[^姓名性别民族出生公民身份号]+)*)', full_text)
+                if address_match:
+                    address = address_match.group(1).replace('\n', '')
+
         return {
         return {
             'name': name,
             'name': name,
+            'gender': gender,
+            'nation': nation,
+            'birth': birth,
+            'address': address,
             'id_number': id_number
             'id_number': id_number
         }
         }
     except Exception as e:
     except Exception as e:
         print(f"身份证识别错误: {str(e)}")
         print(f"身份证识别错误: {str(e)}")
         return {
         return {
             'name': None,
             'name': None,
+            'gender': None,
+            'nation': None,
+            'birth': None,
+            'address': None,
             'id_number': None
             'id_number': None
         }
         }