《 Python 计算机视觉编程》学习笔记(一)

练习代码仓库 https://github.com/wolfg1969/Programming-Computer-Vision-with-Python-practice

第一章 基本图像处理

第二节 Matplotlib

直方图

以下代码生成的直方图显示了示例图像中像素值(灰度值,因为转成了灰度图)的分布情况。

from PIL import Image
from pylab import *

im = array(Image.open('images/empire.jpg').convert('L'))
print im.shape, im.dtype  # (800, 569) uint8

figure()
gray()
axis('equal')
axis('off')
imshow(im)

figure()
h = hist(im.flatten(), 128)
print len(h[1]), h[1]

show()

图像尺寸 800x569, flatten 后一共有 455200 个像素。直方图的横轴代表灰度值的范围(0 ~ 255),hist 函数的第二个 bins=128 将 横轴分成了 bins + 1 = 129 个小区间,纵轴代表灰度值在每个区间灰度值范围内的像素个数,即像素灰度值的分布情况。

观察直方图,横轴值为 0 的附近几乎没有 bin 的存在,说明图像里几乎没有纯黑的地方。而最右侧 255 附近的 bins 说明白色像素的存在。

经过了灰度变换 255 -im 后的直方图,正好是镜像的。

[翻译] Pipenv – 官方推荐的 Python 包管理工具

更新 [20180109]
pipenv 可以生成 requirements.txt

$ pipenv lock --requirments > requirements.txt

原文地址: https://www.ostechnix.com/pipenv-officially-recommended-python-packaging-tool/

译文

上次我们发布了一篇[如何用 Pip 进行 Python 包管理](如何用 Pip 进行 Python 包管理 "如何用 Pip 进行 Python 包管理")的文章。在那篇文章里我们讨论了如何安装 Pip,如何用 Pip 来安装、升级和删除 Python 包。我们也讨论了虚拟环境的重要性以及用 venv 和 virtualenv 工具创建虚拟环境的方法。但用 venv 和 virtualenv 管理多个虚拟环境是项乏味而且繁琐的任务,幸好我们有另外一个名为 Pipenv 的工具,它是 Python.org 官网最新推荐的包管理工具,它能够帮助我们不必建立虚拟环境的情况下非常容易地安装和管理依赖。Pipenv 可以为你的项目自动地创建和管理一个虚拟环境,当你安装 / 删除包文件时它可以添加 / 删除包名称到 Pipfile 文件。

为什么使用 Pipenv ?

Pipenv 解决下列实际问题:
- 你不再需要手动创建虚拟环境,Pipenv 为你自动创建。简单地说就是 pipenv 和 virtualenv 一起工作。
- 管理 requirements.txt 文件会导致一些问题,所以 Pipenv 用 Pipfile 和 Pipfile.lock 替代 requirements.txt,更适合于一般的使用场景。
- 安全。广泛地使用 Hash 校验,能够自动曝露安全漏洞。
- 随时查看图形化的依赖关系。
- 通过加载 .env 文件简化开发流程。

好了,让我们开始学习 pipenv 吧。

有很多方式来安装 pipenv,我们看看推荐的两种:

使用 pip 安装

官方推荐的安装方式是使用 pip。确认你已经装好了 Python 和 pip,如果没有,查看文章开头的那个链接。
如果 pip 已经安装好了,用下面的命令安装 pipenv

$ pip install --user pipenv

这个命令在用户级别(非系统全局)下安装 pipenv。如果安装后 shell 提示找不到 pipenv 命令,你需要添加当前 Python 用户主目录的 bin 目录到 PATH 环境变量。如果你不知道 Python 用户主目录在哪里,用下面的命令来查看:

$ python -m site --user-base

你会看到类似下面的输出

/home/sk/.local

这就是我的 Python 主目录,那 bin 目录就是 /home/sk/.local/bin,清楚了吧?好,运行下面的命令:

$ pipenv --update

任何时候你都可以用下面的命令升级 pipenv

$ pip install --user --upgrade pipenv
$ pipenv --update

使用 pipsi 安装

Pipsi 是一种帮助你在隔离的虚拟环境里安装 Python 脚本的工具。安装 pipsi,执行

$ curl https://raw.githubusercontent.com/mitsuhiko/pipsi/master/get-pipsi.py | python

按照安装提示,你必须修改 PATH 环境变量。
一旦 pipsi 安装完成,用下面的命令来安装 pipenv

$ pipsi install pew
$ pipsi install pipenv

最后,用下面的命令来验证安装

$ pipenv --update

要升级 pipenv,只需执行

$ pipsi upgrade pipenv

使用 pipenv 管理 Python 安装包

在 shell 中执行 pipenv 命令可查看可用命令和通用选项

$ pipenv

示例输出如下
https://www.ostechnix.com/wp-content/uploads/2017/12/pipenv-1.png

安装软件包

新建一个项目目录或切换到已有项目目录

$ mkdir myproject
$ cd myproject

为你的项目安装依赖包

$ pipenv install requests

ls 命令查看当前项目目录,你会发现有两个文件:Pipfile 和 Pipfile.lock 。Pipfile 里有最新安装的包文件的信息,如名称、版本等。用来 在重新安装项目依赖或与他人共享项目时,你可以用 Pipfile 来跟踪项目依赖。

$ cat Pipfile

Pipfile.lock 则包含你的系统信息,所有已安装包的依赖包及其版本信息,以及所有安装包及其依赖包的 Hash 校验信息。

$ cat Pipfile.lock

现在安装另一个包,再次查看这两个文件的内容。你会发现 Pipfile 现在包含两个安装包了,Pipfile.lock 也包含了所有已安装包的依赖包及其版本信息,以及所有安装包及其依赖包的 Hash 校验信息。每次你安装新的依赖包,这两个文件都会自动更新。

你注意到了吗?我并没有创建一个虚拟环境。Pipenv 自动为这个项目创建了一个虚拟环境,想知道它在哪里吗?用下面的命令来查看虚拟环境的位置

$ pipenv --venv
/home/sk/.local/share/virtualenvs/myproject-x7-2XYPN

查看项目根目录详情,用

$ pipenv --where
/home/sk/myproject

由上面命令的输出可知,/home/sk/myproject 是我的项目根目录,/home/sk/.local/share/virtualenvs/myproject-x7-2XYPN 是项目的虚拟环境目录。

你可以用 ls 命令查看虚拟环境目录下的内容。

ls /home/sk/.local/share/virtualenvs/myproject-x7-2XYPN

更新软件包

$ pipenv update

这个命令会删除所有软件包然后重新安装最新的版本。

检查软件包的完整性

你是否担心已安装的软件包有没有安全漏洞?没关系,pipenv 可以帮你检查,运行下面的命令

$ pipenv check
Checking PEP 508 requirements…
Passed!
Checking installed package safety…
All good!

上面的命令根据 Pipfile 里的 PEP 508 标记检查安全漏洞。

查看依赖树

我们执行 pipenv graph 看看会发生什么

$ pipenv graph

可以看到该命令显示了依赖树。

删除软件包

用下面的命令删除软件包

$ pipenv uninstall requests
Un-installing speedtest-cli…
Uninstalling speedtest-cli-1.0.7:
 Successfully uninstalled speedtest-cli-1.0.7

Removing speedtest-cli from Pipfile…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Updated Pipfile.lock (c23e27)!

删除全部软件包

$ pipenv uninstall --all
Un-installing all packages from virtualenv…
Found 1 installed package(s), purging…

Environment now purged and fresh!

查看详细用法

$ pipenv -h

$ pipenv --man

我使用 pipenv 后,确实感觉比 pip 更方便。既然它由 Python.org 官方推荐,你安装 Python 软件包时可以弃用 venv 和 virtualenv 了。

今天的教程就到这里了,更多精彩文章,敬请期待。

资源:
* Pipenv GitHub 页面


译后实践

我在 Mac 下使用时遇到些问题:

  1. 需要指定 LOCALE 环境变量 export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8
  2. 需要升级 pip 到最新版本,最好安装 pipenv 时指定 --upgrade 参数 pip install --user --upgrade pipenv
  3. 安装某些包时会报错,产生不了 Pipfile.lock 文件。 https://github.com/pypa/pipenv/issues/515
  4. 这篇教程里没说怎么使用自动创建的虚拟环境,官方文档里有:
$ pipenv run python main.py

Django 日志过滤器实战

Django 的日志配置采用标准的 Python 日志配置方式,默认采用 dictConfig 的格式。Python 文档里对日志过滤器 Filter 的解释是一种控制日志记录 Record 是否输出的机制。就像物理世界中的筛子,只有特定大小或形状的物体才能通过,Filter 可以按照特定的需求“筛选”出我们需要的日志记录。

我们来看看 Django 的一个内置过滤器 RequireDebugFalse 。这个过滤器一般是用于发送错误邮件日志,只有设置了 DEBUG = False时才把错误日志发送给管理员(生产环境,避免邮件轰炸) :

'filters': {
    'require_debug_false': {
        '()': 'django.utils.log.RequireDebugFalse',
    }
},
'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'filters': ['require_debug_false'],
        'class': 'django.utils.log.AdminEmailHandler'
    }
},

可以看出,过滤器的配置方法是在 filters 的 dict 里指定一个别名 'require_debug_false'作为 key,value 又是一个 dict。这个 dict 用来描述过滤器如何被实例化。其中,'()'作为特殊的 key 用来指定过滤器是哪个类的实例(见 logging.config.DictConfigurator)。其它的 key 用来指定实例化过滤器是传给 __init__ 方法的参数。

再来看 RequireDebugFalse 过滤器的实现:

class RequireDebugFalse(logging.Filter):
    def filter(self, record):
        return not settings.DEBUG

很简单,就是定义 filter 方法,日志记录 record 作为唯一参数,返回值为 0False 表示日志记录将被抛弃,1True则表示记录别放过。

过滤器不仅可以用来滤除不需要的日志记录,还可以修改日志记录。例如,我们可以给日志记录添加自定义的属性,用来区分日志来自于哪个应用和运行环境。

class StaticFieldFilter(logging.Filter):

    def __init__(self, fields):
        self.static_fields = fields

    def filter(self, record):
        for k, v in self.static_fields.items():
            setattr(record, k, v)
        return True

这个过滤器把初始化时得到的属性键值对设为每条日志记录的属性,用法如下:

'filters': {
    'require_debug_false': {
        '()': 'django.utils.log.RequireDebugFalse',
    },
    'static_fields': {
        '()': 'example.utils.log.StaticFieldFilter',
        'fields': {
            'project': os.environ['PROJECT_NAME'],
            'environment': os.environ['ENV_NAME'],
        },
    }
},
'handlers': {
    'mail_admins': {
        'level': 'ERROR',
        'filters': ['require_debug_false', 'static_fields'],
        'class': 'django.utils.log.AdminEmailHandler'
    }
},

注意 fields 是如何配置传入的。

更进一步,我们还可以让错误日志邮件的标题也包含自定义的属性:

def filter(self, record):
    for k, v in self.static_fields.items():
        setattr(record, k, v)
        record.levelname = '[%s] %s' % (v, record.levelname)
    return True

再来看一个例子,Django 可以把 request 请求信息设为日志记录的属性,但有些日志处理器(比如 Graylog2 的 GELFHandler )不能序列化 Request 对象,需要做些处理:

from django.http import build_request_repr
class RequestFilter(logging.Filter):

    def filter(self, record):
        if hasattr(record, 'request'):
            record.request_repr = build_request_repr(record.request)
            del record.request
        return True

这里我们把 request 对象用 Django 内置的函数 build_request_repr 转为字符串格式,然后删掉日志记录的 request 属性。

配置方法:

'filters': {
    'require_debug_false': {
        '()': 'django.utils.log.RequireDebugFalse',
    },
    'static_fields': {
        '()': 'example.utils.log.StaticFieldFilter',
        'fields': {
            'project': os.environ['PROJECT_NAME'],
            'environment': os.environ['ENV_NAME'],
        },
    },
    'exclude_django_request': {
        '()': 'example.utils.log.RequestFilter',
    }
},
'handlers': {
    'gelf': {
        'level': 'DEBUG',
        'class': 'graypy.GELFHandler',
        'host': os.environ['GRAYLOG2_HOST'],
        'port': int(os.environ['GRAYLOG2_PORT']),
        'filters': [
            'require_debug_false', 
            'static_fields', 
            'exclude_django_request', 
        ],
    },
},

参考:

定制 Django REST Framework 的异常处理

今天借助修复其它 Bug 的机会,对项目中 REST API 的异常处理机制进行了优化。

项目的 REST API 是基于 Django REST Framework 实现的,之前已经按照其文档的说明,对 EXCEPTION_HANDLER 进行了定制,用统一的 JSON 数据格式来输出异常错误信息,包括 500 错误。没有定制时,对于系统所有未特殊处理的异常,Django REST Framework 的处理方法是继续向上抛出,而不是像其它情况那样处理成自己封装的 Exception Response。这样的话,客户端得到的响应是 Django 框架的 500 错误页面,不便于客户端的统一处理。

自定义的异常处理函数如下:

from rest_framework.views import exception_handler

def my_api_exception_handler(exc):
    # Call REST framework's default exception handler first to get the standard error response.
    response = exception_handler(exc)

    # handle 500 errors
    if response is None:
        if isinstance(exc, APIException):
            detail = exc.detail
        else:
            detail = exc.message if settings.DEBUG else 'Internal Server Error'
        response = Response({'detail': detail}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    return response

这里先调用框架默认的异常处理函数(Line 5),当这个函数返回的 response 对象为 None 时(Line 8),说明异常没有被它处理。这时我们就自己生成一个 status 状态码为 500 的 Response 对象(Line 13),而不是返回 None 。

自定义的异常处理函数必须在 settings 里启用,如下

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'my_project.my_app.utils. my_api_exception_handler'
}

今天的改进是对异常处理函数中日志的改进。原来的日志记录在发生 500 的错误,就记录一条 Error 日志:

logger.error("api view 500 error: %s >>>\n %s", str(exc), traceback.format_exc())

在生产环境下,这条日志会产生一封通知邮件,但效果并不好,异常堆栈变成了一长串字符被塞进了邮件的主题里,也没有当前 Request 的上下文信息,很难从中分析定位错误。

改进后的日志语句被挪到了 APIView 的 handle_exception 方法里:

def handle_exception(self, exc):
    resp = super(GenericAPIView, self).handle_exception(exc)
    if resp.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
        logger.error("API View 500 error: %s", self.request.path,
                     exc_info=sys.exc_info(),
                     extra={
                         'status_code': 500,
                         'request': self.request
                     })
    return resp

可以看到,异常信息和当前请求信息都被记录了( Line 5 和 Line 8)。exc_info 和 extra 参数的说明见 Python 标准库文档,这里也参考了 Django 中 AdminEmailHandler 类的实现。

以上。

一个Python脚本,让OpenVPN使用postfix邮箱帐号进行身份认证

这几天配置OpenVPN,使用了用户名密码的身份认证方式,借助已有的postfix邮箱帐号,省去了再为每个人设置用户名密码的麻烦。

原理很简单,OpenVPN服务器配置里有这样一句:

auth-user-pass-verify /etc/openvpn/auth-postfix-mailbox.py via-env

就是说要用/etc/openvpn/auth-postfix-mailbox.py这个脚本来验证用户名和密码。用户名和密码如何传递给它呢?via-env,环境变量。

脚本如下:

#!/usr/bin/env python

import os
import sys
from MySQLdb import *
import md5crypt

def auth(username, password):
conn = connect (host = 'localhost',
user = 'dbuser',
passwd = 'dbpasswd',
db = 'postfix')
cursor = conn.cursor()
cursor.execute("""
select password from mailbox
where username=%s
and active=1
""", (username))
row = cursor.fetchone()
if row == None:
return 1
crypt = md5crypt.md5crypt(password, row[0])
cursor.execute("""
select * from mailbox
where username=%s
and password=%s
and active=1
""", (username,crypt))
row = cursor.fetchone()
cursor.close()
conn.close()
if row == None:
return 1
return 0

def main():
status = 0
try:
username = os.environ['username']
password = os.environ['password']
status = auth(username, password)
except:
sys.exit(1)

sys.exit(status)

if __name__ == "__main__":
main()

由于postfix使用md5认证,所以需要用md5crypt这个模块,从这里可以下载到。