原文:Pro Processing for Images and Computer Vision with OpenCV
一、Processing 和 OpenCV 入门
本章向您介绍 Processing 和 OpenCV 以及如何安装它们。在本章结束时,你将对通过遵循本书中的示例可以构建的应用类型有一个大致的了解。您还将能够编写一个“Hello World”程序,在处理编程环境中显示 OpenCV 的版本信息。
处理
来自麻省理工学院媒体实验室前美学+计算小组的本·弗莱和凯西·雷阿斯在 2001 年发起了处理项目( http://processing.org
),为艺术家和设计师创造一个编程环境,以在电子艺术的背景下学习计算机编程的基础。基于 Java 编程语言,Processing 被建模为电子速写本,供艺术家和设计师产生他们的创造性想法。处理是通过集成开发环境(IDE)实现的。用户可以直接在环境中编码并执行代码,以实时查看可视化结果。Processing 配备了一套全面的框架和库,以提供对用于创建 2D 和 3D 图形以及构建动画的功能的简单访问。选择 Java 作为编程语言是为了满足跨平台的兼容性。目前,它支持 macOS、Windows 和主要的 Linux 操作系统。最近,处理已经发展到包括其他编程语言,如 JavaScript 和 Python。
除了处理语言的核心功能和大量的原生 Java 库之外,处理还支持来自社区的用户贡献的库( https://processing.org/reference/libraries/
)。许多库的建立是为了隐藏实现复杂软件的技术细节,如物理引擎和机器学习算法,或者支持附加的硬件设备,如 Kinect 摄像头。例如,我开发了一个名为Kinect4WinSDK
的包装库,通过官方的 Kinect for Windows 软件开发工具包(SDK)来支持 Kinect 版本 1 相机。
在计算机图形领域,处理能够产生矢量图形和光栅图形。在创造性应用中,算法艺术和生成艺术(图 1-1 )经常会用到矢量图形。在本书中,重点是图像处理和计算机视觉。在这种情况下,光栅图形将是生成图像的主要方法。
图 1-1。
Algorithmic art example
开放计算机视觉
开源计算机视觉库(OpenCV, http://opencv.org/
)始于 1999 年左右,是英特尔的一项研究计划。现在,它是最受欢迎的计算机视觉和机器学习开源软件库。一开始是一套图像处理和计算机视觉的 C 库函数。现在,它有 C++、Python、Java 和 MATLAB 绑定,可以在 macOS、Windows、Linux、Android 和 iOS 上工作,并有 CUDA 和 OpenCL 的加速支持。OpenCV 库附带了一组模块。每个模块在图像处理、计算机视觉和机器学习的保护伞下处理一组特定的应用。以下是常用模块:
core
:核心 OpenCV 数据结构和功能imgproc
:图像处理imgcodecs
:图像文件读写videoio
:媒体输入/输出程序highgui
:高级图形用户界面video
:视频分析calib3d
:摄像机标定和三维重建features2d
:处理 2D 特征描述和匹配objdetect
:人脸等目标检测ml
:机器学习flann
:高维空间中的聚类和搜索photo
:计算摄影stitching
:将图像拼接在一起shape
:形状匹配superres
:超分辨率增强videostab
:视频稳定viz
: 3D 可视化
OpenCV 包括几个额外的模块,提供额外的功能,如文本识别、表面匹配和 3D 深度处理。本书还涵盖了执行光流分析的模块optflow
。
加工设备
本节说明下载和安装加工编程环境的步骤。在撰写本文时,最新的处理版本是 3.2.3。出于兼容性原因,建议您使用版本 3 而不是以前版本的处理。每一个分发的处理还包括与 Java 运行时代码。这三个平台的安装过程简单而相似。
安装处理
从 https://processing.org/download/
下载加工代码。在本书中,我将使用 64 位版本。如果想看看处理源代码,可以从 GitHub 发行版( https://github.com/processing/processing
)下载。以下是适用于 macOS、Windows 和 Linux 平台的三个文件:
processing-3.2.3-macosx.zip
processing-3.2.3-windows64.zip
processing-3.2.3-linux64.tgz
处理不假设任何特定的位置来安装软件。对于 macOS,您可以下载该文件并将其展开到名为 Processing 的 macOS 程序中。将程序复制到Applications
文件夹,类似于为 macOS 安装的其他应用。对于 Windows 和 Linux,压缩文件将被展开到一个名为processing-3.2.3
的文件夹中。您可以下载压缩文件并将其展开到您想要维护处理软件的任何文件夹中。在本书中,我们将文件夹processing-3.2.3
展开成用户的Documents
文件夹。图 1-2 显示了文件夹的内容。要运行处理,只需双击处理图标。
图 1-2。
Processing folder for Windows
图 1-3 显示了处理 IDE 的默认屏幕布局。窗口中的代码将是您要测试的第一个处理程序。
图 1-3。
Processing IDE screen
void setup() {
size(800, 600);
}
void draw() {
background(100, 100, 100);
}
开始加工后,它会自动在您的个人Documents
文件夹中创建一个文件夹,保存所有的加工程序。对于 macOS,它的名字是/Users/bryan/Documents/Processing
。在 Windows 中,文件夹名为C:\Users\chung_000
\Documents\Processing
。在 Linux 中,它是/home/bryan/sketchbook
。(在示例中,用户名是bryan
或chung_000
。)图 1-4 显示了文件夹内容的示例视图。
图 1-4。
Processing sketchbook folder contents
每个加工程序都保存在Processing
文件夹中自己的文件夹中。除了每个程序之外,该文件夹还包含其他子文件夹,例如用于从处理发行版下载外部库的libraries
文件夹和用于在处理中实现其他语言(如 Python 和 JavaScript)的modes
文件夹。
运行处理
在处理 IDE 的左上角,有两个按钮,播放和停止。单击播放按钮将开始程序的编译和执行。图 1-5 显示了你的第一个程序创建的空白屏幕。此时点击停止按钮将停止执行并关闭窗口。
图 1-5。
First Processing program
您需要为本书中的练习安装一个额外的库。它就是构建在开源多媒体框架 GStreamer ( https://gstreamer.freedesktop.org/
)之上的video
库。处理将使用它来播放数字视频,如 MP4 文件,并用网络摄像头捕捉直播视频。要安装库(图 1-6 ),从主菜单中选择草图➤导入库➤添加库。从贡献管理器窗口中,选择视频库并单击安装按钮。该库将被下载到您的处理文件夹的libraries
子文件夹中。
图 1-6。
Installing the video library
在第二章中,您将使用这个库来加载外部数字视频和从网络摄像头捕捉实时视频流。安装处理编程环境后,您可以继续在您的系统上安装 OpenCV。
OpenCV 安装
OpenCV 的安装有点复杂,因为您将从源代码构建 OpenCV 库。您将要构建的库不同于现有的由 Greg Borenstein ( https://github.com/atduskgreg/opencv-processing
)编写的 OpenCV for Processing 库。在继续这个安装过程之前,您最好删除现有的 OpenCV for Processing 库。OpenCV 发行版包含了所有的核心函数。要使用其他函数进行运动分析,您还需要构建在贡献的库中维护的额外模块。您将从 GitHub 库下载这两个版本。最初的 OpenCV 源代码在 https://github.com/opencv/opencv
,额外模块的源代码在 https://github.com/opencv/opencv_contrib
。请注意,OpenCV 存储库中的主分支只包含 2.4 版。要使用 3.1 版,您需要选择 3.1.0 标签,如图 1-7 所示。选择正确的版本标签后,点击“克隆或下载”按钮,然后点击下载 ZIP 按钮,即可下载 OpenCV 源代码,如图 1-8 所示。
图 1-8。
Downloading the OpenCV source
图 1-7。
Selecting the tag 3.1.0
下载并解压缩 OpenCV 源代码后,该过程将创建opencv-3.1.0
文件夹。对于opencv_contrib
源,按照相同的步骤选择 3.1.0 标签,下载 zip 文件,并解压到您的opencv-3.1.0
文件夹中。图 1-9 显示了opencv-3.1.0
文件夹的内容。
图 1-9。
Contents of the opencv-3.1.0 folder
成功下载 OpenCV 3.1.0 源代码和额外模块库后,可以在opencv-3.1.0
文件夹中创建一个名为build
的子文件夹。所有 OpenCV 库都将被构建到这个文件夹中。在开始构建过程之前,还有一个步骤需要处理。要构建包含额外模块optflow
的 Java 库,您将使用它进行运动分析,您必须编辑它的CMakeLists.txt
文件。从opencv_contrib-3.1.0
文件夹,进入modules
文件夹,然后进入optflow
文件夹。使用任何文本编辑器修改optflow
文件夹中的CMakeLists.txt
文件。在第二行,原始代码如下:
ocv_define_module(optflow opencv_core opencv_imgproc opencv_video opencv_highgui opencv_ximgproc WRAP python)
在两个关键字WRAP
和python
之间插入记号java
。新生产线将如下所示:
ocv_define_module(optflow opencv_core opencv_imgproc opencv_video opencv_highgui opencv_ximgproc WRAP java python)
新文件将使构建过程能够将optflow
模块包含到已构建的 Java 库中。以下部分根据您使用的平台描述不同的构建过程。既然您要构建 OpenCV Java 库,那么您也应该从 Oracle 网站 www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
下载并安装 Java 开发工具包(JDK)。要检查您是否已经安装了 JDK,您可以进入终端或命令行会话并键入以下内容:
javac -version
苹果
你将使用自制软件安装必要的相关软件。安装过程将从命令行终端会话中执行。终端工具在/Applications/Utilities
文件夹中。家酿安装说明在官方网站 http://brew.sh/
上。安装了家酿软件包管理器后,您可以开始安装 OpenCV 构建过程所需的软件。在终端会话中,输入以下内容:
brew install cmake
brew install ant
这两个命令安装软件cmake
和ant
。cmake
工具( http://cmake.org
)是一个构建、测试和打包软件的开源工具。Apache ant
工具( http://ant.apache.org
)是构建 Java 应用的实用工具。下一步是使用ccmake
交互工具开始配置过程。首先导航到原 OpenCV 文件夹opencv-3.1.0
的build
文件夹,发出ccmake
命令,如图 1-10 所示。
图 1-10。
ccmake command to configure the build process
ccmake ..
在ccmake
面板中,输入c
来配置安装过程。选择合适的选项,如图 1-11 所示。请注意,您应该首先关闭第一页上的大多数选项,包括BUILD_SHARED_LIBS
选项。接下来打开BUILD_opencv_java
选项,如图 1-12 和图 1-13 所示。
图 1-13。
Third page of the build options
图 1-12。
Second page of the build options
图 1-11。
BUILD_SHARED_LIBS and other options
下一个重要的选项是OPENCV_EXTRA_MODULES_PATH
,它应该被设置为 OpenCV 额外模块的路径名。具体来说,应该是你原来的opencv-3.1.0
文件夹里面的文件夹opencv_contrib-3.1.0/modules
,如图 1-14 所示。
图 1-14。
OPENCV_EXTRA_MODULES_PATH option
其余的构建选项如下图所示:图 1-15 ,图 1-16 ,图 1-17 ,图 1-18 。
图 1-18。
Last page of OpenCV build options
图 1-17。
OpenCV build options, continued
图 1-16。
OpenCV build options, continued
图 1-15。
OpenCV build options
填写完第一轮构建选项后,再次键入c
来配置额外的模块。首先关闭BUILD_FAT_JAVA_LIB
选项,如图 1-19 所示。
图 1-19。
OpenCV extra modules build options
为了继续本书后面的光流示例,你还应该打开BUILD_opencv_optflow
、BUILD_opencv_ximgproc
和BUILD_opencv_java
的选项,如图 1-20 和图 1-21 所示。
图 1-21。
Turning on the option for ximgproc
图 1-20。
Turning on options for Java and optflow
完成剩余的额外模块选项,如图 1-22 所示。
图 1-22。
Extra module options
设置好所有选项后,再次输入c
完成最后一个配置任务。键入选项g
生成配置文件(图 1-23 )。ccmake
程序将退出并带你回到终端窗口。
图 1-23。
Generating the configuration file
输入以下命令开始构建过程:
make –j4
当构建过程成功完成时,导航到build
文件夹中的bin
文件夹。找到opencv-310.jar
文件。然后导航到build
文件夹中的lib
文件夹。找到libopencv_java310.so
文件。改名为libopencv_java310.dylib
。将这两个文件复制到单独的文件夹中。您将准备 Windows 和 Linux 版本,并将它们复制到同一个文件夹中,以创建多平台库。作者已经测试了在 macOS 10.11,El Capitan 中构建 OpenCV 3.1。对于使用新 macOS 10.12 Sierra 的读者来说,OpenCV 3.1 的构建将由于 QTKit 的移除而失败。这种情况下,最好是 OpenCV 3.2 配合 macOS 10.12 一起使用。请参考 www。神奇的爱情。com/blog/2017/03/02/OpenCV-3-2-Java-build/
用 OpenCV 3.2 正确生成optflow
模块。
Windows 操作系统
在 Windows 系统上,您使用图形版本的cmake
来配置安装过程。我已经在 Windows 8.1 和 Windows 10 中测试了安装。为 OpenCV 构建过程下载并安装以下软件包:
- 微软 Visual Studio 社区 2015 在
https://www.visualstudio.com/downloads/
- CMake at
https://cmake.org/download/
- 阿帕奇蚂蚁
http://ant.apache.org/bindownload.cgi
www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
甲骨文 JDK 8- 蟒于
https://www.python.org/downloads/
成功安装软件包依赖项后,运行 CMake ( cmake-gui
)程序开始配置过程。填写 OpenCV 发行版的源文件夹名和构建文件夹名,如图 1-24 所示。请记住,您需要在 OpenCV 分发文件夹中创建构建文件夹。
图 1-24。
Folder names of OpenCV distribution in the CMake window
单击“配置”按钮开始配置。对于第一个生成器面板,从下拉菜单中选择 Visual Studio 14 2015 Win64,选择“使用默认本机编译器”单选按钮,如图 1-25 所示。单击“完成”按钮继续。
图 1-25。
Choosing the default compiler
按照图 1-26 到图 1-33 进入构建选项。确保首先关闭BUILD_SHARED_LIBS
选项,并为ANT_EXECUTABLE
选项输入ant.bat
的路径名。
图 1-33。
Eighth page of OpenCV build options
图 1-32。
Seventh page of OpenCV build options
图 1-31。
Sixth page of OpenCV build options
图 1-30。
Fifth page of OpenCV build options
图 1-29。
Fourth page of OpenCV build options
图 1-28。
Third page of OpenCV build options
图 1-27。
Second page of OpenCV build options
图 1-26。
Turning off the BUILD_SHARED_LIBS option
在下一个屏幕中(图 1-34 ,输入OPENCV_EXTRA_MODULES_PATH
选项的opencv_contrib
额外模块的路径名。图 1-35 至 1-37 显示了其余的设置。
图 1-37。
The last page of OpenCV build options
图 1-36。
Eleventh page of OpenCV build options
图 1-35。
Tenth page of OpenCV build options
图 1-34。
OPENCV_EXTRA_MODULES_PATH option
单击“配置”按钮创建配置详细信息。在红色区域,确保启用选项BUILD_opencv_java
、BUILD_opencv_optflow
和BUILD_opencv_ximgproc
(图 1-38 和 1-39 )。将剩余的额外模块保留为空选项。
图 1-39。
BUILD_opencv_ximgproc option
图 1-38。
BUILD_opencv_java and BUILD_opencv_optflow options
再次单击“配置”按钮以完成配置过程。设置完所有配置选项后,单击“生成”按钮创建 Visual Studio 解决方案文件。完成后,退出 CMake 程序。在build
文件夹中,启动 Visual Studio 解决方案OpenCV.sln
(图 1-40 )。
图 1-40。
OpenCV Visual Studio solution file
在 Visual Studio 程序中,从解决方案配置菜单中选择发布(图 1-41);从解决方案平台菜单中选择 x64。
图 1-41。
Visual Studio project options
从解决方案资源管理器中,展开 CMakeTargets 然后右键单击 ALL_BUILD 目标并选择 BUILD(图 1-42 )。
图 1-42。
Choosing the OpenCV build target
成功构建解决方案后,退出 Visual Studio 并导航到build
文件夹。在bin
文件夹中,你会看到opencv-310.jar
文件,如图 1-43 所示。
图 1-43。
OpenCV Windows build file
双击打开bin
文件夹内的Release
文件夹;OpenCV Windows 原生库opencv_java310.dll
将驻留在那里,如图 1-44 所示。
图 1-44。
OpenCV Windows native library file
Linux 操作系统
书中测试的 Linux 发行版是 Ubuntu 16.04。对于 Linux 系统,可以使用apt-get
命令安装相关软件包。OpenCV 3.1.0 文档中有一页描述了详细的安装过程。你可以在 http://docs.opencv.org/3.1.0/d7/d9f/tutorial_linux_install.html
找到参考。在安装 OpenCV 之前,您需要设置适当的 Java 环境。在本书中,您将在 Linux 安装中使用 Oracle JDK 8。要获得正确的版本,请在终端会话中输入以下内容:
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer
在 Java 安装之后,您需要为 OpenCV 构建过程设置适当的环境变量JAVA_HOME
。使用文本编辑器编辑环境文件。
sudo gedit /etc/environment
在文件末尾,插入以下一行:
JAVA_HOME="/usr/lib/jvm/java-8-oracle"
保存并退出环境文件,然后用以下内容重新加载它:
source /etc/environment
使用echo
命令验证环境变量是否设置正确。它应该返回 Java 安装的正确位置。
echo $JAVA_HOME
成功安装 JDK 后,您可以使用apt-get
继续安装 OpenCV 的相关软件包。
sudo apt-get install ant build-essential cmake git libgtk-2.0-dev pkg-config libavcodec-dev, libavformat-dev libswscale-dev python-dev execstack
为了简化构建过程,可以为cmake
安装图形用户界面,这样就可以使用ccmake
来构建 OpenCV。
sudo apt-get install cmake-curses-gui
首先,导航到 OpenCV 发行版文件夹opencv-3.1.0
中的build
文件夹。使用ccmake
命令启动配置过程。
ccmake ..
在菜单屏幕上,键入c
开始自动配置过程。从图 1-45 开始,按照以下截图所示填写选项。
图 1-45。
BUILD_SHARED_LIBS option
请务必打开BUILD_opencv_java
选项,如图 1-46 所示。
图 1-46。
BUILD_opencv_java option
然后在OPENCV_EXTRA_MODULES_PATH
选项中输入 OpenCV 额外模块的路径信息(图 1-47 )。
图 1-47。
OPENCV_EXTRA_MODULES_PATH option
通过打开WITH_GTK
选项(图 1-48 )包括 GTK 对配置的支持。
图 1-48。
WITH_GTK option
继续其余的配置选项,如图 1-49 和图 1-50 所示。
图 1-50。
WITH_V4L option
图 1-49。
WITH_LIBV4L option
在输入最后一个构建选项后,键入c
运行带有额外模块的配置。为BUILD_FAT_JAVA_LIB
选项选择OFF
(图 1-51 )。进入BUILD_opencv_optflow
和BUILD_opencv_ximgproc
的ON
选项。
图 1-51。
BUILD_FAT_JAVA_LIB and BUILD_opencv_optflow options
将其余的额外模块设置为OFF
(图 1-52 )。
图 1-52。
BUILD_opencv_ximgproc option
再次键入c
运行最终配置。键入g
生成所有构建文件并退出(图 1-53 )。
图 1-53。
Final configuration
通过输入以下命令开始构建过程:
make -j4
OpenCV 构建成功后,导航到当前build
文件夹下的bin
文件夹(图 1-54 )。现货opencv-3.1.0.jar
文件。
图 1-54。
Location of opencv-310.jar
然后将目录更改为build
文件夹中的lib
文件夹。找到libopencv_java310.so
文件。在终端会话中,对 OpenCV 库文件运行execstack
,清除可执行堆栈标志(图 1-55 )。
图 1-55。
Location of libopencv_java310.so
execstack -c ./libopencv_java310.so
试运转
安装 Processing 并构建 OpenCV 的 Java 版本后,您可以开始编写两个程序来验证安装并尝试 OpenCV 库的功能。第一个程序是一个“Hello World”练习,用来显示 OpenCV 库的版本信息。第二个程序将定义一个 OpenCV 矩阵数据结构。
你好世界
创建一个名为Chapter01_02
的新加工草图。在 IDE 的菜单栏中,选择草图➤显示草图文件夹,如图 1-56 所示。
图 1-56。
Show Sketch Folder menu item in Processing IDE
在弹出窗口中,创建一个名为code
的新文件夹。将所有 OpenCV Java 库文件复制到其中。它应该包含以下文件。您可以只保留生成的三个平台中的一个opencv-310.jar
文件。或者,你可以只复制与你正在使用的操作系统相关的本地库,比如 macOS 的libopencv_java310.dylib
,Linux 的libopencv_java310.so
,或者 Windows 的opencv_java310.dll
。
opencv-310.jar
libopencv_java310.dylib
libopencv_java310.so
opencv_java310.dll
在 IDE 主窗口中,键入以下代码,然后单击播放按钮执行:
import org.opencv.core.Core;
void setup() {
size(640, 480);
println(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
void draw() {
background(100, 100, 100);
}
这将在 IDE 窗口的底部返回 OpenCV 本地库名称opencv_java310
和版本号 3.1.0。这个位置就是控制台窗口,也是显示消息的地方,如图 1-57 所示。第一个import
语句导入 OpenCV core
模块供后续引用。在setup
函数中,size
函数为程序定义了 Java 窗口的大小。两个println
语句显示两个常量Core.NATIVE_LIBRARY_NAME
和Core.VERSION
的内容。下一条语句System.loadLibrary
从code
文件夹加载本地库。draw
例程只有一个功能,将窗口背景画成灰色。
图 1-57。
Displaying OpenCV information in Processing
矩阵示例
从之前的“Hello world”练习中,选择文件菜单;选择另存为以新名称Chapter01_03
保存程序。在这种情况下,code
文件夹中的内容将被复制到新程序中。在本练习中使用以下代码:
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.CvType;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
Mat m = Mat.eye(3, 3, CvType.CV_8UC1);
println("Content of the matrix m is:");
println(m.dump());
}
void draw() {
background(100, 100, 100);
}
三个import
语句包括 OpenCV 核心组件的定义、矩阵数据类和矩阵元素的数据类型。新语句定义了一个矩阵m
,是一个三行三列的单位矩阵。
Mat m = Mat.eye(3, 3, CvType.CV_8UC1);
矩阵中的每个数据元素都是 8 位无符号字符(一个字节)。第二个println
语句将转储矩阵m
的内容。(下一章将有关于矩阵数据结构及其表示和使用的更详细的解释)。
println(m.dump());
图 1-58 显示了println
语句在加工窗口中显示的内容。
图 1-58。
Displaying matrix information
结论
本章指导您在三个最常见的平台上安装 Processing 和 OpenCV,这三个平台是 macOS、Microsoft Windows 和 Ubuntu Linux。此时,您应该能够准备好环境,继续处理图像处理和计算机视觉任务。下一章将描述数字图像在 Processing (Java)和 OpenCV 中的表示。
二、图像源和表示
本章解释了在 Processing 中创建数字图像的过程,以及在 Processing 和 OpenCV 中光栅图像的内部表示。它演示了如何将外部图像导入到程序中,以及如何使用不同类型的图像处理函数。作为数字图像的一种扩展形式,数字视频和实时视频流的使用将在本章后面的章节中介绍。以下是本章涵盖的基本概念:
- 数字图像基础
- 正在处理的图像
- 处理中的移动图像
- OpenCV 中的矩阵和图像
- Processing 和 OpenCV 之间的图像转换
数字图像基础
我通常用网格的比喻来表示一幅数字图像。图像的尺寸等于网格的大小,宽度代表列数,高度代表行数。因此,网格内的单元数等于宽度×高度。网格中的每个单元格都是一个彩色像素。对于灰度图像,像素是表示灰色调强度的数字。如果使用一个字节的数据来表示每个像素,灰度将在 0 到 255 的范围内。图 2-1 表示八列六行的灰度图像。
图 2-1。
Grayscale image representation
对于彩色图像,像素是一组代表各个颜色通道强度的数字。常见的颜色表示是 RGB(红、绿、蓝)和 HSB(色调、饱和度、亮度)。为了在 Processing 和 OpenCV 之间架起颜色格式的桥梁,本书主要采用了 ARGB 表示法。每个颜色像素有四个独立的颜色通道,即 alpha(透明度)、红色、绿色和蓝色。例如,完全不透明的红色将是(255,255,0,0),或者用十六进制表示法#FFFF0000。
总而言之,您使用宽度和高度来描述数字图像的尺寸,使用通道来描述每个像素中颜色元素的数量,使用深度来描述表示每种颜色的数据位数。
正在处理的图像
您用来处理数字图像的主要处理类是PImage
( https://processing.org/reference/PImage.html
)。它是仿照 Java 中的BufferedImage
类( https://docs.oracle.com/javase/8/docs/api/java/awt/img/BufferedImage.html
)设计的。我不会要求您学习用于PImage
类的 Javadoc,而是带您完成常见的图像处理任务,以完成以下任务:
- 导入外部图像
- 在处理中创建图像
- 显示图像
- 导出图像
导入外部图像
对于这一系列练习,首先创建一个名为Chapter02_01
的加工草图(程序)。添加外部图像的最简单方法是将图像直接拖动到处理 IDE 窗口上。该过程将在您的加工草图文件夹中创建一个data
文件夹。或者,您可以在加工草图文件夹中手动创建一个data
文件夹,并将图像复制到data
文件夹中。处理过程将自动在该文件夹中搜索外部图像、电影和其他数据文件,如可扩展标记语言(XML)文件。检查您的data
文件夹中的外部图像。本次练习使用的图像为HongKong.png
,如图 2-2 所示。下面的代码将加载图像并在处理窗口中显示它。在本例中,处理窗口的大小为 640×480,与图像的大小相同。当裁剪和填充的大小不同时,它们可能会出现。
图 2-2。
Loading an external image
PImage img;
void setup() {
size(640, 480);
img = loadImage("HongKong.png");
noLoop();
}
void draw() {
image(img, 0, 0);
}
第一条语句定义了PImage
类的一个实例img
,并且是外部图像的容器。如图所示,setup()
函数中的语句执行图像文件HongKong.png
到img
变量的实际加载:
img = loadImage("HongKong.png");
draw()
函数中的唯一语句,如这里所示,在加工图形窗口中偏移量(0,0)处显示图像:
image(img, 0, 0);
注意在setup()
函数内部有一个noLoop()
语句。它将执行一次draw()
功能,而不是在动画模式下循环执行。
在下一个练习中,您将从互联网上加载一个外部图像(图 2-3 )。创建另一个名为Chapter02_02
的加工草图。输入以下代码。修改String
变量fName
,使其指向您想要导入的任何外部图像的 URL。
图 2-3。
Loading an image from the Internet
PImage img;
String fName;
void setup() {
size(640, 480);
background(255, 200, 200);
fName = "http://www.magicandlove.com/blog/wp-content/uploads/2011/10/BryanChung-225x300.png";
img = requestImage(fName);
}
void draw() {
if (img.width > 0 && img.height > 0) {
image(img, 360, 100);
}
}
在本练习中,您将使用函数requestImage()
来加载一个外部映像,该映像通常驻留在互联网上的某个服务器上。该函数与另一个线程执行异步加载。但是,它不会在成功加载后进行回调。您可以利用PImage
类的两个属性width
和height
来检查加载是否完成。在加载过程中,图像的width
和height
属性的值为 0。成功完成加载后,这些值将成为所加载图像的尺寸。以下代码显示了如何修改前面的代码以打印draw()
函数中width
和height
的值,以便您可以检查它们的值:
void draw() {
println(img.width + ", " + img.height);
if (img.width > 0 && img.height > 0) {
image(img, 360, 100);
}
}
如果您故意将 URL 更改为错误的地址,您可能会发现img.width
和img.height
的值都变成了-1。
在处理中创建图像
除了加载外部图像(图 2-4 ,您还可以在处理过程中从头开始创建数字图像。这样做的函数是createImage()
,它将返回一个PImage
类的实例。下一个程序,Chapter02_03
,将创建一个空的图像,并改变其所有像素为黄色:
图 2-4。
Creating a yellow image within Processing
PImage img;
void setup() {
size(640, 480);
background(100, 100, 100);
img = createImage(width, height, ARGB);
color yellow = color(255, 255, 0);
for (int y=0; y<img.height; y++) {
for (int x=0; x<img.width; x++) {
img.set(x, y, yellow);
}
}
}
void draw() {
image(img, 0, 0);
}
以下语句创建大小为width
× height
的数字图像:
img = createImage(width, height, ARGB);
变量width
是指在size()
函数中指定的加工窗口宽度。它的值是 640。类似地,变量height
是 480,在size()
函数中定义。参数ARGB
定义了四通道图像(即阿尔法、红色、绿色和蓝色)。您还可以使用RGB
定义三通道图像,使用ALPHA
定义单个 alpha 通道图像。然而,PImage
类的内部表示仍然是ARGB
。下一条语句定义了一个名为yellow
的颜色变量:
color yellow = color(255, 255, 0);
其值为yellow
,红色和绿色通道的强度最大(255)。带有索引y
和x
的嵌套for
循环简单地遍历图像的所有像素img
,并将像素颜色更改为yellow
。注意使用set()
功能修改单个像素的颜色。这是一种通过使用水平和垂直索引来改变图像中特定点的像素颜色的简便方法。然而,set()
函数并不是最有效的像素操作方式。我将在本章的后面介绍其他方法来达到这个效果。
img.set(x, y, yellow);
图形和图像
在下一个练习Chapter02_04
中,您将研究处理画布的内部结构。类PGraphics
是主要的图形和渲染上下文。它也是PImage
的子类。在这种情况下,您可以使用相同的image()
函数来显示这个图形上下文。下面的代码将首先在画布的左上角绘制一个矩形,然后按偏移量显示画布。执行后,您将看到两个矩形。
PGraphics pg;
void setup() {
size(640, 480);
background(100, 100, 100);
pg = getGraphics();
noLoop();
}
void draw() {
rect(0, 0, 200, 120);
image(pg, 200, 120);
}
图 2-5 显示了执行的结果。左上角的第一个矩形是draw()
函数中rect()
语句的结果。image()
语句将整个画布水平偏移 200 像素,垂直偏移 120 像素,并显示整个画布。当您需要将当前绘图画布捕获为图像时,该技术非常有用。
图 2-5。
Use of PGraphics as PImage
在本练习Chapter02_05
中,您将学习PGraphics
类的一般用法。您可以将PGraphics
实例视为一个独立的画布,这样您就可以在屏幕外的画布上进行绘制。当它准备好显示时,您可以使用image()
功能将其显示在加工窗口中。
PGraphics pg;
boolean toDraw;
void setup() {
size(640, 480);
background(0);
pg = createGraphics(width, height);
toDraw = false;
}
void draw() {
if (toDraw)
image(pg, 0, 0);
}
void mouseDragged() {
pg.beginDraw();
pg.noStroke();
pg.fill(255, 100, 0);
pg.ellipse(mouseX, mouseY, 20, 20);
pg.endDraw();
}
void mousePressed() {
pg.beginDraw();
pg.background(0);
pg.endDraw();
toDraw = false;
}
void mouseReleased() {
toDraw = true;
}
图 2-6 显示了草图样本运行的结果。
图 2-6。
Use of createGraphics() and the PGraphics
注意使用下面的createGraphics()
函数来创建一个与处理窗口大小相同的PGraphics
类的实例。它将被用作一个离屏缓冲区来存储您通过拖动鼠标绘制的图形。当您按下、拖动并释放鼠标按钮时,mousePressed()
、mouseDragged()
和mouseReleased()
这三个回调函数将被触发。如果你想在PGraphics
实例pg
中创建任何图形,你必须把命令放在pg.beginDraw()
和pg.endDraw()
块中。还要注意,您可以通过只输入一个数字来指定灰度颜色,例如在background(0)
函数中,它用黑色清除背景。在以下代码行中,变量对mouseX
和mouseY
将返回处理图形窗口中的当前鼠标位置,以像素为单位:
pg.ellipse(mouseX, mouseY, 20, 20);
在该语句中,在当前鼠标位置的屏幕外缓冲区pg
上绘制了一个椭圆/圆。处理还提供了另一对变量,pmouseX
和pmouseY
,它们存储动画最后一帧中的鼠标位置。当您需要绘制一条从前一个鼠标位置到当前位置的线段时,这两对鼠标位置变量将非常有用。
加工中的缓冲损伤
在前面的小节中,您学习了如何在处理中使用主图像处理类,PImage
。对于熟悉 Java 图像处理的人来说,类BufferedImage
( https://docs.oracle.com/javase/8/docs/api/java/awt/img/BufferedImage.html
)对于程序员能够在 Java 中操作图像是很重要的。在处理过程中,您还可以在PImage
类和BufferedImage
类之间执行转换。有时候,在处理返回一个BufferedImage
类的过程中加入其他 Java 图像处理库会很有用。下面的代码演示了处理过程中PImage
类和BufferedImage
类之间的转换:
import java.awt.image.BufferedImage;
PImage img;
BufferedImage bim;
void setup() {
size(640, 480);
noLoop();
}
void draw() {
background(0);
// create the PImage instance img
img = createImage(width, height, ARGB);
// create the BufferedImage instance bim from img
bim = (BufferedImage) img.getNative();
println(bim.getWidth() + ", " + bim.getHeight());
// create a new PImage instance nim from BufferedImage bim
PImage nim = new PImage(bim);
println(nim.width + ", " + nim.height);
}
首先,您使用import java.awt.image.BufferedImage
将BufferedImage
的引用包含到您的加工草图中。在draw()
函数中,使用createImage()
函数创建一个空的PImage
实例img
。通过使用getNative()
方法,您可以创建一个BufferedImage
格式的原始图像的副本。给定一个BufferedImage
、bim
,您可以通过使用new PImage(bim)
命令再次创建一个PImage
。在下面的示例Chapter02_06
中,您可以看到这种转换在创造性结果中的实际应用:
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.awt.Rectangle;
Robot robot;
void setup() {
size(640, 480);
try {
robot = new Robot();
}
catch (Exception e) {
println(e.getMessage());
}
}
void draw() {
background(0);
Rectangle rec = new Rectangle(mouseX, mouseY, width, height);
BufferedImage img1 = robot.createScreenCapture(rec);
PImage img2 = new PImage(img1);
image(img2, 0, 0);
}
这个处理草图主要使用 Java Robot
类( https://docs.oracle.com/javase/8/docs/api/java/awt/Robot.html
)做一个截屏。屏幕截图的输出(图 2-7 )是一个BufferedImage
,您可以将其转换为PImage
以在draw()
功能中显示。在setup()
函数中,初始化try
块中的robot
实例来捕获AWTException
。在draw()
函数中,首先使用一个Rectangle
对象来定义要捕捉的屏幕区域的偏移量和大小。robot.createScreenCapture(rec)
将执行实际的屏幕截图,生成的图像存储在img1
中,它是BufferedImage
的一个实例。下一条语句将img1
实例转换成另一个PImage
实例img2
,以便用image()
函数显示。当您在处理窗口内移动鼠标时,您会发现一个有趣的结果,类似于视频艺术中的反馈效果。正是image()
功能修改了每一帧中的屏幕内容,促成了这个反馈循环。
图 2-7。
Screen capture with PImage
处理中的移动图像
您在上一章中安装的用于处理的外部视频库( https://processing.org/reference/libraries/video/index.html
)提供了视频播放和捕获的必要功能。它基于 GStreamer 多媒体框架中的 Java 绑定。该库包含两个独立的类:Movie
用于视频回放,而Capture
用于实时视频捕捉。两者都是PImage
的子类。您可以使用类似的方法来处理像素数据。
数字电影
下一个练习Chapter02_07
将循环播放视频库中分发的示例视频transit.mov
。就像将图像添加到处理草图中一样,您只需将数字视频文件拖到处理 IDE 窗口中。或者你可以在 sketch 文件夹里面创建一个data
文件夹,把视频文件复制到那里。以下代码执行数字视频的异步回放。每当一个新的帧准备好了,回调movieEvent()
就会被触发来读取该帧。
import processing.video.*;
Movie mov;
void setup() {
size(640, 360);
background(0);
mov = new Movie(this, "transit.mov");
mov.loop();
}
void draw() {
image(mov, 0, 0);
}
void movieEvent(Movie m) {
m.read();
}
当您从主菜单中选择草图➤导入库➤视频时,处理将自动生成第一个import
语句。下一步是定义Movie
类实例mov
。下面的语句将使用视频的名称创建新的实例:
mov = new Movie(this, "transit.mov");
关键字this
指的是当前的加工草图(即Chapter02_07
),回调函数movieEvent()
需要引用它。图 2-8 为运行示意图。
图 2-8。
Digital video playback example
下一个例子Chapter02_08
,提供了另一种读取数字视频的方法。在这个版本中,每个动画帧中的草图检查新帧的可用性,并同步读取它。
import processing.video.*;
Movie mov;
void setup() {
size(640, 360);
background(0);
mov = new Movie(this, "transit.mov");
mov.loop();
frameRate(30);
}
void draw() {
if (mov.available()) {
mov.read();
}
image(mov, 0, 0);
}
注意在setup()
函数中有一个新的frameRate()
语句,指定了draw()
函数的每秒帧速率。对于较慢的计算机,实际帧速率可能比这里指定的要慢。
由于Movie
是PImage
的子类,你可以使用PImage
的get()
方法从视频的任何一帧中检索像素颜色数据。下一张加工草图Chapter02_09
将展示这一点:
import processing.video.*;
Movie mov;
void setup() {
size(640, 360);
background(0);
mov = new Movie(this, "transit.mov");
mov.loop();
frameRate(30);
}
void draw() {
if (mov.available()) {
mov.read();
}
image(mov, 0, 0);
}
void mouseClicked() {
color c = mov.get(mouseX, mouseY);
println(red(c) + ", " + green(c) + ", " + blue(c));
}
在本练习中,您将显示所单击像素的颜色信息。这在mouseClicked()
回调函数中完成。您提供了像素在mov
帧中的水平和垂直位置。它返回变量c
中的颜色数据。通过使用red()
、green()
和blue()
函数,您可以从中检索三原色分量。数字的范围将在 0 到 255 之间。
实时视频捕捉
除了数字视频回放之外,Processing 还提供了类Capture
来支持从常规网络摄像头或捕获设备实时捕获视频流。就像使用Movie
类时,需要导入video
库,如下面的练习Chapter02_10
所示:
import processing.video.*;
Capture cap;
void setup() {
size(640, 480);
background(0);
cap = new Capture(this, width, height);
cap.start();
}
void draw() {
image(cap, 0, 0);
}
void captureEvent(Capture c) {
c.read();
}
您将Capture
类与实例cap
一起使用。new
语句创建该类的一个新实例,并将其分配给cap
。它还需要一个start()
方法来启动捕获设备。类似于Movie
类,Capture
类带有名为captureEvent()
的回调函数,其中捕获设备可以异步通知主处理草图读入任何可用的新视频帧。由于Capture
是PImage
的子类,您可以使用相同的image()
函数在处理窗口中显示捕捉帧。
在下一个练习Chapter02_11
中,您将在draw()
函数中使用捕捉帧的同步读取。同时,您引入了mask()
功能,通过在一幅蒙版图像上交互绘制来遮盖图像的一部分。
import processing.video.*;
Capture cap;
PGraphics pg;
void setup() {
size(640, 480);
background(0);
cap = new Capture(this, width, height);
cap.start();
pg = createGraphics(width, height);
pg.beginDraw();
pg.noStroke();
pg.fill(255);
pg.background(0);
pg.endDraw();
}
void draw() {
if (cap.available()) {
cap.read();
}
tint(255, 0, 0, 40);
cap.mask(pg);
image(cap, 0, 0);
}
void mouseDragged() {
pg.beginDraw();
pg.ellipse(mouseX, mouseY, 20, 20);
pg.endDraw();
}
在本练习中,您将使用一个名为pg
的PGraphics
实例作为离屏缓冲区。在mouseDragged()
回调函数中,用户可以在黑色背景上创建一个白色圆形标记。在draw()
函数中,您引入了两个新函数。第一个是tint()
,用红色(255,0,0)和一点透明度给结果图像着色,如第四个参数 40 所示。第二个是mask()
功能,在这里你把蒙版pg
应用到原始图像上(图 2-9)cap
。结果是一种交互式体验,你可以拖动鼠标来显示底层的实时视频流。
图 2-9。
Live video capture with a mask
OpenCV 中的矩阵和图像
既然我已经介绍完了如何使用外部图像、视频和直播流进行处理,我将切换回 OpenCV 来帮助您理解它是如何表示数字图像的。开始之前,记得在下一个加工草图Chapter02_12
中创建一个code
文件夹。在code
文件夹中,放入你在前一章创建的 OpenCV 库文件。以下是文件夹中的 OpenCV 文件:
opencv-310.jar
libopencv_java310.dylib
(适用于苹果电脑)libopencv_java310.so
(对于 Linux,如 Ubuntu)opencv_java310.dll
(用于 Windows)
在本练习中,您将使用不同的选项定义多个空矩阵Mat
,以便理解Mat
类的内部结构。我将讲述的不同的班级是Mat
、Size
、CvType
和Scalar
。
import org.opencv.core.*;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
noLoop();
}
void draw() {
background(0);
Mat m1 = new Mat();
println(m1.dump());
Mat m2 = new Mat(3, 4, CvType.CV_8UC1, Scalar.all(0));
println(m2.dump());
Mat m3 = new Mat(3, 4, CvType.CV_8UC3, Scalar.all(255));
println(m3.dump());
Mat m4 = new Mat(new Size(4, 3), CvType.CV_8UC3, new Scalar(0, 255, 0));
println(m4.dump());
}
在本练习中,您定义了四个矩阵。第一个矩阵m1
是一个没有维度信息的空矩阵。方法m1.dump()
将返回矩阵内容的可打印形式。第二个矩阵m2
有三行四列,所以元素总数是 12。每个元素都是 8 位无符号数(CvType.CV_8UC1
)。首次创建矩阵时,元素的值为 0 ( Scalar.all(0)
)。方法m2.dump()
将在三行四列中显示 12 个 0 元素。第三个矩阵m3
与m2
具有相同的尺寸。然而,m3
中的每个元素由三个独立的数字或通道组成(CvType.CV_8UC3
)。所有元素的值都是 255 ( Scalar.all(255)
)。您使用不同的方法定义第四个矩阵的维度,m4
。new Size(4, 3)
定义了一个新的Size
对象实例,宽度为 4,高度为 3,相当于一个三行四列的矩阵。每个矩阵元素对于三个通道是相同的。在m4
中,您用值为(0, 255, 0)
的Scalar
实例初始化矩阵元素。
在继续下一个练习之前,让我们看看CvType
类是如何工作的。CvType
之后的数据类型规范如下:
CV_[bits][type]C[channels]
这里有一个解释:
[bits]
表示代表每个数据元素的位数。它可以是 8、16 或 32。[type]
表示数据表示的类型。可以不签名,U
;署名,S
;还是浮,F
。[channels]
表示矩阵中每个数据元素的通道数。它可以是 1、2、3 或 4。
在下一个练习Chapter02_13
中,您将探索Mat
类中的许多方法,以理解矩阵元素的数据类型和表示:
import org.opencv.core.*;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
noLoop();
}
void draw() {
background(0);
Mat m1 = new Mat(new Size(4, 3), CvType.CV_8UC3, new Scalar(0, 100, 0));
println(m1.dump());
println(m1.rows() + ", " + m1.cols());
println(m1.width() + ", " + m1.height());
println("Size: " + m1.size());
println("Dimension: " + m1.dims());
println("Number of elements: " + m1.total());
println("Element size: " + m1.elemSize());
println("Depth: " + m1.depth());
println("Number of channels: " + m1.channels());
}
您应该从处理控制台窗口获得以下输出:
[ 0, 100, 0, 0, 100, 0, 0, 100, 0, 0, 100, 0;
0, 100, 0, 0, 100, 0, 0, 100, 0, 0, 100, 0;
0, 100, 0, 0, 100, 0, 0, 100, 0, 0, 100, 0]
3, 4
4, 3
Size: 4x3
Dimension: 2
Number of elements: 12
Element size: 3
Depth: 0
Number of channels: 3
大多数信息都很简单。元素大小是每个矩阵元素包含的字节数。深度是每个通道的数据类型指示器。值 0 表示数据类型是 8 位无符号整数,主要用于处理和 OpenCV 之间。除了从现有矩阵中获取信息,下一个练习Chapter02_14
将展示如何使用get()
方法从矩阵的单个元素中检索信息:
import org.opencv.core.*;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
noLoop();
}
void draw() {
background(0);
Mat m1 = new Mat(new Size(4, 3), CvType.CV_8UC4, new Scalar(100, 200, 80, 255));
double [] result = m1.get(0, 0);
printArray(result);
byte [] data = new byte[m1.channels()];
m1.get(2, 2, data);
for (byte b : data) {
int i = (b < 0) ? b + 256 : b;
println(i);
}
}
请注意,在前面的代码中,下面的语句使用get()
方法来检索位于m1
中第 0 行第 0 列的数据元素。返回的数据将存储在一个名为result
的double
数组中。
double [] result = m1.get(0, 0);
您可能会发现,即使矩阵中的每个数据元素都被定义为一个字节(8 位),使用这种语法的get()
方法的结果总是返回一个双数组。双数组result
的长度为 4,这是CV_8UC4
中定义的通道数。如果将数据元素定义为CV_8UC1
(即只有一个通道),那么返回的result
也将是一个长度等于 1 的双数组。练习的第二部分演示了您还可以显式定义一个长度为 4 的名为data
的字节数组,使用不同的语法和get()
方法从位置行 2 列 2 中检索数据元素,并直接将其存储到字节数组data
中。在for
循环中,您还需要考虑 Java 没有无符号字节数据类型的事实。对于负数,必须加上 256 才能转换成大于 127 的原始数。
在get()
方法之后,下一个练习Chapter02_15
探索了put()
方法来改变矩阵中数据元素的内容。它演示了使用put()
方法的两种方式。第一个用字节数组更新数据元素。第二个用一个双数字列表更新数据元素。
import org.opencv.core.*;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
noLoop();
}
void draw() {
background(0);
Mat m1 = new Mat(new Size(4, 3), CvType.CV_8UC4, new Scalar(100, 200, 80, 255));
byte [] data1 = new byte[m1.channels()];
byte [] data2 = new byte[m1.channels()];
m1.get(1, 1, data1);
data2[0] = data1[3];
data2[1] = data1[2];
data2[2] = data1[1];
data2[3] = data1[0];
m1.put(1, 1, data2);
printArray(m1.get(1, 1));
m1.put(2, 2, 123, 234, 200, 100);
printArray(m1.get(2, 2));
}
本练习的第一部分是将第 1 行第 1 列的数据元素检索到data1
数组中。然后将data1
数组重新排序为data2
数组,长度相同。第一个put()
方法将data2
数组存储到同一个数据元素中。然后使用printArray()
功能显示该数据元素的单个通道信息。在练习的第二部分,您只需在put()
方法中列出四通道值的四个数字。它们将被存储在第 2 行第 2 列,如第二个printArray()
语句所示。草图控制台窗口的结果如下:
[0] 255.0
[1] 80.0
[2] 200.0
[3] 100.0
[0] 123.0
[1] 234.0
[2] 200.0
[3] 100.0
在结束本次会议之前,您将学习put()
和get()
函数的另一个特性,即进行批量信息更新和检索。当编写代码在 Processing 和 OpenCV 之间转换图像数据时,该功能是必不可少的。在接下来的练习Chapter02_16
中,您将使用一个字节数组和一个矩阵大小的数字序列进行批量更新和检索。
import org.opencv.core.*;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
noLoop();
}
void draw() {
background(0);
Mat m1 = new Mat(new Size(3, 2), CvType.CV_8UC1);
for (int r=0; r<m1.rows(); r++) {
for (int c=0; c<m1.cols(); c++) {
m1.put(r, c, floor(random(100)));
}
}
println(m1.dump());
byte [] data = new byte[m1.rows()*m1.cols()*m1.channels()];
m1.get(0, 0, data);
printArray(data);
Mat m2 = new Mat(new Size(3, 2), CvType.CV_8UC2, Scalar.all(0));
m2.put(0, 0, 1, 2, 3, 4, 5, 6, 7, 8);
println(m2.dump());
}
练习的第一部分定义了一个两行三列的小矩阵。每个数据元素都是存储在一个字节中的单通道数字。for
循环用小于 100 的随机整数值初始化矩阵m1
。然后定义一个名为data
的空字节数组,其大小由矩阵m1
的大小决定(即 2 × 3 × 1 = 6)。在get()
方法之后,所有的矩阵内容都被转储到data
数组中。在本练习的第二部分,您将定义另一个矩阵m2
,它有两行三列。每个数据元素都是一个双通道数字对,如CV_8UC2
所示。put()
方法将把数字序列存储到数组的前四个数据元素中。该序列将按行顺序排列。受影响的单元格是(0,0)、(0,1)、(0,2)、(1,0)。括号内的第一个数字是行号,第二个数字是列号。以下语句将(1,2)、(3,4)、(5,6)、(7,8)存储到(0,0)、(0,1)、(0,2)、(1,0)处的位置:
m2.put(0, 0, 1, 2, 3, 4, 5, 6, 7, 8);
其余的数据元素不会受到影响。图 2-10 显示了操作后的原始矩阵和新矩阵。
图 2-10。
Operation of the matrix put() function
您可能会注意到,即使您在get()
和put()
函数中指定了一个矩阵元素,如果您使用的字节数组超过了一个元素的大小,这些函数也会影响矩阵的其余内容。在下一节中,您将使用这种技术在 Processing 的PImage
和 OpenCV 的Mat
之间转换数据。
Processing 和 OpenCV 之间的图像转换
这一节对于任何想要使用 OpenCV 进行处理的应用都很重要。要使用 OpenCV,您必须将您在处理环境中创建的原始图像(如静态照片、数字视频或网络摄像头直播)转换为 OpenCV 可以操作的Mat
格式。在对图像执行 OpenCV 操作之后,最后一步是将它们转换成处理可以在其窗口中显示的PImage
格式。
在本章开始时,您已经了解到处理中的图像是彩色像素的二维数组。水平尺寸是宽度,垂直尺寸是高度。每个像素都是数据类型color
。颜色像素的内部表示是 32 位的整数。color 实例的十六进制表示法是 0xAARRGGBB,对应于 alpha、红色、绿色和蓝色通道。每个颜色通道的值范围是从 0 到 255。例如,要定义黄色,您可以编写以下代码:
color yellow = color(255, 255, 0);
如果你只指定三个颜色通道,默认的 alpha 值会自动设置为 255。您也可以用十六进制符号来表示颜色,如下所示:
color yellow = 0xFFFFFF00;
以下代码段将演示color
变量的使用以及从中检索颜色通道值的不同方法:
color col = color(200, 100, 40);
println("Color value as integer");
println(col);
println("RGB from bitwise operations");
println(col & 0x000000FF);
println((col & 0x0000FF00) >> 8);
println((col & 0x00FF0000) >> 16);
println("RGB from functions");
println(red(col));
println(green(col));
println(blue(col));
内部处理不会将图像存储为二维数组。而是存储为名为pixels[]
的一维整数数组。数组的长度是由它的width
× height
定义的图像的像素总数。对于图像中第y
行和第x
列的像素,pixels[]
数组的索引如下:
index = y * width + x;
例如,当您有一个只有两行三列的图像时,二维数组如下所示:
| 0 (0, 0) | 1 (0, 1) | 2 (0, 2) | | 3 (1, 0) | 4 (1, 1) | 5 (1, 2) |括号内的两个数字是列和行的索引。括号外的单个数字是一维数组中的索引,pixels[]
,存储处理中的图像。
在pixels[]
数组中,每个单元格都是像素的颜色信息,以 0xAARRGGBB 格式存储为整数。每个整数由 4 个字节组成。整数中的每个字节按照 ARGB 顺序为像素存储一个单独的颜色通道。
在 OpenCV 中,颜色像素格式更加灵活,如前一节所示。为了使 OpenCV 与 Processing 兼容,您将坚持使用CV_8UC4
格式,这样您就可以在 Processing 和 OpenCV 之间交换相同数量的存储。然而,许多 OpenCV 函数依赖于灰度图像(即CV_8UC1
)和三通道彩色图像的使用,例如 BGR 顺序中的CV_8UC3)
。在这种情况下,让我们灵活地将图像转换为三个通道和一个单通道彩色图像。
以您用于处理的两行三列为例,OpenCV 表示如下:
| 倍黑 | 游戏结束 | 乡邮投递路线 | 嗜酒者互诫协会 | 倍黑 | 游戏结束 | 乡邮投递路线 | 嗜酒者互诫协会 | 倍黑 | 游戏结束 | 乡邮投递路线 | 嗜酒者互诫协会 | 倍黑 | 游戏结束 | 乡邮投递路线 | 嗜酒者互诫协会 | 倍黑 | 游戏结束 | 乡邮投递路线 | 嗜酒者互诫协会 | 倍黑 | 游戏结束 | 乡邮投递路线 | 嗜酒者互诫协会 |细胞总数为width × height × channels
,在本例中为 24。二维图像矩阵将存储为 24 字节或 48 个十六进制字符的线性数组。四个连续的字节组成一个通道顺序为 BGRA 的像素。字节数组将是 OpenCV 图像矩阵的内部表示。
现在你有两个数组。第一个是来自处理的大小为(width × height
)的整数数组;第二个是来自 OpenCV 的大小为(width × height × channels
)的字节数组。问题是如何在它们之间进行转换。Java ByteBuffer
和IntBuffer
类是这个问题的解决方案。
从处理到 OpenCV
您需要在处理环境中有一个源图像。您将使用Capture
类来检索本练习的视频帧Chapter02_17
。在每次运行draw()
函数时,你都试图将帧转换成 OpenCV Mat
。为了验证转换是否有效,处理草图将允许用户单击视频帧内的任何位置,以在窗口的右上角显示其像素颜色。
import processing.video.*;
import org.opencv.core.*;
import java.nio.ByteBuffer;
Capture cap;
String colStr;
Mat fm;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width, height);
cap.start();
frameRate(30);
colStr = "";
fm = new Mat();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
fm = imgToMat(cap);
image(cap, 0, 0);
text(nf(round(frameRate), 2), 10, 20);
text(colStr, 550, 20);
}
Mat imgToMat(PImage m) {
Mat f = new Mat(new Size(m.width, m.height), CvType.CV_8UC4,
Scalar.all(0));
ByteBuffer b = ByteBuffer.allocate(f.rows()*f.cols()*f.channels());
b.asIntBuffer().put(m.pixels);
b.rewind();
f.put(0, 0, b.array());
return f;
}
void mouseClicked() {
int x = constrain(mouseX, 0, width-1);
int y = constrain(mouseY, 0, height-1);
double [] px = fm.get(y, x);
colStr = nf(round((float)px[1]), 3) + ", " +
nf(round((float)px[2]), 3) + ", " +
nf(round((float)px[3]), 3);
}
有三个全局变量。第一个,cap
,是视频捕捉对象。第二个,fm
,是临时的 OpenCV Mat
,存储网络摄像头图像的当前帧。第三个是String
变量colStr
,保存用户点击的像素的 RGB 颜色值。在draw()
函数中,程序将当前网络摄像头图像cap
传递给函数imgToMat()
。该函数返回一个 OpenCV Mat
并存储在变量fm
中。每当用户点击屏幕,回调函数mouseClicked()
将通过使用get(y, x)
函数从fm
对象获取像素颜色数据。然后,它将返回一个名为px[]
的双数组,该数组按照 ARGB 顺序保存颜色像素信息。请注意,您尚未执行通道重新排序过程,以从处理中的 ARGB 顺序更改为 OpenCV 中的 BGRA 顺序。
程序的核心是imgToMat()
函数。它接受类型为PImage
的输入参数。第一条语句定义了一个临时 OpenCV Mat f
,其大小与输入m
相同。第二条语句创建了一个大小为 640 × 480 × 4 = 1228800 的ByteBuffer
变量b
。它是处理PImage
和 OpenCV Mat
之间交换数据的关键缓冲区。下一条语句将ByteBuffer
视为IntBuffer
,并将整数数组m.pixels
作为内容放入自身。在put
动作之后,你倒带缓冲区b
,这样指针将回到它的起点,以便后续访问。最后一步是用下面的语句将byte
数组缓冲区b
的内容放到Mat f
中:
f.put(0, 0, b.array());
图 2-11 显示了该处理草图从处理视频捕获图像转换为 OpenCV 的测试运行的示例截图。在下一节中,您将从相反的方向将 OpenCV 矩阵转换为处理图像。
图 2-11。
Conversion from Processing to OpenCV
从 OpenCV 到处理
在下一个练习Chapter02_18
中,您将简单地定义一个纯色的四通道 OpenCV 矩阵,并将其直接转换为处理PImage
进行显示。
import org.opencv.core.*;
import java.nio.ByteBuffer;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
noLoop();
}
void draw() {
background(0);
Mat fm = new Mat(new Size(width, height), CvType.CV_8UC4, new Scalar(255, 255, 200, 0));
PImage img = matToImg(fm);
image(img, 0, 0);
}
PImage matToImg(Mat m) {
PImage im = createImage(m.cols(), m.rows(), ARGB);
ByteBuffer b = ByteBuffer.allocate(m.rows()*m.cols()*m.channels());
m.get(0, 0, b.array());
b.rewind();
b.asIntBuffer().get(im.pixels);
im.updatePixels();
return im;
}
这个程序的核心函数是matToImg()
。它将 OpenCV Mat
作为唯一的参数,并输出一个处理PImage
作为返回值。逻辑与上一节正好相反。它再次使用一个ByteBuffer
类作为临时存储位置。该函数的第一条语句创建一个临时的PImage
变量im
,其大小与输入Mat
参数m
相同。第二条语句定义了 1,228,800 字节的临时存储。第三条语句使用Mat
的get()
方法将内容加载到ByteBuffer b
中。第四个语句在加载后倒回ByteBuffer
。下一条语句将ByteBuffer
视为IntBuffer
,并将其内容作为整数数组传输到临时PImage
变量im
的pixels
。然后对PImage
执行一个updatePixels()
来刷新它的内容并返回给调用者。该草图的结果将是一个填充橙色的窗口,如 ARGB 顺序中Scalar(255, 255, 200, 0)
所定义。
在下一个练习Chapter02_19
中,您将在 OpenCV 中进行视频捕捉,并将Mat
图片帧转换为处理后的PImage
进行显示。我将在本练习中介绍一些新功能。第一个是 OpenCV 中执行视频捕捉任务的videoio
(视频输入输出)模块。第二个是imgproc
(图像处理)模块,帮助你进行色彩转换。您使用与上一个练习相同的matToImg()
功能。
import org.opencv.core.*;
import org.opencv.videoio.*;
import org.opencv.imgproc.*;
import java.nio.ByteBuffer;
VideoCapture cap;
Mat fm;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new VideoCapture();
cap.set(Videoio.CAP_PROP_FRAME_WIDTH, width);
cap.set(Videoio.CAP_PROP_FRAME_HEIGHT, height);
cap.open(Videoio.CAP_ANY);
fm = new Mat();
frameRate(30);
}
void draw() {
background(0);
Mat tmp = new Mat();
cap.read(tmp);
Imgproc.cvtColor(tmp, fm, Imgproc.COLOR_BGR2RGBA);
PImage img = matToImg(fm);
image(img, 0, 0);
text(nf(round(frameRate), 2), 10, 20);
tmp.release();
}
PImage matToImg(Mat m) {
PImage im = createImage(m.cols(), m.rows(), ARGB);
ByteBuffer b = ByteBuffer.allocate(m.rows()*m.cols()*m.channels());
m.get(0, 0, b.array());
b.rewind();
b.asIntBuffer().get(im.pixels);
im.updatePixels();
return im;
}
第一步是从 OpenCV 导入您将在本练习中使用的所有新模块(即org.opencv.videoio.*
和org.opencv.imgproc.*
)。setup()
函数中的new
语句是针对VideoCapture
对象实例cap
的。它需要定义其捕捉帧大小和计算机中可用的默认相机Videoio.CAP_ANY
。在draw()
函数中,cap.read(tmp)
语句抓取并获取新的视频帧到临时的Mat tmp
。不幸的是,tmp
中的颜色通道数量只有三个,并且按 BGR 顺序排列。如果您感兴趣,您可以尝试通过使用它的channels()
方法来显示通道的数量。下一条语句使用imgproc
模块将色彩空间从 BGR 转换到 RGBA,并将新图像保存在矩阵变量fm
中:
Imgproc.cvtColor(tmp, fm, Imgproc.COLOR_BGR2RGBA);
如果你查看 OpenCV 3.1.0 的 Javadoc(http://docs.opencv.org/java/3.1.0/
),你实际上不会发现一个直接从 BGR 到 ARGB 的色彩空间转换。在本练习中,您可以坐下来看看网络摄像头图像会是什么样子。您将在下一个练习中学习如何处理这个问题。图 2-12 为该加工示意图。
图 2-12。
Conversion from OpenCV to Processing
正如所料,由于颜色通道的顺序错误,颜色不自然。您将在下一个练习Chapter02_20
中通过重新排列颜色通道的顺序来解决这个问题:
import org.opencv.core.*;
import org.opencv.videoio.*;
import org.opencv.imgproc.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
VideoCapture cap;
Mat fm;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new VideoCapture();
cap.set(Videoio.CAP_PROP_FRAME_WIDTH, width);
cap.set(Videoio.CAP_PROP_FRAME_HEIGHT, height);
cap.open(Videoio.CAP_ANY);
fm = new Mat();
frameRate(30);
}
void draw() {
background(0);
Mat tmp = new Mat();
Mat src = new Mat();
cap.read(tmp);
Imgproc.cvtColor(tmp, src, Imgproc.COLOR_BGR2RGBA);
fm = src.clone();
ArrayList<Mat> srcList = new ArrayList<Mat>();
ArrayList<Mat> dstList = new ArrayList<Mat>();
Core.split(src, srcList);
Core.split(fm, dstList);
Core.mixChannels(srcList, dstList, new MatOfInt(0, 1, 1, 2, 2, 3, 3, 0));
Core.merge(dstList, fm);
PImage img = matToImg(fm);
image(img, 0, 0);
text(nf(round(frameRate), 2), 10, 20);
src.release();
tmp.release();
}
PImage matToImg(Mat m) {
PImage im = createImage(m.cols(), m.rows(), ARGB);
ByteBuffer b = ByteBuffer.allocate(m.rows()*m.cols()*m.channels());
m.get(0, 0, b.array());
b.rewind();
b.asIntBuffer().get(im.pixels);
im.updatePixels();
return im;
}
您在本练习中使用的重新排列颜色通道的新功能是split()
、mixChannels()
和merge()
。您还可以使用 Java 中的ArrayList
类来处理图像的各个颜色通道。在draw()
函数中,在函数Imgproc.cvtColor()
将 BGR 颜色矩阵转换为 RGBA 颜色矩阵src
之后,您计划将Mat src
复制到目标Mat fm
,颜色通道按照 ARGB 顺序重新排列。首先,将src
矩阵复制到fm
。第二,您将源Mat src
分割成一个由四个Mat
、srcList
组成的ArrayList
。列表中的每个成员都是一个数据类型为CV_8UC1
的Mat
,对应一个单色通道。第三,你把目的地Mat fm
拆分成Mat
的另一个ArrayList
,命名为dstList
。第四,函数Core.mixChannels()
使用MatOfInt
参数中指定的信息重新排列颜色通道的顺序。MatOfInt
是Mat
的子类。它类似于 C++中的向量。本练习中的MatOfInt
实例是一个一行八列的矩阵。这个矩阵的内容是四对数字,将源通道位置映射到目的通道位置。src
和srcList
中的原始颜色通道顺序为 RGBA。fm
和dstList
中的目的色彩通道顺序是 ARGB。
- 资料来源:R(0)、G(1)、B(2)、A(3)
- 目的地:A(0),R(1),G(2),B(3)
红色的源通道 0 映射到目的通道 1。源通道 1 映射到目的通道 2 以获得绿色。蓝色的源通道 2 映射到目的通道 3。对于 alpha,源通道 3 映射到目的地 0。这正是new MatOfInt(0, 1, 1, 2, 2, 3, 3, 0)
命令所指定的。在Core.mixChannels()
函数之后,名为dstList
的ArrayList
包含四个具有正确颜色顺序的单通道矩阵。下一个函数,Core.merge()
,将把四个矩阵组合成一个单一的矩阵,四个颜色通道按 ARGB 顺序排列。然后程序将matToImg()
函数应用于fm
并将PImage
实例返回给img
,通过image()
函数显示在窗口中。图 2-13 显示了如何在加工中运行草图。
图 2-13。
Conversion from OpenCV to Processing with correct color channel order
对于本章的最后一个练习Chapter02_21
,您将把处理和 OpenCV 之间的转换封装在一个 Java 类中,这样您就不需要在本书的后续练习中显式地调用它们。由于转换函数在处理过程中依赖于类PImage
,所以扩展PImage
类来定义它的子类是很方便的。在本练习中,您将新类命名为CVImage
。在处理 IDE 中,可以添加新的页签来创建新的类,如图 2-14 所示。将新选项卡命名为 CVImage。
图 2-14。
Adding a new tab to create a class in Processing
CVImage
类的内容如下:
import org.opencv.core.*;
import org.opencv.imgproc.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
public class CVImage extends PImage {
final private MatOfInt BGRA2ARGB = new MatOfInt(0, 3, 1, 2, 2, 1, 3, 0);
final private MatOfInt ARGB2BGRA = new MatOfInt(0, 3, 1, 2, 2, 1, 3, 0);
// cvImg - OpenCV Mat in BGRA format
// pixCnt - number of bytes in the image
private Mat cvImg;
private int pixCnt;
public CVImage(int w, int h) {
super(w, h, ARGB);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
pixCnt = w*h*4;
cvImg = new Mat(new Size(w, h), CvType.CV_8UC4, Scalar.all(0));
}
public void copyTo() {
// Copy from the PImage pixels array to the Mat cvImg
Mat tmp = new Mat(new Size(this.width, this.height), CvType.CV_8UC4, Scalar.all(0));
ByteBuffer b = ByteBuffer.allocate(pixCnt);
b.asIntBuffer().put(this.pixels);
b.rewind();
tmp.put(0, 0, b.array());
cvImg = ARGBToBGRA(tmp);
tmp.release();
}
public void copyTo(PImage i) {
// Copy from an external PImage to here
if (i.width != this.width || i.height != this.height) {
println("Size not identical");
return;
}
PApplet.arrayCopy(i.pixels, this.pixels);
this.updatePixels();
copyTo();
}
public void copyTo(Mat m) {
// Copy from an external Mat to both the Mat cvImg and PImage pixels array
if (m.rows() != this.height || m.cols() != this.width) {
println("Size not identical");
return;
}
Mat out = new Mat(cvImg.size(), cvImg.type(), Scalar.all(0));
switch (m.channels()) {
case 1:
// Greyscale image
Imgproc.cvtColor(m, cvImg, Imgproc.COLOR_GRAY2BGRA);
break;
case 3:
// 3 channels colour image BGR
Imgproc.cvtColor(m, cvImg, Imgproc.COLOR_BGR2BGRA);
break;
case 4:
// 4 channels colour image BGRA
m.copyTo(cvImg);
break;
default:
println("Invalid number of channels " + m.channels());
return;
}
out = BGRAToARGB(cvImg);
ByteBuffer b = ByteBuffer.allocate(pixCnt);
out.get(0, 0, b.array());
b.rewind();
b.asIntBuffer().get(this.pixels);
this.updatePixels();
out.release();
}
private Mat BGRAToARGB(Mat m) {
Mat tmp = new Mat(m.size(), CvType.CV_8UC4, Scalar.all(0));
ArrayList<Mat> in = new ArrayList<Mat>();
ArrayList<Mat> out = new ArrayList<Mat>();
Core.split(m, in);
Core.split(tmp, out);
Core.mixChannels(in, out, BGRA2ARGB);
Core.merge(out, tmp);
return tmp;
}
private Mat ARGBToBGRA(Mat m) {
Mat tmp = new Mat(m.size(), CvType.CV_8UC4, Scalar.all(0));
ArrayList<Mat> in = new ArrayList<Mat>();
ArrayList<Mat> out = new ArrayList<Mat>();
Core.split(m, in);
Core.split(tmp, out);
Core.mixChannels(in, out, ARGB2BGRA);
Core.merge(out, tmp);
return tmp;
}
public Mat getBGRA() {
// Get a copy of the Mat cvImg
Mat mat = cvImg.clone();
return mat;
}
public Mat getBGR() {
// Get a 3 channels Mat in BGR
Mat mat = new Mat(cvImg.size(), CvType.CV_8UC3, Scalar.all(0));
Imgproc.cvtColor(cvImg, mat, Imgproc.COLOR_BGRA2BGR);
return mat;
}
public Mat getGrey() {
// Get a greyscale copy of the image
Mat out = new Mat(cvImg.size(), CvType.CV_8UC1, Scalar.all(0));
Imgproc.cvtColor(cvImg, out, Imgproc.COLOR_BGRA2GRAY);
return out;
}
}
类定义最重要的部分是Mat
变量cvImg
。它维护了一个 OpenCV 矩阵的副本,类型为CV_8UC4
,颜色通道顺序在 BGRA。copyTo()
方法有三个版本。第一个没有参数的函数将当前的本地pixels
数组复制到 OpenCV 矩阵cvImg
。带有PImage
参数的第二个函数将输入参数pixels
数组复制到本地PImage
pixels
数组,并更新cvImg
。带有Mat
参数的第三个是最复杂的一个。根据输入参数的通道数,该方法首先使用Imgproc.cvtColor()
函数将输入Mat
转换为 BGRA 格式的标准四色通道并存储在cvImg
中。同时,通过使用ByteBuffer b
,它将图像内容复制到内部的pixels
数组中,该数组具有 ARGB 颜色通道顺序,用于处理。剩下的三个方法将不同类型的 OpenCV Mat
返回给调用者。getGrey()
方法返回类型为CV_8UC1
的灰度图像。getBGR()
方法以 BGR 顺序返回带有 OpenCV 标准三色通道的彩色图像。getBGRA()
方法返回存储为cvImg
的彩色图像Mat
,四个通道按 BGRA 顺序排列。
为了演示它的用法,主程序将使用视频捕获类来启动网络摄像头图像流,并从CVImage
对象实例img
中获取灰度图像。灰度图像被复制回实例以供显示。处理窗口中的最终显示将是原始网络摄像头图像的灰度版本。
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(640, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width, height);
cap.start();
img = new CVImage(cap.width, cap.height);
frameRate(30);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat grey = img.getGrey();
img.copyTo(grey);
image(img, 0, 0);
text(nf(round(frameRate), 2), 10, 20);
grey.release();
}
在结束本章之前,让我们在前面的代码中再添加一个函数。您可能经常想要保存图像的内容以备后用。为此,您可以在处理中使用PImage
类中的save()
方法。save()
方法的参数是您想要保存的图像文件的完整路径名。它可以接受 TARGA、TIFF、JPEG 和 PNG 格式。每当用户按下鼠标左键时,下面的代码将把名为screenshot.jpg
的图像保存到草图的data
文件夹中:
void mousePressed() {
img.save(dataPath("screenshot.jpg"));
}
结论
本章解释了 Processing 和 OpenCV 中不同的图像表示。通过以下练习,您已经掌握了在处理环境中创建和操作图像的基本技能。您学习了如何在 Processing 和 OpenCV 之间转换图像。上一个练习中定义的类将构成本书的基础,让您学习 OpenCV 和处理,而不必回到与格式转换相关的繁琐细节。在下一章,你将开始操作图像的单个像素来生成有创意的图片。
三、基于像素的操作
本章介绍了处理单个像素颜色值的不同方法,从而为图像创建有趣的效果。您将学习如何以算法和交互的方式处理单个像素。在这一章中,你将只关注改变像素的颜色值,而不是它们在图像中的位置和总数。在学习图像处理的技术细节之前,本章还将介绍艺术和设计中常用的基本图形属性。本章将涵盖以下主题:
- 视觉属性
- 像素颜色处理
- 随机性
- 用现有图像绘图
- 混合多个图像
视觉属性
在视觉艺术和设计专业,你学习如何创造视觉材料,并把它们组合起来。对于任何视觉材料,您通常可以用以下内容来描述其属性:
- 位置
- 大小
- 形状
- 方向
- 颜色
- 价值
在经典著作《图形符号学》中,Jacque Bertin 使用术语视网膜变量来描述视觉元素的相似属性。让我们浏览一下这些属性,看看它们中是否有任何一个与像素颜色处理的讨论有关。
位置
图像中的每个像素都有一个位置。如图 3-1 所示,测量的原点在左上角,而不是你可能在学校学过的左下角。水平位置是 x 轴,其值随着向右侧移动而增加。垂直位置是 y 轴,其值随着向底部移动而增加。
图 3-1。
Pixel position in an image
您可以根据像素在图像中的位置来更改像素颜色信息,以实现渐变效果。图 3-2 显示了一个典型的例子。
图 3-2。
Gradation effect
大小
像素没有任何大小信息。准确地说,每个像素的大小为 1 乘 1。您可以想象通过将像素周围的相邻像素更改为相同的颜色来增加像素的大小。这就是你大概熟悉的马赛克效果,如图 3-3 。
图 3-3。
Mosaic effect
形状
很难描述像素的形状。事实上,由于它的大小只有一个像素,所以描述像素的形状是没有意义的。从概念上讲,在矩形网格表示中,您可以将像素视为一个微小的正方形或圆形。
方向
如果一个像素没有确定的形状,你就不能描述它的方向(即,在二维平面上的旋转量)。但是,您可以从整体上描述数码图像的方向/旋转。在这种情况下,您正在转换图像中像素的位置,这将是下一章的主题。
颜色
像素的颜色信息是本章的主要内容。你将会看到如何用不同的方式改变颜色。您可以使用颜色来传达图像中的信息。如果两幅图像有两种不同的纯色,你很容易断定它们是不同的。如图 3-4 所示,如果你随意选择颜色,你无法立即分辨哪个“高”哪个“低”。
图 3-4。
Color difference
价值
该值有时被称为颜色的强度或亮度。如果你只是使用灰度图像,这更有意义,如图 3-5 所示。与任意使用颜色不同,它创建了一个比较来提示订单信息。例如,使用深灰色和浅灰色可能暗示重量比较,以表明某人较重或较轻。在处理中,除了使用 RGB 作为默认的颜色表示外,还可以使用 HSB(色调、饱和度、亮度)。在这种情况下,如果保持色调和饱和度不变,只改变亮度,就可以创建一个表示顺序的比较。
图 3-5。
Grayscale image with value comparison
像素颜色处理
在前一章中,您在处理中使用了PImage
的get()
和set()
方法来获取和更新像素颜色信息。PImage
对象有一个内部数组来存储每个像素的颜色信息。在这一章中,我将介绍一种更新PImage
对象的内部数组pixels[]
的直接方法。在第一个练习Chapter03_01
中,草图通过使用pixels[]
数组将所有像素改变为一种颜色来创建一个纯色图像。
PImage img;
void setup() {
size(750, 750);
background(0);
img = createImage(width, height, ARGB);
noLoop();
}
void draw() {
img.loadPixels();
color orange = color(255, 160, 0);
for (int i=0; i<img.pixels.length; i++) {
img.pixels[i] = orange;
}
img.updatePixels();
image(img, 0, 0);
}
注意,代码使用了一个名为img
的PImage
对象实例。它是使用该设置中的createImage()
功能创建的。您还可以使用noLoop()
功能运行一次draw()
功能,而不会循环。在draw()
函数中,使用loadPixels()
方法将图像数据加载到img
的pixels
数组中,在for
循环中更新pixels
数组元素后,使用updatePixels()
方法改变颜色。在for
循环中,使用索引i
从 0 开始执行重复,直到到达pixels
数组的长度。每个像素的颜色都与变量 orange 中定义的颜色相同。pixels[]
数组是一个整数数组,大小等于PImage
的width × height
(即图像的像素数)。每个像素是一个 32 位整数,以 ARGB 格式存储四个颜色通道。不用直接为一种颜色写一个整数,你可以使用color()
函数用四个数字指定一种颜色为color(red, green, blue, alpha)
。默认情况下,red
、green
、blue
和alpha
值都是 0 到 255 范围内的数字。
颜色随像素位置变化
在下一个练习Chapter03_02
中,您将考虑像素位置来改变其颜色。它创建的结果图像将是您在“视觉属性”一节中学到的渐变效果。
PImage img;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
noLoop();
}
void draw() {
background(0);
img.loadPixels();
float xStep = 256.0/img.width;
float yStep = 256.0/img.height;
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
for (int x=0; x<img.width; x++) {
img.pixels[rows+x] = color(x*xStep, 0, y*yStep);
}
}
img.updatePixels();
image(img, 0, 0);
}
图 3-6 显示运行加工草图的结果。
图 3-6。
Gradation image in two colors, red and blue
在上一个练习中,您根据像素位置的线性变化更改了颜色 RGB 分量。然而,你可以尝试另一种非线性的方法来观察差异。在下面的练习Chapter03_03
中,您可以看到这种方法的演示:
PImage img;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
noLoop();
}
void draw() {
background(0);
img.loadPixels();
float colStep = 256.0/colFunc(img.height);
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
color col = color(colFunc(y)*colStep);
for (int x=0; x<img.width; x++) {
img.pixels[rows+x] = col;
}
}
img.updatePixels();
image(img, 0, 0);
}
float colFunc(float v) {
return v;
}
在第一个版本中,您使用线性颜色渐变,它实际上是 y 轴上的灰度渐变。为了灵活起见,您使用一个名为colFunc()
的独立函数来计算像素的颜色变化和y
位置之间的关系。在第一次运行时,您只需返回y
位置值作为函数的输出。在draw()
函数中,通过除以 256 来定义变量colStep
,256 是灰度的最大值colFunc(img.height)
,最大值来自colFunc()
。在 y 轴的for
循环的每一步中,颜色变量col
通过将colFunc(y)
乘以colStep
值来计算。在这种情况下,当y
位置为 0 时col
的最小值为 0,当y
位置为img.height - 1
时col
的最大值为 255。图 3-7 显示运行加工草图的结果。
图 3-7。
Grayscale gradation with linear function
在第二个版本中,您可以通过返回v
的平方来修改colFunc()
函数,如下所示:
float colFunc(float v) {
return v*v;
}
图 3-8 显示了该版本根据y
位置灰度非线性变化的结果。
图 3-8。
Grayscale gradation with nonlinear change, y-square
在本练习的最后一个版本中,您用一个更一般的数学函数代替了colFunc()
函数,这个函数叫做非整数值的pow()
。你可以尝试,例如,把参数v
的 1.5 次方。新的colFunc()
定义如下:
float colFunc(float v) {
return (float) Math.pow(v, 1.5);
}
图 3-9 包含在这里,以便您可以与最后两个进行比较。
图 3-9。
Grayscale gradation with nonlinear change, y to the power of 1.5
颜色随像素距离变化
除了根据像素的位置更改颜色值,您还可以根据像素与屏幕上另一个位置的距离更改颜色值。在以下练习中,您将尝试不同的距离函数和位置,并查看结果。首先,试着用这个练习来比较一个像素到图像中心的距离,Chapter03_04
:
PImage img;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
noLoop();
}
void draw() {
background(0);
img.loadPixels();
float colStep = 256.0/max(img.width/2, img.height/2);
PVector ctr = new PVector(img.width/2, img.height/2);
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
for (int x=0; x<img.width; x++) {
float d = distance(ctr, new PVector(x, y));
color col = color(d*colStep, 0, 255-d*colStep);
img.pixels[rows+x] = col;
}
}
img.updatePixels();
image(img, 0, 0);
}
float distance(PVector p1, PVector p2) {
float d = abs(p1.x-p2.x) + abs(p1.y-p2.y);
return d;
}
请注意在名为distance()
的程序中使用了自定义距离函数。它有两个类型为PVector
( https://processing.org/reference/PVector.html
)的参数,这是一个在处理中很有用的类,可以简化矢量计算的使用。一个PVector
有三个属性:x
、y
和z
。这些对应于三维空间中的位置。在本练习中,您仅使用 2D 图形中的x
和y
。该版本的距离函数使用两点的x
和y
位置之间差值的绝对值之和。在draw()
函数中,您计算每个像素到图像中心的距离,并使用它来计算红色和蓝色分量。图 3-10 显示运行加工草图的结果。
图 3-10。
Color change with distance from center
您可以修改distance()
函数,使用更常见的欧几里德距离来测试结果。以下是distance()
函数的新定义:
float distance(PVector p1, PVector p2) {
float d = p1.dist(p2);
return d;
}
它采用PVector
内置的dist()
方法来计算二维空间中两点之间的距离。最终的图像(如图 3-11 所示)将看起来像一个圆形而不是菱形。
图 3-11。
Color change with distance from center
在下一个练习Chapter03_05
中,您将增强draw()
函数中的计算,这样您就可以在处理中利用一些好的特性来简化代码:
PImage img;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
noLoop();
}
void draw() {
background(0);
img.loadPixels();
float distMax = max(img.width/2, img.height/2);
PVector ctr = new PVector(img.width/2, img.height/2);
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
for (int x=0; x<img.width; x++) {
float d = distance(ctr, new PVector(x, y));
float c = map(d, 0, distMax, 0, 255);
color col = color(c, 0, 255-c);
img.pixels[rows+x] = col;
}
}
img.updatePixels();
image(img, 0, 0);
}
float distance(PVector p1, PVector p2) {
float d = p1.dist(p2);
return d;
}
使用的新功能是map()
功能。它接受示例中的变量d
,并将其从源范围 0 到distMax
映射到目标范围 0 到 255。它简化了许多应用的线性映射计算。
该程序的一个快速变化是向变量ctr
引入交互性。想象一下,如果它能跟随鼠标的移动;你可以通过使用mouseX
和mouseY
变量来生成它的交互版本。
PImage img;
float distMax;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
img.loadPixels();
distMax = max(img.width, img.height);
}
void draw() {
background(0);
PVector ctr = new PVector(mouseX, mouseY);
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
for (int x=0; x<img.width; x++) {
float d = distance(ctr, new PVector(x, y));
float c = map(d, 0, distMax, 0, 255);
color col = color(c, 0, 255-c);
img.pixels[rows+x] = col;
}
}
img.updatePixels();
image(img, 0, 0);
}
float distance(PVector p1, PVector p2) {
float d = p1.dist(p2);
return d;
}
用三角函数改变颜色
三角函数指的是你在学校学过的正弦、余弦、正切函数。正弦和余弦函数的输出值具有周期性。Processing 内置了从 Java 中采用的sin()
和cos()
函数。它们有一个输入值,以弧度为单位。输入值的正常范围是在-PI 到 PI 的范围内,以完成一个周期。两个函数的输出范围都在-1 到 1 的范围内。在下一个练习Chapter03_07
中,将输入范围(即像素和鼠标位置之间的距离)映射到-PI 和 PI 之间,同时将-1 到 1 之间的输出范围映射到 0 到 255 之间的颜色范围。
PImage img;
float num;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
img.loadPixels();
num = 8;
}
void draw() {
background(0);
PVector mouse = new PVector(mouseX, mouseY);
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
for (int x=0; x<img.width; x++) {
PVector dist = distance(mouse, new PVector(x, y));
float xRange = map(dist.x, -img.width, img.width, -PI*num, PI*num);
float yRange = map(dist.y, -img.height, img.height, -PI*num, PI*num);
float xCol = map(cos(xRange), -1, 1, 0, 255);
float yCol = map(sin(yRange), -1, 1, 0, 255);
color col = color(xCol, 0, yCol);
img.pixels[rows+x] = col;
}
}
img.updatePixels();
image(img, 0, 0);
}
PVector distance(PVector p1, PVector p2) {
return PVector.sub(p1, p2);
}
您修改distance()
函数来返回一个PVector
,存储两个输入向量的相减结果。在输入范围中,还引入了一个新变量num
,来扩展原来的范围(-PI,PI)。图像将由更多的重复组成。图 3-12 显示了试运行的结果。
图 3-12。
Color change with trigonometric functions
为了增强图像的复杂性,您可以简单地将x
和y for
循环变量添加到输入范围计算中,如下一个练习Chapter03_08
所示。同时,您需要减少变量num
的值,这样xRange
和yRange
的值就不会变得太大。
PImage img;
float num;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
img.loadPixels();
num = 0.1;
}
void draw() {
background(0);
PVector mouse = new PVector(mouseX, mouseY);
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
for (int x=0; x<img.width; x++) {
PVector dist = distance(mouse, new PVector(x, y));
float xRange = map(dist.x, -img.width, img.width, -PI*num*y, PI*num*x);
float yRange = map(dist.y, -img.height, img.height, -PI*num*x, PI*num*y);
float xCol = map(cos(xRange), -1, 1, 0, 255);
float yCol = map(sin(yRange), -1, 1, 0, 255);
color col = color(xCol, 0, yCol);
img.pixels[rows+x] = col;
}
}
img.updatePixels();
image(img, 0, 0);
}
PVector distance(PVector p1, PVector p2) {
return PVector.sub(p1, p2);
}
这将产生一种更迷幻的效果,类似于 20 世纪 60 年代和 70 年代常见的光学艺术图形,如图 3-13 所示。因为在计算xRange
和yRange
值时包含了x
和y
值,所以结果不太容易预测。
图 3-13。
Another example with trigonometric function
在下一个练习Chapter03_09
中,您将简化distance()
函数,并在draw()
函数中仅使用一个值dist
来生成正弦和余弦函数的输入范围。修改不是实质性的,但视觉效果与之前的有很大不同。
PImage img;
float num;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
img.loadPixels();
num = 2;
}
void draw() {
background(0);
PVector mouse = new PVector(mouseX, mouseY);
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
for (int x=0; x<img.width; x++) {
float dist = distance(mouse, new PVector(x, y));
float range = map(dist, -img.width, img.width, -PI*num*y, PI*num*x);
float xCol = map(sin(range), -1, 1, 0, 255);
float yCol = map(cos(range), -1, 1, 0, 255);
color col = color(0, 255-xCol, yCol);
img.pixels[rows+x] = col;
}
}
img.updatePixels();
image(img, 0, 0);
}
float distance(PVector p1, PVector p2) {
return p1.dist(p2);
}
xCol
和yCol
颜色变量共享相同的输入范围值,但使用不同的三角函数。视觉结果可能类似于一个圆,因为您知道圆可以表示如下:
x = radius * cos(angle)
y = radius * sin(angle)
在本练习中,图像要复杂得多,因为这里表示为angle
的输入范围不仅仅是从-PI 到 PI。图 3-14 显示运行程序后的图像。
图 3-14。
Color change with more trigonometric function
在图像处理中使用三角函数会给你带来很多乐趣。请随意探索更多的变化。在下一节,我将开始解释随机性的想法如何帮助你生成有趣的图像。
随机性
处理提供了一个基于java.util.Random
类的随机数生成器。您可以使用random()
功能创建各种类型的随机彩色图像。在下一个练习Chapter03_10
中,你将使用随机数来填充灰度图像:
PImage img;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
img.loadPixels();
noLoop();
}
void draw() {
background(0);
for (int i=0; i<img.pixels.length; i++) {
img.pixels[i] = color(floor(random(0, 256)));
}
img.updatePixels();
image(img, 0, 0);
}
draw()
函数中的for
循环遍历PImage
中的所有像素,并使用函数random(0, 256)
将颜色设置为 0 到 255 之间的随机值。结果图像完全混乱,没有任何可识别的图案,如图 3-15 所示。
图 3-15。
Random grayscale image
如果您希望创建一个更具随机性的视觉愉悦的图像,可以通过在颜色信息中施加规则来降低随机性的程度。下一个练习Chapter03_11
,将使用随机灰色调初始化图像中的第一个像素。下一个像素将增加或减少随机部分的灰度值。比较两个结果,看看第二个版本中是否有任何模式。
PImage img;
float value1;
float range;
void setup() {
size(750, 750);
img = createImage(width, height, ARGB);
img.loadPixels();
value1 = floor(random(0, 256));
range = 50;
noLoop();
}
void draw() {
background(0);
for (int i=0; i<img.pixels.length; i++) {
float v = random(-range, range);
value1 += v;
value1 = constrain(value1, 0, 255);
img.pixels[i] = color(value1);
}
img.updatePixels();
image(img, 0, 0);
}
代码基本上使用了random(-range, range)
语句在draw()
函数中引入了一个受控版本的随机性。图像将由随机的灰色调像素组成,但随机性被控制在一个较小的范围内,同时依赖于前一个像素,如图 3-16 所示。
图 3-16。
Random grayscale image with patterns
由于像素颜色信息依赖于最后一个,具有一定程度的随机性,您可以很容易地识别图像的水平纹理,因为阵列中像素的排列首先按行顺序排序。
下一个练习Chapter03_12
,在处理中使用noise()
函数来探索随机性。这是肯·柏林开发的柏林噪声函数。该函数的输出显示了一个更自然、更平滑的数字序列。处理提供了多达三维的柏林噪声函数。在本练习中,您将使用二维版本的噪波值用灰色调填充图像。
PImage img;
float xScale, yScale;
void setup() {
size(750, 750);
background(0);
img = createImage(width, height, ARGB);
img.loadPixels();
xScale = 0.01;
yScale = 0.01;
noLoop();
}
void draw() {
for (int y=0; y<img.height; y++) {
int rows = y*img.width;
for (int x=0; x<img.width; x++) {
img.pixels[rows+x] = color(floor(noise(x*xScale, y*yScale)*256));
}
}
img.updatePixels();
image(img, 0, 0);
}
请注意,对于像素的x
和y
位置,您使用xScale
和yScale
变量来缩小范围,以实现图像中更平滑的噪声效果,如图 3-17 所示。
图 3-17。
Grayscale color with Perlin noise
到目前为止,您已经使用算法的方式为每个像素填充颜色,创建了一个图像。您还学习了如何创建像素中带有随机颜色的图像。在下一节中,您将导入一个现有的图像,并使用前面几节中的步骤来处理像素颜色。
用现有图像绘图
下一个练习Chapter03_13
,使用现有图像将彩色图像转换为灰色调。当然,您可以使用 Processing 和 OpenCV 的内置函数来进行转换。您可以将此练习作为起点,学习如何编写简单的图像处理代码。
PImage img1, img2;
void setup() {
size(1500, 750);
background(0);
img1 = loadImage("landscape.png");
img1.loadPixels();
img2 = createImage(img1.width, img1.height, ARGB);
img2.loadPixels();
noLoop();
}
void draw() {
for (int i=0; i<img1.pixels.length; i++) {
color col = img1.pixels[i];
img2.pixels[i] = color((red(col) + green(col) + blue(col))/3);
}
img2.updatePixels();
image(img1, 0, 0);
image(img2, img1.width, 0);
}
在程序中,您将处理窗口定义为照片宽度的两倍,以便并排显示原始图像和修改后的图像。您使用两个PImage
变量。第一个是img1
,加载外部图像。第二个,img2
,使用红色、绿色和蓝色的简单平均,将第一个的颜色像素转换成单一的灰色调。图 3-18 显示了转换过程。
图 3-18。
Color to grayscale conversion with simple averaging
还有另一种方法可以从原始 RGB 图像计算灰度图像的亮度。视觉不会检测到强度相等的 RGB。在本练习版本Chapter03_14
中,您将使用以下公式得出亮度值:
img2.pixels[i] = color(0.2*red(col) + 0.7*green(col) + 0.1*blue(col));
图 3-19 显示了用于比较的结果图像。
图 3-19。
Color to grayscale conversion with relative luminance
在下一个练习Chapter03_15
中,您将编写一个反转滤镜来反转原始彩色图像的所有红色、绿色和蓝色通道。为了达到这种效果,您使用 255 并减去所有三个颜色通道值。公式如下:
img2.pixels[i] = color(255-red(col), 255-green(col), 255-blue(col));
图 3-20 显示了结果图像。
图 3-20。
Color change with inverse effect
您也可以交换三个颜色通道,以不同的顺序混合它们,以获得 Photoshop 中可以找到的其他效果。下面是一个例子,Chapter03_16
,它交换了三个通道的顺序,并反转了原来的红色通道:
img2.pixels[i] = color(blue(col), 255-red(col), green(col));
图 3-21 显示了使用相同图像的输出。
图 3-21。
Color change by swapping different color channels
处理有一个filter()
功能(
https://processing.org/reference/filter_.html
,它提供了许多图像处理预设,如下所示:
THRESHOLD
GRAY
OPAQUE
INVERT
POSTERIZE
BLUR
ERODE
DILATE
除了这些预设,您还可以实现自己的预设。以下练习将说明如何基于现有图像在画布上进行绘制。你要测试的第一个预设是 Photoshop 中的马赛克效果。马赛克效果实质上是在保持图像尺寸的同时降低图像分辨率。让我们来看看这个练习的代码,Chapter03_17
:
PImage img;
int step;
void setup() {
size(1500, 750);
background(0);
img = loadImage("landscape.png");
img.loadPixels();
step = 10;
noStroke();
noLoop();
}
void draw() {
for (int y=0; y<img.height; y+=step) {
int rows = y*img.width;
for (int x=0; x<img.width; x+=step) {
color col = img.pixels[rows+x];
fill(col);
rect(x+img.width, y, step, step);
}
}
image(img, 0, 0);
}
注意,在嵌套的for
循环中,你不需要遍历每一个像素。相反,您用变量step
中的一个值来增加索引。然后对这些像素的颜色进行采样,并将其用作正方形的fill()
颜色。图 3-22 显示了原始照片和拼接图像。
图 3-22。
Mosaic effect example
如果将rect()
命令替换为ellipse()
命令,可以实现圆形镶嵌效果,如图 3-23 所示。
图 3-23。
Mosaic effect with circles
前两个练习对矩形和圆形使用fill()
颜色。如果你使用stroke()
颜色来绘制线条,你可以对同一张照片进行不同的渲染,类似于图 3-24 。
图 3-24。
Mosaic effect with short line segments
在draw()
函数中,您使用随机机制floor(random(2))
来选择线段的绘制方向。其结果将是 0 或 1。您可以用它来确定对角线线段的方向。
PImage img;
int step;
void setup() {
size(1500, 750);
background(0);
img = loadImage("landscape.png");
img.loadPixels();
step = 10;
smooth();
noFill();
noLoop();
}
void draw() {
for (int y=0; y<img.height; y+=step) {
int rows = y*img.width;
for (int x=0; x<img.width; x+=step) {
color col = img.pixels[rows+x];
stroke(col);
int num = floor(random(2));
if (num == 0) {
line(x+img.width, y, x+img.width+step, y+step);
} else {
line(x+img.width+step, y, x+img.width, y+step);
}
}
}
image(img, 0, 0);
}
下一个练习Chapter03_20
,探索图像处理中常见的条形码效应。Irma Boom 等著名设计师也对旧的经典画作进行了采样,并用垂直色条来表示,类似于您在本练习中计划做的事情。首先,你拍摄一张彩色照片,并在照片中间添加一条水平线(图 3-25 )。
图 3-25。
Sample photograph with a horizontal line
沿着水平线,对线上每个像素进行采样,并检索其颜色值。通过使用颜色值,您可以沿着水平线为每个像素绘制一条垂直线。代码如下:
PImage img;
void setup() {
size(1200, 900);
background(0);
img = loadImage("christmas.png");
img.loadPixels();
noFill();
noLoop();
}
void draw() {
int y = img.height/2;
for (int x=0; x<img.width; x++) {
color c = img.pixels[y*img.width+x];
stroke(c);
line(x, 0, x, img.height-1);
}
}
这个程序很简单。视觉结果是原始照片的条形码表示,如图 3-26 所示。
图 3-26。
Barcode effect example
您可以通过将变量y
改为mouseY
并删除noLoop()
函数来试验这个程序的交互版本。在这种情况下,结果是仅由一张照片生成一个美丽的动画。
到目前为止,您已经探索了基于现有图像创建新图像的各种方法,或者通过替换像素颜色,或者通过在画布上参照像素颜色进行绘制。在下一节中,您将学习如何组合两幅图像。
混合多个图像
处理有一个blend()
功能( https://processing.org/reference/blend_.html
),它提供了许多选项来组合两幅图像。工作机制类似于 Photoshop 中的图层选项。本节将不详细解释每个选项。本节中的练习将说明组合两个图像的基本逻辑。下面的练习Chapter03_21
演示了blend()
功能在选项ADD
处理中的使用:
PImage img1, img2;
void setup() {
size(1200, 900);
background(0);
img1 = loadImage("hongkong.png");
img2 = loadImage("sydney.png");
noLoop();
}
void draw() {
img1.blend(img2, 0, 0, img2.width, img2.height,
0, 0, img1.width, img1.height, ADD);
image(img1, 0, 0);
}
您有两个PImage
实例,img1
和img2
,每个实例都从data
文件夹加载了一个外部图像。在draw()
函数中,img2
实例将通过img1.blend()
方法融合到img1
实例中。其余的参数是源偏移(x, y
)和尺寸(width, height
)、目标偏移(x, y
)和尺寸(width, height
)以及混合选项ADD
。注意,在blend()
功能之后,img1
的内容将会改变。练习中使用的两幅图像大小相同(1200×900 像素)。然而,这里的blend()
功能将改变img2
的分辨率,如果两幅图像的尺寸不同。图 3-27 显示了结果图像。
图 3-27。
Blending two images with the ADD option
您也可以使用自己的代码在处理过程中执行这种混合效果。对于ADD
选项,您可以将两幅图像中的两个像素颜色分量相加。因为 RGB 的有效范围是 0 到 255,所以您可以将值限制在此范围内。这里是练习的来源,Chapter03_22
。在这个版本中,假设两个图像具有相同的大小(1200×900 像素)。
PImage img1, img2, img3;
void setup() {
size(1200, 900);
background(0);
img1 = loadImage("hongkong.png");
img2 = loadImage("sydney.png");
img3 = createImage(img1.width, img1.height, ARGB);
noLoop();
}
void draw() {
for (int i=0; i<img1.pixels.length; i++) {
color c1 = img1.pixels[i];
color c2 = img2.pixels[i];
float r = constrain(red(c1) + red(c2), 0, 255);
float g = constrain(green(c1) + green(c2), 0, 255);
float b = constrain(blue(c1) + blue(c2), 0, 255);
img3.pixels[i] = color(r, g, b);
}
img3.updatePixels();
image(img3, 0, 0);
}
逻辑很简单。draw()
函数有一个for
循环来遍历img1
和img2
中的所有像素。三种颜色分量相加在一起,并限制在 0 到 255 的范围内。第三个PImage
实例img3
,存储所有新的像素颜色值,并在屏幕上显示图像。
作为演示,本节的最后一个练习Chapter03_23
,也将展示一个在 OpenCV 中完成的版本。要在处理环境中使用 OpenCV,记得将code
文件夹复制到你的 sketch 文件夹中,并在一个新的选项卡中重新创建CVImage
类,如前一章所示。您将创建三个CVImage
类的实例来维护hongkong.png
、sydney.png
和生成的图像。这里显示了主程序的示例代码。同样,假设两个源图像具有相同的大小(1200×900 像素)。
CVImage img1, img2, img3;
void setup() {
size(1200, 900);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
background(0);
PImage tmp = loadImage("hongkong.png");
img1 = new CVImage(tmp.width, tmp.height);
img2 = new CVImage(tmp.width, tmp.height);
img3 = new CVImage(tmp.width, tmp.height);
img1.copyTo(tmp);
tmp = loadImage("sydney.png");
img2.copyTo(tmp);
noLoop();
}
void draw() {
Mat m1 = img1.getBGR();
Mat m2 = img2.getBGR();
Mat m3 = new Mat(m1.size(), m1.type());
Core.add(m1, m2, m3);
img3.copyTo(m3);
image(img3, 0, 0);
m1.release();
m2.release();
m3.release();
}
程序的主要命令是Core.add()
功能。它将前两个源矩阵与第三个源矩阵相加作为目标矩阵。它也依赖于你在前一章开发的CVImage
类。图像对象使用copyTo()
和getBGR()
方法在处理格式和 OpenCV 格式之间转换。
结论
本章通过改变单个像素的颜色来介绍图像处理的基本任务。您现在了解了如何实现简单的图像滤镜,如灰度和反转滤镜。您还了解了如何从头开始创建图形图像,以及如何修改现有图像以进行创造性输出。在下一章中,你将改变像素的位置,这样你可以获得更动态的图像处理效果。