|
|
4 weeks ago | |
|---|---|---|
| api | 4 weeks ago | |
| data | 4 weeks ago | |
| input | 4 weeks ago | |
| models | 4 weeks ago | |
| ocr | 4 weeks ago | |
| tests | 4 weeks ago | |
| utils | 4 weeks ago | |
| visualize | 4 weeks ago | |
| .gitignore | 4 weeks ago | |
| OPTIMIZATION_REPORT.md | 4 weeks ago | |
| README.md | 4 weeks ago | |
| download_models.py | 4 weeks ago | |
| main.py | 4 weeks ago | |
| requirements.txt | 4 weeks ago | |
README.md
Vision-OCR: 图片 OCR 识别系统
基于 PaddleOCR 的图片 OCR 识别系统,支持单张图片、批量图片和目录扫描,提供文本检测、识别、方向分类,输出结构化识别结果。同时提供 REST API 服务,支持 HTTP 接口调用。
功能特性
- 多输入模式: 单张图片、多张图片列表、目录批量扫描
- 完整 OCR 能力: 文本检测 + 文本识别 + 方向分类
- 结构化输出: 文字内容、置信度、位置信息(4 点坐标)
- 快递单解析: 自动合并分散文本块,提取运单号、收寄件人等结构化信息
- 可视化展示: 在图片上绘制文本框和识别结果
- 结果导出: 支持 JSON 结果导出和标注图片保存
- ROI 裁剪: 支持只识别图片指定区域
- REST API: 提供 HTTP 接口,支持文件上传和 Base64 两种方式
- 模块化设计: 图片加载与 OCR 逻辑完全解耦,便于扩展
- 全本地运行: 不依赖任何云服务
项目结构
vision-ocr/
├── api/ # REST API 模块
│ ├── __init__.py
│ ├── main.py # FastAPI 应用入口
│ ├── dependencies.py # 依赖注入
│ ├── exceptions.py # 自定义异常
│ ├── security.py # 安全验证
│ ├── routes/ # 路由模块
│ │ ├── health.py # 健康检查端点
│ │ └── ocr.py # OCR 识别端点
│ └── schemas/ # 数据模型
│ ├── request.py # 请求模型
│ └── response.py # 响应模型
├── input/ # 图片输入模块
│ ├── __init__.py
│ └── loader.py # 图片加载器
├── ocr/ # OCR 处理模块
│ ├── __init__.py
│ ├── engine.py # PaddleOCR 引擎封装
│ ├── pipeline.py # OCR 处理管道
│ └── express_parser.py # 快递单解析器
├── visualize/ # 可视化模块
│ ├── __init__.py
│ └── draw.py # 结果绘制器
├── utils/ # 工具模块
│ ├── __init__.py
│ └── config.py # 配置管理
├── tests/ # 测试模块
│ ├── conftest.py # pytest 配置
│ ├── test_api_health.py # 健康检查测试
│ └── test_api_ocr.py # OCR API 测试
├── models/ # 模型文件目录
├── main.py # CLI 主入口
├── download_models.py # 模型下载脚本
├── requirements.txt # 依赖清单
└── README.md
环境要求
- Python 3.9+
- 支持的操作系统: Windows / Linux / macOS
安装
1. 克隆项目
git clone <repository-url>
cd vision-ocr
2. 创建虚拟环境(推荐)
python -m venv venv
# Windows
venv\Scripts\activate
# Linux/macOS
source venv/bin/activate
3. 安装依赖
pip install -r requirements.txt
4. 模型说明
本项目已内置 PaddleOCR 模型文件(位于 models/ 目录),clone 后即可直接使用,无需额外下载。
备用方案:如果模型文件缺失或需要更新,可运行
python download_models.py重新下载。
模型详情
本项目使用 PaddleOCR 的 PP-OCRv4 系列模型,包含 3 个模型协同工作:
| 模型类型 | 模型名称 | 作用 | 大小 |
|---|---|---|---|
| det (检测模型) | ch_PP-OCRv4_det_infer | 定位图像中所有文本区域的位置,输出每个文本块的 4 点边界框坐标 | ~4.7MB |
| rec (识别模型) | ch_PP-OCRv4_rec_infer | 将检测到的文本区域图像转换为实际文字内容,输出文本和置信度 | ~10MB |
| cls (方向分类模型) | ch_ppocr_mobile_v2.0_cls_infer | 判断文本是正向(0度)还是倒置(180度),用于矫正倒置文本后再识别 | ~1.4MB |
OCR 处理流程:
输入图像 -> [det 检测] -> 文本区域 -> [cls 分类] -> 方向矫正 -> [rec 识别] -> 文字结果
模型下载地址:
- 检测模型: https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_det_infer.tar
- 识别模型: https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_rec_infer.tar
- 方向分类模型: https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar
注意: 可通过 --no-angle-cls 参数禁用方向分类模型,适用于文本方向固定的场景,可略微提升处理速度。
5. GPU 加速(可选)
如需使用 GPU 加速,请安装对应 CUDA 版本的 PaddlePaddle:
# CUDA 11.8
pip install paddlepaddle-gpu==2.5.2.post118 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html
# CUDA 12.0
pip install paddlepaddle-gpu==2.5.2.post120 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html
使用方法
基础用法
# 识别单张图片
python main.py --image path/to/image.jpg
# 识别目录中的所有图片
python main.py --dir path/to/images/
# 识别目录中的特定格式图片
python main.py --dir path/to/images/ --pattern "*.png"
# 递归搜索子目录
python main.py --dir path/to/images/ --recursive
高级选项
# 启用 GPU 加速
python main.py --image test.jpg --gpu
# 启用 ROI 区域裁剪(只识别画面中央 60% 区域)
python main.py --image test.jpg --roi 0.2 0.2 0.6 0.6
# 调整置信度阈值(过滤低置信度结果)
python main.py --image test.jpg --drop-score 0.7
# 切换识别语言
python main.py --image test.jpg --lang en # 英文
python main.py --image test.jpg --lang ch # 中文(默认)
# 禁用方向分类(轻微提升速度)
python main.py --image test.jpg --no-angle-cls
# 显示可视化窗口
python main.py --image test.jpg --show-window
# 保存标注后的图片
python main.py --image test.jpg --save-image
# 指定输出目录
python main.py --dir images/ --output-dir results/
完整参数列表
| 参数 | 简写 | 说明 | 默认值 |
|---|---|---|---|
--image |
-i |
单张图片路径 | - |
--dir |
-d |
图片目录路径 | - |
--pattern |
-p |
文件匹配模式 | - |
--recursive |
-r |
递归搜索子目录 | False |
--lang |
-l |
OCR 语言 | ch |
--gpu |
- | 启用 GPU 加速 | False |
--no-angle-cls |
- | 禁用方向分类 | False |
--drop-score |
- | 置信度阈值 | 0.5 |
--roi |
- | ROI 区域 (x y w h) | - |
--show-window |
- | 显示可视化窗口 | False |
--no-confidence |
- | 不显示置信度 | False |
--output-dir |
-o |
输出目录路径 | - |
--save-image |
- | 保存标注后的图片 | False |
--no-json |
- | 不保存 JSON 结果 | False |
--json-filename |
- | JSON 结果文件名 | ocr_result.json |
--express |
-e |
启用快递单解析模式 | False |
运行时快捷键
| 按键 | 功能 |
|---|---|
q |
退出程序 |
| 任意键 | 处理下一张图片 |
结果导出
程序处理完成后会自动将所有识别结果导出到 JSON 文件:
- 默认输出文件:
ocr_result.json - 可通过
--json-filename参数指定文件名 - 可通过
--output-dir参数指定输出目录
汇总 JSON 格式:
{
"total_images": 10,
"total_text_blocks": 45,
"results": [
{
"image_index": 1,
"image_path": "path/to/image.jpg",
"timestamp": 1704355200.123,
"processing_time_ms": 45.2,
"text_count": 3,
"average_confidence": 0.92,
"roi_applied": false,
"roi_rect": null,
"text_blocks": [
{
"text": "识别的文本",
"confidence": 0.95,
"bbox": [[100, 50], [200, 50], [200, 80], [100, 80]],
"bbox_with_offset": [[100, 50], [200, 50], [200, 80], [100, 80]],
"center": [150, 65],
"width": 100,
"height": 30
}
]
}
]
}
JSON 字段说明
顶层字段
| 字段 | 类型 | 说明 |
|---|---|---|
total_images |
int | 处理的图片总数 |
total_text_blocks |
int | 所有图片识别出的文本块总数 |
results |
array | 每张图片的识别结果数组 |
单张图片结果字段
| 字段 | 类型 | 说明 |
|---|---|---|
image_index |
int | 图片索引,从 1 开始递增 |
image_path |
string | 图片文件的完整路径 |
timestamp |
float | 处理完成时的 Unix 时间戳(秒) |
processing_time_ms |
float | OCR 处理耗时(毫秒) |
text_count |
int | 该图片识别出的文本块数量 |
average_confidence |
float | 所有文本块的平均置信度 (0.0 ~ 1.0) |
roi_applied |
bool | 是否应用了 ROI 区域裁剪 |
roi_rect |
array|null | ROI 矩形区域 [x, y, width, height],未应用时为 null |
text_blocks |
array | 识别出的文本块数组 |
文本块 (text_blocks) 字段
| 字段 | 类型 | 说明 |
|---|---|---|
text |
string | 识别出的文本内容 |
confidence |
float | 识别置信度 (0.0 ~ 1.0),越高表示识别结果越可靠 |
bbox |
array | 文本边界框的 4 个顶点坐标 [[x1,y1], [x2,y2], [x3,y3], [x4,y4]],顺序为左上、右上、右下、左下。如果启用了 ROI,坐标相对于 ROI 区域 |
bbox_with_offset |
array | 带偏移的边界框坐标,已还原到原图坐标系。格式同 bbox |
center |
array | 文本块中心点坐标 [cx, cy] |
width |
float | 文本块宽度(像素) |
height |
float | 文本块高度(像素) |
快递单解析模式
使用 --express 参数启用快递单解析模式,系统会自动:
- 合并分散文本块: 基于位置信息将同一行的文本块合并
- 提取结构化信息: 运单号、快递公司、收/寄件人姓名、电话、地址
使用方式
# 单张快递单图片
python main.py --image express.jpg --express
# 批量处理快递单图片
python main.py --dir express_images/ --express --output-dir results/
输出格式
快递单模式下的 JSON 输出格式:
{
"total_images": 5,
"total_text_blocks": 50,
"results": [
{
"image_index": 1,
"image_path": "express.jpg",
"processing_time_ms": 45.2,
"express_info": {
"tracking_number": "SF1234567890",
"sender": {
"name": "张三",
"phone": "13800138000",
"address": "北京市朝阳区xxx路"
},
"receiver": {
"name": "李四",
"phone": "13900139000",
"address": "上海市浦东新区xxx路"
},
"courier_company": "顺丰速运",
"confidence": 0.95,
"extra_fields": {},
"raw_text": "顺丰速运\n运单号:SF1234567890\n..."
},
"merged_text": "顺丰速运\n运单号:SF1234567890\n收件人:李四 13900139000\n..."
}
]
}
快递单模式 JSON 字段说明
单张图片结果字段(快递单模式)
| 字段 | 类型 | 说明 |
|---|---|---|
image_index |
int | 图片索引,从 1 开始递增 |
image_path |
string | 图片文件的完整路径 |
processing_time_ms |
float | OCR 处理耗时(毫秒) |
express_info |
object | 解析出的快递单结构化信息 |
merged_text |
string | 基于位置信息智能合并后的完整文本,同一行的文本块会被合并 |
快递单信息 (express_info) 字段
| 字段 | 类型 | 说明 |
|---|---|---|
tracking_number |
string|null | 运单号/快递单号 |
sender |
object | 寄件人信息 |
sender.name |
string|null | 寄件人姓名 |
sender.phone |
string|null | 寄件人电话(11位手机号) |
sender.address |
string|null | 寄件人地址 |
receiver |
object | 收件人信息 |
receiver.name |
string|null | 收件人姓名 |
receiver.phone |
string|null | 收件人电话(11位手机号) |
receiver.address |
string|null | 收件人地址 |
courier_company |
string|null | 快递公司名称(如:顺丰速运、圆通速递等) |
confidence |
float | 所有文本块的平均置信度 (0.0 ~ 1.0) |
extra_fields |
object | 其他识别到的额外字段(键值对形式) |
raw_text |
string | 原始合并文本,用于调试和验证 |
支持的快递公司
顺丰、圆通、中通、韵达、申通、极兔、京东、邮政、EMS、百世、德邦、天天、宅急送
编程接口
from ocr import OCRPipeline, ExpressParser
# 使用 OCRResult 的 parse_express() 方法
result = pipeline.process(image)
if result and result.text_count > 0:
# 解析快递单信息
express_info = result.parse_express()
print(f"运单号: {express_info.tracking_number}")
print(f"收件人: {express_info.receiver_name}")
print(f"收件电话: {express_info.receiver_phone}")
# 获取合并后的完整文本
merged_text = result.merge_text()
print(f"合并文本: {merged_text}")
编程接口
作为模块使用
from input import ImageLoader
from ocr import OCRPipeline
from visualize import OCRVisualizer
from utils import Config, InputConfig, InputMode
# 创建配置
config = Config.for_single_image("path/to/image.jpg")
# 创建组件
loader = ImageLoader()
pipeline = OCRPipeline(config.ocr, config.pipeline)
visualizer = OCRVisualizer(config.visualize)
# 初始化
pipeline.initialize()
# 加载并处理图片
image_info = loader.load("path/to/image.jpg")
if image_info:
result = pipeline.process(image_info.image, image_info.path)
if result and result.text_count > 0:
# 获取识别结果
for block in result.text_blocks:
print(f"文本: {block.text}")
print(f"置信度: {block.confidence}")
print(f"位置: {block.bbox}")
# 导出为 JSON
json_data = result.to_dict()
# 可视化
display_image = visualizer.draw_result(image_info.image, result)
visualizer.show(display_image, wait_key=0)
# 清理资源
visualizer.close()
批量处理
from input import ImageLoader
from ocr import OCRPipeline
from utils import Config
# 创建配置
config = Config.for_directory("path/to/images/", pattern="*.jpg")
# 创建组件
loader = ImageLoader()
pipeline = OCRPipeline(config.ocr)
pipeline.initialize()
# 批量处理
for image_info in loader.load_directory("path/to/images/"):
result = pipeline.process(image_info.image, image_info.path)
print(f"{image_info.filename}: 识别到 {result.text_count} 个文本块")
OCRResult 数据结构
{
"image_index": 1,
"image_path": "path/to/image.jpg",
"timestamp": 1704355200.123,
"processing_time_ms": 45.6,
"text_count": 3,
"average_confidence": 0.92,
"roi_applied": False,
"roi_rect": None,
"text_blocks": [
{
"text": "识别的文本",
"confidence": 0.95,
"bbox": [[x1, y1], [x2, y2], [x3, y3], [x4, y4]],
"bbox_with_offset": [[x1, y1], [x2, y2], [x3, y3], [x4, y4]],
"center": [cx, cy],
"width": 120.0,
"height": 30.0
}
]
}
模块说明
input/loader.py - 图片加载模块
提供图片加载功能,支持单张、批量和目录加载。
ImageLoader: 图片加载器类ImageInfo: 图片信息数据结构load_image(): 便捷函数,加载单张图片load_images(): 便捷函数,批量加载图片
ocr/engine.py - OCR 引擎模块
封装 PaddleOCR,提供简洁的 OCR 调用接口。
OCREngine: OCR 引擎类TextBlock: 文本块数据结构
ocr/pipeline.py - OCR 处理管道
串联图片加载、ROI 裁剪、OCR 识别、结果封装。
OCRPipeline: 处理管道类OCRResult: OCR 结果数据结构
visualize/draw.py - 可视化模块
在图像上绘制 OCR 识别结果。
OCRVisualizer: 可视化器类
utils/config.py - 配置管理模块
集中管理所有可配置参数。
Config: 全局配置聚合类InputConfig: 输入配置OCRConfig: OCR 引擎配置PipelineConfig: 管道配置VisualizeConfig: 可视化配置OutputConfig: 输出配置ROIConfig: ROI 区域配置
扩展开发
添加图片预处理器
import cv2
def denoise_preprocessor(image):
"""降噪预处理"""
return cv2.fastNlMeansDenoisingColored(image, None, 10, 10, 7, 21)
pipeline.add_preprocessor(denoise_preprocessor)
添加结果后处理器
def filter_short_text(result):
"""过滤短文本"""
result.text_blocks = [
block for block in result.text_blocks
if len(block.text) >= 3
]
return result
pipeline.add_postprocessor(filter_short_text)
性能优化建议
- 启用 ROI: 使用
--roi参数只处理感兴趣区域 - 使用 GPU: 使用
--gpu参数启用 GPU 加速 - 禁用方向分类: 如果文本方向固定,使用
--no-angle-cls - 提高置信度阈值: 使用
--drop-score过滤低质量结果 - 批量处理: 使用目录模式批量处理多张图片
REST API 服务
除了命令行工具,本项目还提供基于 FastAPI 的 REST API 服务。
启动服务
# 开发模式 (支持热重载)
uvicorn api.main:app --reload --host 0.0.0.0 --port 8000
# 生产模式 (多进程)
uvicorn api.main:app --host 0.0.0.0 --port 8000 --workers 4
# 或直接运行
python -m api.main
启动后访问:
- API 文档 (Swagger UI): http://localhost:8000/docs
- API 文档 (ReDoc): http://localhost:8000/redoc
- 健康检查: http://localhost:8000/api/v1/health
API 端点
| 方法 | 端点 | 功能 | 输入格式 |
|---|---|---|---|
GET |
/api/v1/health |
健康检查 | - |
GET |
/api/v1/health/ready |
就绪检查 | - |
POST |
/api/v1/ocr/recognize |
OCR 识别 | multipart/form-data |
POST |
/api/v1/ocr/recognize/base64 |
OCR 识别 | JSON (Base64) |
POST |
/api/v1/ocr/express |
快递单解析 | multipart/form-data |
POST |
/api/v1/ocr/express/base64 |
快递单解析 | JSON (Base64) |
使用示例
方式一: 文件上传 (multipart/form-data)
# OCR 识别
curl -X POST "http://localhost:8000/api/v1/ocr/recognize" \
-F "file=@image.jpg" \
-F "lang=ch" \
-F "drop_score=0.5"
# 快递单解析
curl -X POST "http://localhost:8000/api/v1/ocr/express" \
-F "file=@express.jpg"
# 带 ROI 参数
curl -X POST "http://localhost:8000/api/v1/ocr/recognize" \
-F "file=@image.jpg" \
-F "roi_x=0.1" \
-F "roi_y=0.1" \
-F "roi_width=0.8" \
-F "roi_height=0.8"
# 返回标注图片
curl -X POST "http://localhost:8000/api/v1/ocr/recognize" \
-F "file=@image.jpg" \
-F "return_annotated_image=true"
方式二: Base64 JSON
# OCR 识别
curl -X POST "http://localhost:8000/api/v1/ocr/recognize/base64" \
-H "Content-Type: application/json" \
-d '{
"image_base64": "'"$(base64 -w 0 image.jpg)"'",
"lang": "ch",
"drop_score": 0.5
}'
# 快递单解析
curl -X POST "http://localhost:8000/api/v1/ocr/express/base64" \
-H "Content-Type: application/json" \
-d '{
"image_base64": "'"$(base64 -w 0 express.jpg)"'"
}'
# 带 ROI 参数
curl -X POST "http://localhost:8000/api/v1/ocr/recognize/base64" \
-H "Content-Type: application/json" \
-d '{
"image_base64": "...",
"roi": {"x": 0.1, "y": 0.1, "width": 0.8, "height": 0.8}
}'
Python 调用示例
import requests
import base64
# 方式一: 文件上传
with open("image.jpg", "rb") as f:
response = requests.post(
"http://localhost:8000/api/v1/ocr/recognize",
files={"file": ("image.jpg", f, "image/jpeg")},
data={"lang": "ch", "drop_score": 0.5}
)
result = response.json()
# 方式二: Base64
with open("image.jpg", "rb") as f:
image_base64 = base64.b64encode(f.read()).decode()
response = requests.post(
"http://localhost:8000/api/v1/ocr/recognize/base64",
json={
"image_base64": image_base64,
"lang": "ch"
}
)
result = response.json()
# 处理结果
if result["success"]:
for block in result["data"]["text_blocks"]:
print(f"文本: {block['text']}, 置信度: {block['confidence']}")
API 响应格式
OCR 识别响应
{
"success": true,
"data": {
"processing_time_ms": 45.2,
"text_count": 3,
"average_confidence": 0.92,
"roi_applied": false,
"roi_rect": null,
"text_blocks": [
{
"text": "识别的文本",
"confidence": 0.95,
"bbox": [[100, 50], [200, 50], [200, 80], [100, 80]],
"bbox_with_offset": [[100, 50], [200, 50], [200, 80], [100, 80]],
"center": [150, 65],
"width": 100,
"height": 30
}
],
"annotated_image_base64": null
},
"error": null
}
快递单解析响应
{
"success": true,
"data": {
"processing_time_ms": 52.3,
"express_info": {
"tracking_number": "SF1234567890",
"sender": {
"name": "张三",
"phone": "13800138000",
"address": "北京市朝阳区xxx路"
},
"receiver": {
"name": "李四",
"phone": "13900139000",
"address": "上海市浦东新区xxx路"
},
"courier_company": "顺丰速运",
"confidence": 0.95,
"extra_fields": {},
"raw_text": "..."
},
"merged_text": "顺丰速运\n运单号:SF1234567890\n...",
"annotated_image_base64": null
},
"error": null
}
错误响应
{
"success": false,
"data": null,
"error": {
"code": "InvalidImageError",
"message": "无效的图片文件"
}
}
API 请求参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
file |
File | - | 图片文件 (multipart 模式) |
image_base64 |
string | - | Base64 编码图片 (JSON 模式) |
lang |
string | "ch" | 识别语言 |
use_gpu |
bool | false | 是否使用 GPU |
drop_score |
float | 0.5 | 置信度阈值 (0.0~1.0) |
roi_x |
float | - | ROI 左上角 X (0.0~1.0) |
roi_y |
float | - | ROI 左上角 Y (0.0~1.0) |
roi_width |
float | - | ROI 宽度 (0.0~1.0) |
roi_height |
float | - | ROI 高度 (0.0~1.0) |
return_annotated_image |
bool | false | 是否返回标注图片 |
安全限制
- 最大文件大小: 10MB
- 支持的格式: jpg, jpeg, png, bmp, webp, tiff
- 文件验证: 扩展名 + MIME 类型 + 文件魔数
运行测试
# 运行所有 API 测试
pytest tests/ -v
# 运行特定测试
pytest tests/test_api_health.py -v
pytest tests/test_api_ocr.py -v
常见问题
Q: Windows 中文用户名导致模型加载失败?
A: PaddlePaddle 的 C++ 推理引擎无法正确处理包含中文字符的路径。请运行以下命令将模型下载到项目目录:
python download_models.py
程序会自动检测并使用 models/ 目录中的模型。
Q: 中文无法正常显示?
A: OpenCV 默认字体不支持中文。可以在 VisualizeConfig 中配置 font_path 指向系统中文字体文件:
config.visualize.font_path = "C:/Windows/Fonts/simhei.ttf" # Windows
config.visualize.font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc" # Linux
Q: OCR 速度慢?
A: 参考上方「性能优化建议」部分。
Q: 支持哪些图片格式?
A: 支持以下格式:.jpg, .jpeg, .png, .bmp, .tiff, .tif, .webp
贡献指南
欢迎提交 Issue 和 Pull Request。
开发流程
- Fork 本仓库
- 创建功能分支:
git checkout -b feature/your-feature - 提交更改:
git commit -m "Add your feature" - 推送分支:
git push origin feature/your-feature - 创建 Pull Request
代码规范
- 遵循 PEP 8 代码风格
- 所有公共类和函数需要添加文档字符串
- 新功能需要添加相应的类型注解
- 提交前确保代码可正常运行
目录结构规范
input/: 仅包含图片加载相关代码ocr/: 仅包含 OCR 处理相关代码visualize/: 仅包含可视化相关代码utils/: 通用工具和配置
许可证
MIT License