立体摄影






4.96/5 (70投票s)
这是一个简单的程序,用于解释 3D 立体视觉系统的工作原理,也包含了一些趣味性!

目录
引言
你想找点乐子吗?如果你度过了忙碌的一天,想从工作中放松一下,那么这个就是为你准备的!
立体视觉背后的逻辑非常简单,但结果却令人惊叹和有趣!特别是当你意识到,你可以用 2D 对象轻松地制作自己的 3D 环境。
当你使用像 3D Studio 或 Maya 这样的专业 3D 软件时,你是在一个 3D 环境中工作,并在显示器上看到(通常是)二维的结果,但在这里情况不同。这意味着你是在一个二维环境中工作,但结果完全是 3D 的。
什么是立体视觉?
立体视觉是一种观看三维图像的技术;当你观看立体图时,你可以想象你正在从一个窗口观看真实的场景。尺寸、深度和距离都可以像观看原图一样感知。
如何实现?
我们的眼睛之间相隔约 6-7 厘米。这导致了每只眼睛的视角差异,因此每个场景在眼睛中的呈现方式略有不同。当这两张不同的图像在大脑中融合时,就会形成一个 3D 场景。

既然 3D 观看基于差异,你就需要拍摄两张主体照片才能重现原始场景。这些照片必须以与正常眼睛相似的配置捕获,从同一表面上的两个位置,相距约 5-10 厘米,并且相互平行。
我通过使用两个摄像头来模拟眼睛的位置和每只眼睛的视角,如下图所示。

而且,你可以在下面的图中看到结果,这两张照片略有不同。
![]() |
![]() |
左眼视角 | 右眼视角 |
它是如何工作的?
是什么逻辑让这些图像对能够让我们的 Makes 大脑感知 3D 深度?
为了找到一些原因,我想在一个真实的主题上进行工作。我准备了一对立体图像,你可以在这里看到。

然后,我将右边的图片放在左边(半透明),结果如下图所示。

正如你所见,有些形状在相同的位置。两张图片中的蘑菇都在同一个位置,但其他物体则在不同的位置。有些物体更近,有些则更远。
蘑菇位于 3D 场景的底部,因为它在两张图片中的位置相同;这意味着如果你在两张图片中看到相同位置的物体,它们就位于场景的底部。

左图中的球体与边框的距离更大(红色框显示了差异量)。当你看深度时,它会比底部物体更靠近你。

心形之间的差异比球体更大(比较红色框),所以心形比其他物体更靠近你。

那鱼呢?鱼的位置有差异,但有些不同。红色框在右图。这意味着右边的鱼比左边的鱼离边框更远。那么,在 3D 场景中会发生什么?
在这种情况下,物体被放置在底部后面;这意味着鱼是场景中最远的物体。

我们找到了逻辑。现在,我们只需在立体图像对中让它们在水平位置上产生差异,就可以制作出具有不同深度、不同物体的任何 3D 场景;这就是我在这个软件中使用的逻辑。
Using the Code
程序的算法非常简单,没有任何复杂性。如果我想用流程图来表示,它会是这样的。

现在,我将尝试解释源代码的一些部分。
在程序开始时,我声明了一些变量。
private frmBackgrounds myfrmBackgrounds = new frmBackgrounds();
private List<string> imageTypes =
new List<string>(new string[] { "*.gif", "*.png", "*.jpg", "*.bmp" });
private List<string> someWords;
private List<img> picsBackground;
private List<img> picsSmily;
private List<img> picsSimple;
private List<img> pics3DLeft;
private List<img> pics3DRight;
private class picCollection
{
internal Image myImage=null ;
internal Image myImage2=null ;
internal int myX=0;
internal int myY=0;
internal float myScale=0;
internal int myDepth=0;
}
private picCollection myPicInfo;
private List<piccollection> myAllPicCollection;
Random myRandom = new Random();
int xChange = 1;
然后,我从内部资源加载一些图片。
// add some pictures from internal resources
picsBackground.Add(Properties.Resources.back1_150_L);
picsBackground.Add(Properties.Resources.back4_100_L );
picsBackground.Add(Properties.Resources.back6_100_L );
...
还有一些文字。
// add some words
someWords = new List<string>(new string[] { "I", "You", "He", "She", "We", "They" });
someWords.AddRange(new string[] { "My", "Your", "His", "Her", "Our", "Their" });
...
我现在从硬盘添加额外的图片。
// load pictures from the hard disk
private void loadFromHard(string myDir, List<img > myPicList, List<img > myPicList2)
{
DirectoryInfo myFileDir = new DirectoryInfo(Application.StartupPath +"\\" + myDir);
if (myFileDir.Exists)
{
// For each image extension (.jpg, .bmp, etc.)
foreach (string imageType in imageTypes)
{
// all graphic files in the directory
foreach (FileInfo myFile in myFileDir.GetFiles(imageType))
{
// add image
try
{
if (myPicList2 != null)
{
Image image2 = Image.FromFile(myFile.FullName + ".right");
myPicList2.Add(image2);
}
Image image = Image.FromFile(myFile.FullName);
myPicList.Add(image);
}
catch (OutOfMemoryException)
{
continue;
}
}
}
}
}
现在,程序准备好了。你可以直接点击运行按钮,或者更改一些控件,然后按下按钮。

GUI 非常简单。

当你运行时,最初,它会声明一个图形状态。
Graphics gLeft;
gLeft = picLeft3D.CreateGraphics();
gLeft.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
gLeft.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
gLeft.Clear(lblBackgroundColor.BackColor);
...
FontStyle myFontStyle = FontStyle.Regular ;
if(chkFontBold.Checked ) myFontStyle = FontStyle.Bold;
if (chkFontItalic.Checked) myFontStyle = myFontStyle |FontStyle.Italic;
Font myFont = new Font(txtFont.Text.ToString(), (float)updFontSize.Value,
myFontStyle, GraphicsUnit.Pixel);
Brush myBrush, myBrushShadow;
...
然后,它会显示一个背景纹理或纯色。
if (chkRandomize.Checked)
{
if (rdoBackgroundPic.Checked)
{
picBackground.BackgroundImage =
picsBackground[myRandom.Next(picsBackground.Count)];
picLeft3D.BackgroundImage = picBackground.BackgroundImage;
}
else
lblBackgroundColor.BackColor = Color.FromArgb(myRandom.Next(255),
myRandom.Next(255), myRandom.Next(255));
}
else
{
if (rdoBackgroundPic.Checked)
picLeft3D.BackgroundImage = picBackground.BackgroundImage;
}
picLeft3D.BackColor = lblBackgroundColor.BackColor;
之后,程序会创建多个随机对象和文字,并将它们的信息收集到一个List
中。请注意,myDepth
属性是重要部分,它唯一的作用是改变对象的 X 坐标。
...
// selecting Simple pictures with different size for painting on the scene
if (chkSimpleObjects.Checked)
{
if (picsSimple.Count > 0)
{
for (int i = 1; i <= updSimpleObject.Value; i++)
{
myPicInfo = new picCollection();
myPicInfo.myImage = picsSimple[myRandom.Next(picsSimple.Count)];
myPicInfo.myImage2 = myPicInfo.myImage;
myPicInfo.myDepth = myRandom.Next(1, trbDepthShapes.Value / 2);
myPicInfo.myScale = (float)(myRandom.NextDouble() * .7 + .4);
myPicInfo.myX = (myRandom.Next(myPicInfo.myDepth, picLeft3D.Width -
myPicInfo.myImage.Width - myPicInfo.myDepth) %
picLeft3D.Width) + myPicInfo.myDepth;
myPicInfo.myY = myRandom.Next(picLeft3D.Height - 100 - limitY);
myAllPicCollection.Add(myPicInfo);
}
}
}
...
最后,程序将按照深度顺序在场景中显示对象和文字。
// drawing all objects of the collection in order by depth on the scene
for (int d = 0; d < 120; d++)
{
foreach (picCollection myP in myAllPicCollection)
{
if (d == myP.myDepth) DrawPicture(gLeft,gRight , myP);
}
if (chkShadow.Checked && chkShadow.Enabled && d ==
(int)(trbDepthText.Value / 2 - 2))
DrawText(txtMain.Text, gLeft,gRight ,
10 + d, 20, d, new SolidBrush(lblShadow.BackColor), myFont, 4);
if (chkText.Checked && d == (int)(trbDepthText.Value / 2))
DrawText(txtMain.Text, gLeft, gRight , 10 + d, 20, d,
new SolidBrush(lblTextColor.BackColor), myFont, 0);
}
...
// Draw Graphics on the scene
private void DrawPicture(Graphics gLeft,Graphics gRight, picCollection myP)
{
gLeft.DrawImage(myP.myImage ,myP. myX,myP. myY,myP.myImage.Width *myP. myScale,
myP. myImage.Height *myP. myScale);
gRight.DrawImage(myP.myImage2 ,myP. myX -myP.myDepth,myP. myY,
myP. myImage2.Width *myP. myScale,
myP. myImage2.Height *myP. myScale);
}
// Draw Text on the scene
private void DrawText(string myText,Graphics gLeft,Graphics gRight,
int myX, int myY,int myDepth, Brush myBrush,
Font myFont,int shaDow)
{
gLeft.DrawString(myText , myFont, myBrush, myX + shaDow, myY + shaDow);
gRight.DrawString(myText , myFont, myBrush, myX+ shaDow - myDepth, myY + shaDow );
}
就是这样,请享用!
关注点
你可以在这个程序中使用你自己的图片和纹理。在可执行文件所在的位置有四个目录,如下图所示。
- PicBackground:此目录中的文件将用作背景纹理。
- PicSmily:此目录中的文件将用于 3D 场景的不同深度,并保持原始尺寸,因此我只在此目录中使用了小图片。
- PicSimple:这些文件将以不同尺寸出现在场景中,建议不要使用大于 120x120 像素的文件。
- Pic3D:在此类别中,每个对象都有两张图片,一张用于左眼,一张用于右眼。这两张图片略有不同,使得主体看起来是 3D 的。
如何观看立体图像?
有几种方法可以实现这一点,但本文基于平行视角和交叉眼视角。

1. 平行视角
这是一种非常简单的方法,不需要任何设备。如果你是立体视觉的新手,请从这个方法开始,使用程序的原始窗口大小(不要最大化!)。成对的图像必须并排放置(左图在左边,右图在右边),当你观看它们时,你不需要聚焦,只需放松你的眼睛,就像你想看远处一样(你的眼睛必须几乎平行)。几秒钟后,你会看到一张模糊的图片,然后逐渐变得清晰,然后你就会看到具有完整深度的 3D 场景。
|
|
正常视角 | 平行视角 |
在这个软件中,成对图像的顶部有 2 个红点,以帮助你更快地观看。

当你观看成对的图像时(不聚焦它们),几秒钟后,这两个红点会一起移动,最终变成一个。此时,你将看到三个图像,中间有一个清晰锐利的图像,两侧各有一个模糊的图像。我们的 3D 场景就是中间那个。

这种方法有一些限制,我不认为你可以观看全屏的平行图像对,因为观看比你的眼距更宽的主题非常困难,你不得不尝试其他方法。
2. 交叉眼视角
这比平行视角好得多,但第一次看起来很困难。在这种方法中,你可以看到一张像墙一样大的图像对!此外,深度感知更远、更快。我推荐这种方法,并将程序设置为全屏。

在这种方法中,你必须交换左右图像(左图在右边,右图在左边),你的焦点位于图像之间和你的眼睛之间。
最初,最大化窗口大小并点击主按钮以创建一些新对象。
![]() |
![]() |
将一支笔放在显示器屏幕前面,在中间线上,比上面的红点稍低一些。

聚焦笔尖,轻轻地将其移向你的脸,并保持聚焦。同时,关注后面的图像(尤其是 2 个蓝点)。
你会看到 2 个蓝点一起移动并变成 4 个蓝点。

在笔与显示器和你的眼睛之间有一个特定的距离,中间的蓝点会变成一个。

这个新的蓝点并不清晰。停止使用笔,几秒钟后,看向新的蓝点及其周围的其他图像,最初它们是模糊的。不要着急,让你的眼睛慢慢适应。很快就会变得清晰和 3D。如果你丢失了新的焦点,眼睛恢复到正常视野,那么就重新开始。
有一个值得注意的情况。你必须看到两个蓝点在水平方向上相等。如果一个比另一个高(见图),那么稍微转动你的头,使它们在同一水平线上。

虽然交叉眼视角第一次很难,但有了一点经验,这是最快、最好的方法。
3. 红蓝(模拟)方法
这种方法需要特殊的红蓝眼镜。我将在将来进行解释。
历史
- 首次发布(2008 年 12 月 2 日):基于平行视角方法
- 更新 1(2008 年 12 月 12 日):增加了交叉眼方法,并支持不同大小的程序窗口
- 更新 2(2009 年 1 月 15 日):更新了源代码和演示项目
- 更新 3(2009 年 2 月 11 日):添加了计时器
- 更新 4(2009 年 2 月 20 日):添加了保存和自动保存选项
- 更新 5(2009 年 3 月 18 日):更新了源代码和演示项目