65.9K
CodeProject 正在变化。 阅读更多。
Home

立体摄影

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (70投票s)

2008 年 12 月 2 日

CPOL

8分钟阅读

viewsIcon

112308

downloadIcon

3856

这是一个简单的程序,用于解释 3D 立体视觉系统的工作原理,也包含了一些趣味性!

Sample Image

目录

引言

你想找点乐子吗?如果你度过了忙碌的一天,想从工作中放松一下,那么这个就是为你准备的!

立体视觉背后的逻辑非常简单,但结果却令人惊叹和有趣!特别是当你意识到,你可以用 2D 对象轻松地制作自己的 3D 环境。

当你使用像 3D StudioMaya 这样的专业 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 日):更新了源代码和演示项目
© . All rights reserved.