tien_nemo

demo

from paddleocr import PaddleOCR
from pdf2image import convert_from_path
import os
import time
import numpy as np
import json
# ==== Config ====
pdf_path = "D:/Learning_Tien/OCR/PaddleOCR/pdf/data_picking_detail.pdf"
output_folder = "D:/Learning_Tien/OCR/ocr-mapping/public/image"
os.makedirs(output_folder, exist_ok=True)
pdf_name = "data_picking_detail"
timestamp = int(time.time())
img_base_name = f"{pdf_name}_{timestamp}"
# ==== OCR Init ====
ocr = PaddleOCR(
use_doc_orientation_classify=False,
use_doc_unwarping=False,
use_textline_orientation=False
)
# ==== PDF to Image ====
pages = convert_from_path(pdf_path, first_page=1, last_page=1)
image_path = os.path.join(output_folder, f"{img_base_name}.jpg")
pages[0].save(image_path, "JPEG")
# ==== Run OCR ====
image_np = np.array(pages[0])
results = ocr.predict(image_np)
# ==== Convert polygon to bbox ====
def poly_to_bbox(poly):
xs = [p[0] for p in poly]
ys = [p[1] for p in poly]
return [int(min(xs)), int(min(ys)), int(max(xs)), int(max(ys))]
# ==== Build ocrData ====
ocr_data_list = []
for res in results:
for text, poly in zip(res['rec_texts'], res['rec_polys']):
bbox = poly_to_bbox(poly)
ocr_data_list.append({
"text": text,
"bbox": bbox,
"field": "",
"hideBorder": False
})
# ==== Save JSON ====
json_path = os.path.join(output_folder, f"{pdf_name}_{timestamp}.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(ocr_data_list, f, ensure_ascii=False, indent=2)
print(f"Saved OCR data JSON to: {json_path}")
[
{
"text": "出庫指示書",
"bbox": [
65,
73,
449,
128
],
"field": "",
"hideBorder": false
},
{
"text": "出庫指示No.",
"bbox": [
1303,
76,
1472,
111
],
"field": "",
"hideBorder": false
},
{
"text": "391189",
"bbox": [
1498,
78,
1604,
110
],
"field": "",
"hideBorder": false
},
{
"text": "2025/06/24",
"bbox": [
952,
94,
1106,
121
],
"field": "",
"hideBorder": false
},
{
"text": "18:57迄",
"bbox": [
1139,
89,
1250,
124
],
"field": "",
"hideBorder": false
},
{
"text": "PAGE1/1",
"bbox": [
1473,
121,
1594,
153
],
"field": "",
"hideBorder": false
},
{
"text": "運送形態",
"bbox": [
83,
145,
239,
184
],
"field": "",
"hideBorder": false
},
{
"text": "30西濃運輸",
"bbox": [
234,
144,
485,
185
],
"field": "",
"hideBorder": false
},
{
"text": "得意先",
"bbox": [
84,
206,
202,
246
],
"field": "",
"hideBorder": false
},
{
"text": "42031(株)フジカケ",
"bbox": [
243,
206,
556,
244
],
"field": "",
"hideBorder": false
},
{
"text": "ミタケ",
"bbox": [
536,
205,
707,
247
],
"field": "",
"hideBorder": false
},
{
"text": "住所",
"bbox": [
84,
266,
179,
298
],
"field": "",
"hideBorder": false
},
{
"text": "〒5050100岐阜県可児郡御嵩町中2411-7",
"bbox": [
207,
267,
834,
297
],
"field": "",
"hideBorder": false
},
{
"text": "電話番号",
"bbox": [
88,
308,
214,
340
],
"field": "",
"hideBorder": false
},
{
"text": "0574673181",
"bbox": [
215,
310,
382,
338
],
"field": "",
"hideBorder": false
},
{
"text": "出庫者",
"bbox": [
925,
331,
1008,
367
],
"field": "",
"hideBorder": false
},
{
"text": "検品者",
"bbox": [
1176,
330,
1261,
366
],
"field": "",
"hideBorder": false
},
{
"text": "包者",
"bbox": [
1431,
331,
1516,
367
],
"field": "",
"hideBorder": false
},
{
"text": "担当者",
"bbox": [
86,
344,
182,
381
],
"field": "",
"hideBorder": false
},
{
"text": "NAS00240渡邊雅章",
"bbox": [
239,
343,
518,
380
],
"field": "",
"hideBorder": false
},
{
"text": "摘要",
"bbox": [
83,
386,
183,
429
],
"field": "",
"hideBorder": false
},
{
"text": "棚番",
"bbox": [
34,
515,
97,
554
],
"field": "",
"hideBorder": false
},
{
"text": "品",
"bbox": [
345,
519,
378,
552
],
"field": "",
"hideBorder": false
},
{
"text": "名",
"bbox": [
423,
519,
456,
551
],
"field": "",
"hideBorder": false
},
{
"text": "規",
"bbox": [
870,
517,
909,
552
],
"field": "",
"hideBorder": false
},
{
"text": "格",
"bbox": [
948,
517,
986,
552
],
"field": "",
"hideBorder": false
},
{
"text": "数量",
"bbox": [
1110,
516,
1174,
554
],
"field": "",
"hideBorder": false
},
{
"text": "受注番号",
"bbox": [
1393,
519,
1505,
551
],
"field": "",
"hideBorder": false
},
{
"text": "B0504",
"bbox": [
39,
567,
124,
600
],
"field": "",
"hideBorder": false
},
{
"text": "ダービー",
"bbox": [
297,
565,
419,
600
],
"field": "",
"hideBorder": false
},
{
"text": "斜ニッパー",
"bbox": [
444,
564,
600,
602
],
"field": "",
"hideBorder": false
},
{
"text": "#30 150MM",
"bbox": [
861,
568,
1014,
602
],
"field": "",
"hideBorder": false
},
{
"text": "2",
"bbox": [
1146,
568,
1176,
606
],
"field": "",
"hideBorder": false
},
{
"text": "(",
"bbox": [
1232,
566,
1256,
601
],
"field": "",
"hideBorder": false
},
{
"text": ")",
"bbox": [
1363,
567,
1385,
600
],
"field": "",
"hideBorder": false
},
{
"text": "250430015",
"bbox": [
1419,
567,
1562,
598
],
"field": "",
"hideBorder": false
},
{
"text": "4562144610607",
"bbox": [
295,
611,
474,
638
],
"field": "",
"hideBorder": false
},
{
"text": "3220060",
"bbox": [
567,
610,
668,
638
],
"field": "",
"hideBorder": false
},
{
"text": "C3101",
"bbox": [
40,
654,
121,
687
],
"field": "",
"hideBorder": false
},
{
"text": "タジマ",
"bbox": [
296,
653,
389,
685
],
"field": "",
"hideBorder": false
},
{
"text": "スーパー墨汁",
"bbox": [
414,
654,
599,
685
],
"field": "",
"hideBorder": false
},
{
"text": "180ML PSB2-180",
"bbox": [
787,
656,
1013,
687
],
"field": "",
"hideBorder": false
},
{
"text": "3",
"bbox": [
1145,
655,
1176,
693
],
"field": "",
"hideBorder": false
},
{
"text": "(",
"bbox": [
1232,
653,
1257,
687
],
"field": "",
"hideBorder": false
},
{
"text": ")",
"bbox": [
1362,
653,
1386,
686
],
"field": "",
"hideBorder": false
},
{
"text": "250430015",
"bbox": [
1420,
655,
1563,
686
],
"field": "",
"hideBorder": false
},
{
"text": "4975364054074",
"bbox": [
295,
698,
474,
725
],
"field": "",
"hideBorder": false
},
{
"text": "550207",
"bbox": [
567,
697,
655,
726
],
"field": "",
"hideBorder": false
},
{
"text": "C3101",
"bbox": [
40,
741,
122,
774
],
"field": "",
"hideBorder": false
},
{
"text": "タジマ",
"bbox": [
295,
738,
390,
774
],
"field": "",
"hideBorder": false
},
{
"text": "雨の日墨汁",
"bbox": [
414,
740,
570,
774
],
"field": "",
"hideBorder": false
},
{
"text": "PSB3-180",
"bbox": [
879,
743,
1013,
774
],
"field": "",
"hideBorder": false
},
{
"text": "2",
"bbox": [
1146,
742,
1176,
780
],
"field": "",
"hideBorder": false
},
{
"text": "(",
"bbox": [
1232,
740,
1257,
774
],
"field": "",
"hideBorder": false
},
{
"text": ")",
"bbox": [
1361,
740,
1386,
774
],
"field": "",
"hideBorder": false
},
{
"text": "250430015",
"bbox": [
1419,
741,
1562,
772
],
"field": "",
"hideBorder": false
},
{
"text": "49270501",
"bbox": [
294,
783,
406,
814
],
"field": "",
"hideBorder": false
},
{
"text": "548140",
"bbox": [
567,
783,
655,
812
],
"field": "",
"hideBorder": false
},
{
"text": "明细行数= 3",
"bbox": [
882,
822,
1136,
871
],
"field": "",
"hideBorder": false
}
]
\ No newline at end of file
<html lang="en"><head>
<meta charset="UTF-8">
<title>OCR Mapping with Manual Select Tool</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
body { font-family: sans-serif; background: #f5f5f5; }
#app { display: flex; gap: 20px; padding: 20px; }
.left-panel {
width: 500px; background: #fff; padding: 15px;
border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1);
}
.form-group { margin-bottom: 15px; }
.form-group label { font-weight: bold; display: block; margin-bottom: 5px; }
.form-group input { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
.right-panel { flex: 1; position: relative; background: #eee; border-radius: 8px; overflow: hidden; user-select: none; }
.pdf-container { position: relative; display: inline-block; }
.bbox {
position: absolute;
border: 2px solid #ff5252;
/*background-color: rgba(255, 82, 82, 0.2);*/
cursor: pointer;
}
.bbox.active {
/*border-color: #2196F3;*/
background-color: rgb(33 243 132 / 30%);
}
select {
position: absolute;
z-index: 10;
background: #fff;
border: 1px solid #ccc;
}
.select-box {
position: absolute;
/*border: 2px dashed #2196F3;*/
background-color: rgba(33, 150, 243, 0.2);
pointer-events: none;
z-index: 5;
}
.delete-btn {
position: absolute;
bottom: -10px;
right: -10px;
background: #ff4d4d;
color: #fff;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 14px;
padding: 3px 6px;
z-index: 20;
}
</style>
</head>
<body>
<div id="app">
<!-- Right: PDF viewer + select tool -->
<div class="right-panel" >
<div class="pdf-container" ref="pdfContainer"
@mousedown="startSelect"
@mousemove="onSelect"
@mouseup="endSelect">
<img
ref="pdfImage"
:src="pdfImageUrl"
@load="onImageLoad"
style="width: 100%; height: auto;pointer-events: none;"
/>
<!-- Vùng kéo chọn -->
<div v-if="selectBox.show" class="select-box"
:style="{ left: selectBox.x + 'px', top: selectBox.y + 'px', width: selectBox.width + 'px', height: selectBox.height + 'px' }"></div>
<!-- Vẽ bbox OCR -->
<div
v-for="(item, index) in ocrData"
:key="index"
v-if="!item.isDeleted"
class="bbox"
:class="{ active: index === activeIndex }"
:data-field="item.field"
:style="getBoxStyle(item, index)"
@click="selectingIndex = index">
<button v-if="item.isManual && item.showDelete"
class="delete-btn"
@click.stop="deleteBox(index)">🗑</button>
</div>
<!-- Dropdown OCR -->
<select v-if="selectingIndex !== null"
:style="getSelectStyle(ocrData[selectingIndex])"
v-model="ocrData[selectingIndex].field"
@change="applyMapping"
>
<option disabled value="">-- Chọn trường dữ liệu --</option>
<option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option>
</select>
<!-- Dropdown thủ công -->
<select v-if="selectBox.showDropdown"
:style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }"
v-model="manualField"
@change="applyManualMapping"
@click.stop
>
<option disabled value="">-- Chọn trường dữ liệu --</option>
<option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option>
</select>
</div>
</div>
<!-- Left: Form inputs -->
<div class="left-panel">
<div v-for="field in fieldOptions" :key="field.value" class="form-group">
<label>{{ field.label }}</label>
<input v-model="formData[field.value]" @focus="highlightField(field.value)">
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
pdfImageUrl: "",
selectingIndex: null,
isMappingManually: false,
isSelecting: false,
activeField: null,
manualField: "",
formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" },
fieldOptions: [
{ value: "export_date", label: "Ngày xuất" },
{ value: "order_code", label: "Mã đơn hàng" },
{ value: "customer", label: "Khách hàng" },
{ value: "address", label: "Địa chỉ" },
{ value: "staff", label: "Nhân viên" }
],
ocrData: [],
selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 },
manualIndex: null
}
},
mounted() {
this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python
this.initData();
},
methods: {
deleteBox(index) {
const item = this.ocrData[index];
if (item.isManual) {
const manualBbox = item.bbox;
// Hiện lại border các box OCR gốc nằm trong vùng thủ công
this.ocrData.forEach(o => {
if (!o.isManual && this.isBoxInside(o.bbox, manualBbox)) {
o.hideBorder = false;
}
});
// Đánh dấu xoá vùng thủ công
this.ocrData[index].isDeleted = true;
this.ocrData[index].showDelete = false;
// Reset trạng thái nếu đây là vùng đang chọn
if (this.manualIndex === index) {
this.isMappingManually = false;
this.selectBox.show = false;
this.selectBox.showDropdown = false;
this.manualField = "";
this.manualIndex = null;
}
}
},
async initData() {
await this.loadOCRData();
},
async loadOCRData() {
const res = await fetch("/public/image/data_picking_detail_1754967679.json");
this.ocrData = await res.json();
},
onImageLoad() {
const img = this.$refs.pdfImage;
this.imageWidth = img.naturalWidth;
this.imageHeight = img.naturalHeight;
},
getBoxStyle(item) {
if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {};
const [x1, y1, x2, y2] = item.bbox;
const displayedWidth = this.$refs.pdfImage.clientWidth;
const displayedHeight = this.$refs.pdfImage.clientHeight;
const scaleX = displayedWidth / this.imageWidth;
const scaleY = displayedHeight / this.imageHeight;
return {
position: 'absolute',
left: `${Math.round(x1 * scaleX)}px`,
top: `${Math.round(y1 * scaleY)}px`,
width: `${Math.round((x2 - x1) * scaleX)}px`,
height: `${Math.round((y2 - y1) * scaleY)}px`,
border: item.hideBorder ? 'none' : '2px solid ' + (this.activeField === item.field ? '#199601' : '#ff5252'),
//backgroundColor: item.hideBorder ? 'transparent' : (this.activeField === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'),
boxSizing: 'border-box',
cursor: 'pointer',
zIndex: item.isManual ? 30 : 10
};
},
highlightField(field) {
this.activeField = field;
},
startSelect(e) {
if (this.isMappingManually || e.button !== 0) return;
this.isSelecting = true;
const rect = this.$refs.pdfContainer.getBoundingClientRect();
this.selectBox.startX = e.clientX - rect.left;
this.selectBox.startY = e.clientY - rect.top;
this.selectBox.x = this.selectBox.startX;
this.selectBox.y = this.selectBox.startY;
this.selectBox.width = 0;
this.selectBox.height = 0;
this.selectBox.show = true;
this.selectBox.showDropdown = false;
this.manualField = "";
},
onSelect(e) {
if (!this.isSelecting) return;
const rect = this.$refs.pdfContainer.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
this.selectBox.x = Math.min(currentX, this.selectBox.startX);
this.selectBox.y = Math.min(currentY, this.selectBox.startY);
this.selectBox.width = Math.abs(currentX - this.selectBox.startX);
this.selectBox.height = Math.abs(currentY - this.selectBox.startY);
},
endSelect(e) {
if (!this.isSelecting) return;
this.isSelecting = false;
if (this.selectBox.width < 10 || this.selectBox.height < 10) {
this.selectBox.show = false;
return;
}
// displayed coords (như hiện tại, dùng để hiển thị select overlay)
const dispX1 = this.selectBox.x;
const dispY1 = this.selectBox.y;
const dispX2 = this.selectBox.x + this.selectBox.width;
const dispY2 = this.selectBox.y + this.selectBox.height;
// scale: displayed -> original
const displayedWidth = this.$refs.pdfImage.clientWidth;
const displayedHeight = this.$refs.pdfImage.clientHeight;
const scaleX = this.imageWidth / displayedWidth;
const scaleY = this.imageHeight / displayedHeight;
// bbox ở hệ gốc (original image pixels) — dùng để so sánh với ocrData và lưu vào ocrData
const origBbox = [
Math.round(dispX1 * scaleX),
Math.round(dispY1 * scaleY),
Math.round(dispX2 * scaleX),
Math.round(dispY2 * scaleY)
];
// Ẩn border các box OCR gốc nằm giao nhau với vùng thủ công (dùng coords gốc)
this.ocrData.forEach(item => {
if (!item.isManual && this.isBoxInside(item.bbox, origBbox)) {
item.hideBorder = true;
}
});
// Thêm box thủ công (lưu theo coords gốc)
this.ocrData.push({
text: "",
bbox: origBbox,
field: "",
isManual: true,
showDelete: true,
isDeleted: false,
hideBorder: false
});
this.manualIndex = this.ocrData.length - 1;
this.isMappingManually = true;
this.selectBox.showDropdown = true;
e.stopPropagation();
e.preventDefault();
}
,
applyMapping() {
const item = this.ocrData[this.selectingIndex];
if (item && item.isManual) {
this.manualIndex = this.selectingIndex;
this.manualField = item.field; // đảm bảo sync field hiện tại
this.applyManualMapping();
return;
}
if (item.field) {
this.formData[item.field] = item.text;
this.activeField = item.field;
}
this.selectingIndex = null;
},
applyManualMapping() {
if (!this.manualField) return;
const manualIndex = this.manualIndex;
const newBbox = this.ocrData[manualIndex].bbox;
let combinedText = [];
this.ocrData.forEach(item => {
if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) {
const partial = this.getPartialText(item.text, item.bbox, newBbox);
if (partial) combinedText.push(partial);
// combinedText.push(item.text.trim());
}
});
const finalText = combinedText.join(" ");
this.ocrData[manualIndex].field = this.manualField;
this.formData[this.manualField] = finalText;
this.activeField = this.manualField;
console.log('manualField',this.manualField,this.manualIndex)
// Reset trạng thái chọn
this.isMappingManually = false;
this.selectBox.show = false;
this.selectBox.showDropdown = false;
// this.manualField = "";
// this.manualIndex = null;
},
isBoxInside(inner, outer) {
return !(
inner[2] < outer[0] || // box bên trái vùng chọn
inner[0] > outer[2] || // box bên phải vùng chọn
inner[3] < outer[1] || // box phía trên vùng chọn
inner[1] > outer[3] // box phía dưới vùng chọn
);
},
getPartialText(text, bbox, selectBbox) {
const [x1, y1, x2, y2] = bbox;
const [sx1, sy1, sx2, sy2] = selectBbox;
// Chiều rộng box OCR
const boxWidth = x2 - x1;
// Vị trí start và end tương đối trong text
let startRatio = Math.max(0, (sx1 - x1) / boxWidth);
let endRatio = Math.min(1, (sx2 - x1) / boxWidth);
const startIndex = Math.floor(startRatio * text.length);
const endIndex = Math.ceil(endRatio * text.length);
return text.substring(startIndex, endIndex).trim();
},
getSelectStyle(item) {
if (!this.imageWidth) return { position: 'absolute' };
const [x1, y1, x2, y2] = item.bbox;
const displayedWidth = this.$refs.pdfImage.clientWidth;
const displayedHeight = this.$refs.pdfImage.clientHeight;
const scaleX = displayedWidth / this.imageWidth;
const scaleY = displayedHeight / this.imageHeight;
return {
position: 'absolute',
left: `${Math.round(x1 * scaleX)}px`,
top: `${Math.round(y2 * scaleY)}px`,
zIndex: 9999
};
}
}
});
</script>
</body></html>