使用 Atalasoft DotImage 有效处理多张图像





0/5 (0投票)
2006年5月2日
8分钟阅读

31560
Atalasoft DotImage 包含许多用于处理或分析图像的工具。其中包含一个 ImageSource 类,该类允许在不担心图像来源和管理细节的情况下,有效地处理任意数量的图像。请继续阅读...
这是我们CodeProject赞助商的展示评论。这些评论旨在为您提供我们认为对开发人员有用且有价值的产品和服务信息。
摘要
Atalasoft DotImage 包含许多用于处理或分析图像的工具。该套件中包含一个名为 ImageSource
的类,它是能够有效处理任意数量图像的基础,而无需担心图像的来源和管理细节。本文档将详细介绍 ImageSource
类及其派生类。
在批量图像处理或图像处理应用程序中,经常需要处理大量图像。虽然可以编写代码来处理特定情况,但通常更方便的是能够以更通用的方式解决此问题,然后将其作为更具体情况的基础。
在 DotImage 中,抽象类 ImageSource
正是为此目的而设计的。ImageSource
的思考方式是将其视为一个源/汇对的一半。ImageSource
是图像的来源。汇是应用程序或图像消费者。图像通过获取/释放模型进行管理。ImageSource
对象执行以下服务:
- 允许按顺序获取图像。
- 允许按任意顺序释放图像。
- 跟踪已提供图像所占用的内存。
- 使用延迟或积极的机制自动释放已释放的图像。
- 允许有限地重新获取已释放的图像。
- 允许通过重新加载机制缓存图像。
在此模型中,可以将图像视为资源。图像不仅仅是被读取和使用,而是从 ImageSource
获取,并在完成后释放。任何数量的消费者都可以获取任何给定的图像,并且只有当每个 Acquire
都与一个 Release
相匹配时,该图像才会被释放。
这样,ImageSource
可以按以下方式使用:
public void ProcessImages(ImageSource source)
{
while (source.HasMoreImages()) {
AtalaImage image = source.AcquireNext();
ProcessImage(image);
source.Release(image);
}
}
已获取但尚未释放的图像可以被获取任意次数。在上面的示例中,ImageSource
中的所有图像都是串行处理的。可以通过创建工作线程来执行处理,并允许它们获取和释放图像,从而轻松地使其成为并行过程。将代码组织如下可以实现这一点:
public void ProcessImages(ImageSource source)
{
while (source.HasMoreImages()) {
AtalaImage image = source.AcquireNext();
CreateImageWorker(source, image, ProcessImage);
source.Release(image);
}
}
private void ProcessImage(ImageSource source, AtalaImage image)
{
// do processing here
source.Release(image);
}
public delegate void ProcessImageProc(ImageSource source,
AtalaImage image);
public void CreateImageWorker(ImageSource source,
AtalaImage image, ProcessImageProc proc)
{
source.Acquire(image); // Acquire here
Thread t = CreateImageWorkerThread(source, image, proc);
t.Start();
}
private Thread CreateImageWorkerThread(ImageSource source,
AtalaImage image, ProcessImageProc proc)
{
// threading details left out
}
在此代码中,主循环获取每个图像,将其传递给 CreateImageWorker
,然后释放它。CreateImageWorker
调用 Acquire
(现在是第二次),然后创建一个工作线程来执行处理,启动它,然后返回。工作线程调用 ProcessImage
来完成工作,然后再调用 Release
。这样,图像就会以并行方式进行处理。
ImageSource
将图像分为三组:Acquired
(已获取)、Released
(已释放)和 Culled
(已剔除)。`Acquired` 状态的图像在内存中,可供使用。`Released` 状态的图像在内存中,但在重新获取之前不应使用。`Culled` 状态的图像已不在内存中,但可能具有重新加载的功能。
例如,此代码将始终有效:
TryOne(ImageSource source)
{
source.Reset();
AtalaImage image = source.AcquireNext();
AtalaImage image1 = source.Acquire(0); // reacquire the 0th image
}
如果 image
非 null
,则 image1
始终非 null
且与 image
相同。
此代码在大多数情况下都有效:
TryTwo(ImageSource source)
{
source.Reset();
AtalaImage image = source.AcquireNext();
source.Release(image);
AtalaImage image1 = source.Acquire(0); // reacquire the 0th image
}
ImageSource
会将 image
标记为 `Released`,并且除非存在严重的内存限制,否则可以重新获取该图像。但应检查结果图像是否为 null
。
此代码仅在特定的 ImageSource
实现可重新加载图像时才可靠有效:
TryThree(ImageSource source)
{
source.Reset();
while (source.HasMoreImages()) {
AtalaImage image = source.AcquireNext();
source.Release(image);
}
// reacquire the 0th image
AtalaImage image1 = source.Acquire(0);
}
重新加载图像的能力不是在 ImageSource
中定义的,而是留给继承自 ImageSource
的类。ImageSource
本身非常适合只需要访问一次且仅访问一次图像的情况,例如视频源或带自动送纸器的扫描仪。
由于并非所有 ImageSource
都属于此类,因此有一个名为 RandomAccessImageSource
的 ImageSource
的抽象子类。对于 RandomAccessImageSource
,可以随时以任何顺序可靠地获取任何图像。同样,图像可以被 `Acquired`、`Released` 和 `Culled`,但在这种情况下,`Acquire` 应该始终成功。
RandomAccessImageSource
为对象添加了数组运算符和 Count
属性。这样,就可以按以下方式访问图像源:
public void ProcessImages(RandomAccessImageSource source)
{
for (int i=0; i < source.Count; i++) {
AtalaImage image = source[i]; // this does the acquire
ProcessImage(image);
source.Release(image);
}
}
从这里开始,距离主要具体的 ImageSource
类 FileSystemImageSource
就很近了。FileSystemImageSource
允许客户端迭代一组图像文件以及支持多帧的图像文件中的多个帧。由于它显然是 ImageSource
的一种变体,可以轻松地重新加载图像,因此它继承自 RandomAccessImageSource
。按照设计,FileSystemImageSource
可以遍历文件夹中的所有图像文件、文件夹中匹配特定模式的所有文件,或者通过文件列表进行遍历。可选地,FileSystemImageSource
还会跨所有帧进行迭代。
无论好坏,模式匹配仅限于 .NET 为文件提供的匹配,而不是完整的正则表达式匹配。一方面,这与通用的 Windows 用户界面一致;另一方面,它有些局限。
为了避免这种固有的限制,同时保持兼容性,FileSystemImageSource
包括一个文件过滤器钩子,允许客户端执行所有图像文件的过滤。通过将 FileFilterDelegate
属性设置为一个方法,其形式为:
bool MyFilter(string path, int frameIndex, int frameCount)
{
}
客户端可以根据自己的标准允许或拒绝任何文件。通过从 FileFilterDelegate
返回 true
,文件或文件中的帧将被包含。返回 false
将忽略该文件或帧。
要实现自定义 ImageSource
,请创建一个继承自 ImageSource
或 RandomAccessImageSource
的类。继承自 ImageSource
的类声明它可以按顺序提供图像序列。为此,类必须实现以下 abstract
方法:
protected abstract ImageSourceNode LowLevelAcquireNextImage();
LowLevelAcquireNextImage
获取序列中的下一个可用图像,并将其打包在 ImageSourceNode
中返回。ImageSourceNode
用于管理内存中的图像。ImageSourceNode
的主构造函数接受 AtalaImage
和实现 IImageReloader
接口的对象作为参数。IImageReloader
是一个类,可以使图像重新加载到内存中。对于典型的继承自 ImageSource
的类,LowLevelAcquireNextImage()
将简单地返回一个带有有效图像的新 ImageSourceNode
,但 IImageReloader
为 null。这表示一旦图像被从内存中剔除,就无法重新加载。如果无法获取下一个图像,LowLevelAcquireNextImage
应该返回 null
。
protected abstract bool LowLevelHasMoreImages();
LowLevelHasMoreImages
返回一个布尔值,指示是否有更多图像需要加载。
protected abstract void LowLevelReset();
LowLevelReset
用于将 ImageSource
返回到其起始状态(如果可能)。对于某些 ImageSource
,这并非总是可能的。如果不可能 Reset
,此方法应不执行任何操作。
protected abstract void LowLevelSkipNextImage();
LowLevelSkipNextImage
在先前加载的图像仍可用时被调用。例如,如果 ImageSource
需要加载图像,它将调用 LowLevelAcquireNext
,但如果它确定加载图像不是必需的,则不会调用 LowLevelAcquireNext
。在这种情况下,有必要允许类维护其记账。
protected abstract void LowLevelDispose();
LowLevelDispose
在类被垃圾回收时被调用,以允许类处理任何不可回收的资源。这可能包括关闭文件、释放设备、关闭网络连接等。
protected abstract bool LowLevelFlushOnReset();
LowLevelFlushOnReset
指示 ImageSource
在 Reset
时是否应清除所有缓存的图像。对于每次都不会返回相同图像序列的 ImageSource
变体,此方法应返回 true
。通常,大多数类将返回 false
以充分利用缓存。
protected abstract bool LowLevelTotalImagesKnown();
LowLevelTotalImagesKnown
如果此 ImageSource
可以预先知道有多少可用图像,则返回 true
,否则返回 false
。
protected abstract int LowLevelTotalImages();
LowLevelTotalImages
返回可用图像的总数。如果 LowLevelTotalImagesKnown
返回 false
,则永远不会调用此方法。
RandomAccessImageSource
添加了一个新方法需要实现:
protected abstract ImageSourceNode LowLevelAcquire(int index);
LowLevelAcquire
的作用与 LowLevelAcquireNext
相同,只是它传递了一个索引。使用此方法,可以方便地根据 LowLevelAcquire
来实现 LowLevelAcquireNext
。
重要的是要注意,继承自 RandomAccessImageSource
的类在被要求加载图像时必须提供 IImageReloader
。没有它,就无法保证 ImageSource
的稳健运行。
此外,RandomAccessImageSource
实现 LowLevelTotalImagesKnown
,并返回 true
。
ImageSource
的真正强大之处在于能够创建可泛型使用的新源。下面是一个可以访问 Windows AVI 文件的 ImageSource
的完整示例。
在这个类中,我们希望能够加载 AVI 文件的每一帧。由于 AVI 文件可以在任何点读取,因此它是一个适合以 RandomAccessImageSource
作为基类的候选者,尽管普通的 ImageSource
也可以工作。
此类包含许多 PInvoke 定义,这些定义直接链接到 Win32 AVI 调用。对这些方法的解释超出了本文档的范围。
大部分工作在于打开 AVI 文件和加载帧。RandomAccessImageSource
的所有其他抽象成员都变成了单行方法。这非常好,因为它能带来高度健壮的软件。
using System;
using System.Runtime.InteropServices;
using Atalasoft.Imaging;
namespace AviSource
{
public class AviImageSource : RandomAccessImageSource
{
string _fileName;
IntPtr _aviFileHandle = IntPtr.Zero;
int _currentFrame = 0;
int _firstFramePosition;
int _totalFrames = 0;
IntPtr _aviStream = IntPtr.Zero;
AVISTREAMINFO _streamInfo = new AVISTREAMINFO();
static AviImageSource()
{
AVIFileInit();
}
public AviImageSource(string fileName)
{
_fileName = fileName;
// LowLevelReset will force the file to be loaded
// and will fetch all the relevant information
LowLevelReset();
}
protected override void LowLevelReset()
{
// attempt to load the file if we haven't
if (_aviFileHandle == IntPtr.Zero)
{
OpenAvi();
LoadAviInfo();
}
// reset the frame counter
_currentFrame = 0;
}
private void CloseAvi()
{
// clear everything out
_currentFrame = 0;
_totalFrames = 0;
// if the file handle is non-null, there may be a stream to close
if (_aviFileHandle != IntPtr.Zero)
{
// if the stream handle is non-null, close it
if (_aviStream != IntPtr.Zero)
{
AVIStreamRelease(_aviStream);
_aviStream = IntPtr.Zero;
}
AVIFileRelease(_aviFileHandle);
_aviFileHandle = IntPtr.Zero;
}
}
private void OpenAvi()
{
// open the file and get a stream interface
int result = AVIFileOpen(out _aviFileHandle, _fileName,
32 /*OF_SHARE_DENY_WRITE*/, 0);
if (result != 0)
throw new Exception("Unable to open avi file " +
_fileName + " (" + result + ")");
result = AVIFileGetStream(_aviFileHandle, out _aviStream,
0x73646976 /* 'vids' -> four char code */, 0);
if (result != 0)
throw new Exception("Unable to get video stream (" +
result + ")");
}
private void LoadAviInfo()
{
if (_aviStream == IntPtr.Zero)
throw new Exception("LoadAviInfo(): Bad stream handle.");
// get first frame
_firstFramePosition = AVIStreamStart(_aviStream);
if (_firstFramePosition < 0)
throw new Exception("LoadAviInfo():" +
" Unable to get stream start position.");
// get total frame count
_totalFrames = AVIStreamLength(_aviStream);
if (_totalFrames < 0)
throw new Exception("LoadAviInfo(): " +
"Unable to get stream length.");
// pull in general information
int result = AVIStreamInfo(_aviStream, ref _streamInfo,
Marshal.SizeOf(_streamInfo));
if (result != 0)
throw new Exception("LoadAviInfo(): unable " +
"to get stream info (" + result + ")");
}
// this method retrieves a frame from the file.
// the class is internal because it will be used by
// the AviImageReloader class.
internal AtalaImage GetAviFrame(int frame)
{
// set up a bitmap info header to make a frame request
BITMAPINFOHEADER bih = new BITMAPINFOHEADER();
bih.biBitCount = 24;
bih.biCompression = 0; //BI_RGB;
bih.biHeight = _streamInfo.frameBottom;
bih.biWidth = _streamInfo.frameRight;
bih.biPlanes = 1;
bih.biSize = (uint)Marshal.SizeOf(bih);
// the getFrameObject is an accessor for retrieving a frame
// from an AVI file. We could make exactly one when the stream
// is opened, but this works just fine.
IntPtr frameAccessor = AVIStreamGetFrameOpen(_aviStream, ref bih);
if (frameAccessor == IntPtr.Zero)
throw new Exception("Unable to get frame decompressor.");
IntPtr theFrame = AVIStreamGetFrame(frameAccessor,
frame + _firstFramePosition);
if (theFrame == IntPtr.Zero)
{
AVIStreamGetFrameClose(frameAccessor);
throw new Exception("Unable to get frame #" + frame);
}
// make a copy of this image
AtalaImage image = AtalaImage.FromDib(theFrame, true);
// closing the frame accessor drops
// the memory used by the frame as well
AVIStreamGetFrameClose(frameAccessor);
return image;
}
protected override ImageSourceNode LowLevelAcquireNextImage()
{
if (_currentFrame >= _totalFrames)
return null;
AtalaImage image = GetAviFrame(_currentFrame);
if (image != null)
{
ImageSourceNode node = new ImageSourceNode(image, null);
_currentFrame++;
return node;
}
return null;
}
protected override ImageSourceNode LowLevelAcquire(int index)
{
if (index < 0 || index >= _totalFrames)
return null;
AtalaImage image = GetAviFrame(index);
if (image != null)
{
ImageSourceNode node = new ImageSourceNode(image,
new AviImageReloader(this, index));
_currentFrame++;
return node;
}
return null;
}
protected override bool LowLevelTotalImagesKnown()
{
return true;
}
protected override int LowLevelTotalImages()
{
return _totalFrames;
}
protected override bool LowLevelHasMoreImages()
{
return _currentFrame < _totalFrames;
}
protected override void LowLevelSkipNextImage()
{
_currentFrame++;
}
protected override bool LowLevelFlushOnReset()
{
return true;
}
protected override void LowLevelDispose()
{
CloseAvi();
}
#region AviHooks
[DllImport("avifil32.dll")]
private static extern void AVIFileInit();
[DllImport("avifil32.dll", PreserveSig=true)]
private static extern int AVIFileOpen(
out IntPtr ppfile,
String szFile,
int uMode,
int pclsidHandler);
[DllImport("avifil32.dll")]
private static extern int AVIFileGetStream(
IntPtr pfile,
out IntPtr ppavi,
int fccType,
int lParam);
[DllImport("avifil32.dll")]
private static extern int AVIStreamRelease(IntPtr aviStream);
[DllImport("avifil32.dll")]
private static extern int AVIFileRelease(IntPtr pfile);
[DllImport("avifil32.dll")]
private static extern void AVIFileExit();
[DllImport("avifil32.dll", PreserveSig=true)]
private static extern int AVIStreamStart(IntPtr pAVIStream);
[DllImport("avifil32.dll", PreserveSig=true)]
private static extern int AVIStreamLength(IntPtr pAVIStream);
[DllImport("avifil32.dll")]
private static extern int AVIStreamInfo(
IntPtr pAVIStream,
ref AVISTREAMINFO psi,
int lSize);
[DllImport("avifil32.dll")]
private static extern IntPtr AVIStreamGetFrameOpen(
IntPtr pAVIStream,
ref BITMAPINFOHEADER bih);
[DllImport("avifil32.dll")]
private static extern IntPtr AVIStreamGetFrame(
IntPtr pGetFrameObj,
int lPos);
[DllImport("avifil32.dll")]
private static extern int AVIStreamGetFrameClose(IntPtr pGetFrameObj);
#endregion
}
}
除了这个类之外,还需要一个实现 IImageReloader
的类。为此,我们提供了一个 AviReloader
类,该类封装了重新加载帧文件所需的信息。在这种情况下,它是帧索引以及它来自的 AviImageSource
。AviImageSource
有一个内部方法,用于提取帧并将其转换为 AtalaImage
。为了避免保留比需要更多的信息,我们可以直接使用此方法。这假设在重新加载图像时 AVI 文件和关联的流仍然打开,但由于这在 AviImageSource
对象的整个生命周期中保持不变,因此这是一个安全的假设。
using System;
using Atalasoft.Imaging;
namespace AviSource
{
public class AviImageReloader : IImageReloader
{
private int _frame;
private AviImageSource _source;
public AviImageReloader(AviImageSource source, int frame)
{
_source = source;
_frame = frame;
}
#region IImageReloader Members
public AtalaImage Reload()
{
return _source.GetAviFrame(_frame);
}
#endregion
#region IDisposable Members
public void Dispose()
{
}
#endregion
}
}