转 第十五章 3D 基础 (2)(as3.0)

本文探讨了3D环境中物体的缓动运动、弹性运动及z排序等关键概念,通过实例展示了如何实现平滑的3D动画效果,并解决了物体在不同深度下的正确显示问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

排序
     在添加了多个物体后代码中显现出了一个新问题---称为z  排序。Z  排序就像它的名
字一样:物体如何在 z  轴上进行排序,或者说哪个物体在前面。由于物体都使用纯色,所
以看起来不是很明显。为了让效果更加明显,请将 Ball3D  的 init  方法改为以下代码,并
运行刚才那个程序:
public function init():void {
 graphics.lineStyle(0);
 graphics.beginFill(color);
  graphics.drawCircle(0, 0, radius);
 graphics.endFill();
}

     通过给小球添加轮廓线,我们就可以看出哪个小球在前面了。这几乎毁掉了整个 3D 
果,因为现在较小的物体出现在了较大物体的前面。Z  排序就是用来解决这个问题的,但
不是自动的。Flash  不知道我们在模拟 3D。它只知道我们在移动和缩放影片。它也不知道
我们到底是使用左手还是右手坐标系。在小球远离时应该将这个小球放在相邻小球的后面。
Flash  只根据在显示列表中的相对索引进行排序。在 AS 2 中,z  排序只需要改变影片剪辑
的深度即可完成。swapDepths(深度)。深度较高的影片剪辑出现在深度较低的影片的前面。
然而在 AS 3  中,操作会稍微有些复杂。对于显示列表没有可以任意修改的深度值。显示列
表的作用更像是与个数组。列表中的每个显示对象都有一个索引。索引从 0  开始,直到列
表中所有对象的个数。例如,假设在类中加入三个影片 A, B, C。它们的索引应该是 0, 1, 2。
无法将其中的一个影片的索引设置为 100,或 -100。如果已经删除了 B 影片,那么这时 A
和 C  影片的索引应该是 0  和 1。明白了吧,在显示列表中永远没有“空间”这个概念
     根据深度,索引 0  是最低的,任何深度较高的显示对象都将出现在这个较低对象的前
面。我们可以用几种不同的方法来改变物体的深度:
■ setChildIndex(child:DisplayObject, index:int) 给对象指定索引值(index)。
■ swapChildren(child1:DisplayObject, child2:DisplayObject) 交换两个指定的对象。
■ swapChildrenAt(index1:int, index2:int)  交换两个指定的深度。
使用 setChildIndex 是最简单的。因为我们已经有了一个 balls 数组。可以根据小球的 z 轴
深度从高到低来排序这个数组,然后从 balls 的 0(最远的)  到 49(最近的)为每个小球
设置索引。请看下面这段代码:

private function sortZ():void {
 balls.sortOn("zpos", Array.DESCENDING | Array.NUMERIC);
  for (var i:uint = 0; i < numBalls; i++) {
   var ball:Ball3D = balls[i];
  setChildIndex(ball, i);
 }

}
     根据数组中每个对象的 zpos  属性对该数组进行排序。因为指定了数组的
DESCENDING  和 Array.NUMERIC,则是按数值大小反向排序的——换句话讲,就是从高
到低。结果会使最远的小球(zpos  值最高的)将成为数组中的第一个,最近的将成为最后
一个。
     然后循环这个数组,将每个小球在显示列表中的索引值设置为与当前在数组中的索引值
相同。
     将这个方法放入类中,只需要在小球移动后调用它即可,将函数调用放在 onEnterFrame
方法的最后:

private function onEnterFrame(event:Event):void {
  for (var i:uint = 0; i < numBalls; i++) {
   var ball:Ball3D = balls[i];
  move(ball);
 }
 sortZ();
}
     剩下的代码与上一个例子中的相同。全部代码可在 Zsort.as  中找到。
 

 

重力
     这里我们所说的重力就像地球表面上的重力一样,如第五章所讲的。既然这样 3D 
重力和 2D  的就很像了。我们所需要做的就是选择一个施加在物体上的重力值,并在每帧
中将它加入到物体的速度中。
     由于 3D  的重力非常简单,我差点就跳过去说“是的,同 2D  一样。OK,下一话题。”
但是,我决定将它放到一个很好的例子中加以解释,让大家知道即使很简单的东西也可以创
造出非常棒的效果,就像 3D  烟火一样。
     首先,我们需要找个物体代表一个单独的“烟火”——我们知道,这些发光的点可以组
合到一起形成巨大的爆炸。我们给忠实的 Ball3D  类一个较小的半径值,就可以完成这个目
的。只要给每个小球一个随机的颜色效果就会非常漂亮。如果将背景色设置为黑色就更好了。
我使用 SWF  元数据来完成这个设置,但如果是在 Flash CS3 IDE  中,只需要简单地改变
一下文档属性的背景色即可。
     我确信大家现在一定能够完成。先将所有的代码列出来(Fireworks.as),随后加以解释。

package {
 import flash.display.Sprite;
 import flash.events.Event;
 [SWF(backgroundColor=0x000000)];
  public class Fireworks extends Sprite {
   private var balls:Array;
   private var numBalls:uint = 100;
   private var fl:Number = 250;
   private var vpX:Number = stage.stageWidth / 2;
   private var vpY:Number = stage.stageHeight / 2;
   private var gravity:Number = 0.2;
   private var floor:Number = 200;
   private var bounce:Number = -0.6;
   public function Fireworks() {
   init();
  }

 private function init():void {
  balls = new Array();
   for (var i:uint = 0; i < numBalls; i++) {
    var ball:Ball3D = new Ball3D(3, Math.random() * 0xffffff);
   balls.push(ball);
   ball.ypos = -100;
    ball.vx = Math.random() * 6 - 3;
    ball.vy = Math.random() * 6 - 6;
    ball.vz = Math.random() * 6 - 3;
   addChild(ball);
  }
  addEventListener(Event.ENTER_FRAME, onEnterFrame);
 }
 private function onEnterFrame(event:Event):void {
   for (var i:uint = 0; i < numBalls; i++) {
   var ball:Ball3D = balls[i];
   move(ball);
  }
  sortZ();
 }

 private function move(ball:Ball3D):void {
  ball.vy += gravity;
  ball.xpos += ball.vx;
  ball.ypos += ball.vy;
  ball.zpos += ball.vz;
   if (ball.ypos > floor) {
   ball.ypos = floor;
   ball.vy *= bounce;
  }
   if (ball.zpos > -fl) {
   var scale:Number = fl / (fl + ball.zpos);
    ball.scaleX = ball.scaleY = scale;
    ball.x = vpX + ball.xpos * scale;
    ball.y = vpY + ball.ypos * scale;
   ball.visible = true;
  } else {
   ball.visible = false;
  }
 }
 private function sortZ():void {
  balls.sortOn("zpos", Array.DESCENDING | Array.NUMERIC);
   for (var i:uint = 0; i < numBalls; i++) {
   var ball:Ball3D = balls[i];
   setChildIndex(ball, i);

  }
  }
 }
}
     首先加入一些属性:gravity, bounce, floor。前两个大家都见过。Floor  属性就是  --

bottom --也就是物理反弹之前可以运动到的 y 值。除了增加 y  轴速度以及碰撞地
面后的反弹以外,所有的内容我们前面都介绍过,是不是越来越酷了,哈?
     运行结果如图 15-6 所示。

转 <wbr>第十五章 <wbr>3D <wbr>基础 <wbr>(2)(as3.0)

图 15-6  烟火(相信我,运动中的效果更好)

屏幕环绕
     回忆一下第六章,我们说过三种当物体碰到边界后受到反作用力的可能。目前为
们只介绍了反弹。还有两个:屏幕环绕与重置。对于 3D  而言,我发现屏幕环绕效果是最
为有用的,但只能在 z 轴上使用。
    2D  屏幕环绕中,在 x  或 y  轴上判断物体是否出了屏幕。效果非常好,因为当物体超
出了其中一个边界时就看不到它了,因此可以轻松地重新设置物体的位置,不会引起人们的
注意。但是 3D  中就不能这么潇洒了。
     在 3D  中,实际上只有两个点可以安全地删除和重置物体。一个就是当物体运动到观
察点的后面。前面例子中,将物体设置为不可见时,就是这个道理。另一个就是当物体的距
离太远和太小时也可以将其设为不可见的。这就意味着我们可以在 z  轴上安全地进行屏幕
包装。当物体走到身后时,就将它放到面前的远方。如果物体离得过远,超出了可见范围,
就可以删除它并将其重置到身后。如果大家喜欢 x, y  轴也可以这样做,但是多数情况下,
这么做会导致一些不自然的忽隐忽现的效果。
     还好 z 轴的环绕可以是相当常用的。我曾用它制作出真实的 3D 赛车游戏,下面我们
就来制作其中的一部分。
     主体思想是把不同的 3D  物体放到观察点前。然后将这些物体向观察点移动。换句话
讲,给它们一些负的 z  速度。这要看我们如何设置了,可以让物体向我们走来,让眼睛以
为是我们向物体走去。一旦物体走到了观察点后,就将它重置到眼前一段距离。这样就可以
永无止境地掠过这些物体了。
     本例中使用的物体是一棵线条化的树。创建一棵带有随机枝叉的树形结构。我确信
能做得更好!
     绘制树的代码放在名为 Tree  的类中,下面会看到,用三个位置属性以及随机绘制树枝
的代码来代表一颗树。

 

package {
 import flash.display.Sprite;
  public class Tree extends Sprite {
   public var xpos:Number = 0;
    public var ypos:Number = 0;
   public var zpos:Number = 0;
   public function Tree() {
   init();
  }
   public function init():void {
   graphics.lineStyle(0, 0xffffff);
     graphics.lineTo(0, -140 - Math.random() * 20);
     graphics.moveTo(0, -30 - Math.random() * 30);
     graphics.lineTo(Math.random() * 80 - 40,
     -100 - Math.random() * 40);
     graphics.moveTo(0, -60 - Math.random() * 40);
     graphics.lineTo(Math.random() * 60 - 30,
     -110 - Math.random() * 20);
  }
 }
}

  同样,还是使用 SWF  元数据将背景色设置为黑色。大家可以创建任何喜欢的物体,
想要多复杂都可以自行设置。在文档类中创建所有的树(100  左右)。随机分散在 x  轴上,
每个方向 1000  像素。它们同样随机分散到 z  轴上,从 0  到 10000。它们都以 floor  属性
为基础,具有相同的 y 坐标,给人一种地平面的感觉。
     以下是代码(可以见 Trees.as):
Here’s the code (which you can also find in Trees.as):
package {
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.events.KeyboardEvent;
 import flash.ui.Keyboard;
 [SWF(backgroundColor=0x000000)];
  public class Trees extends Sprite {
   private var trees:Array;
   private var numTrees:uint = 100;
   private var fl:Number = 250;
   private var vpX:Number = stage.stageWidth / 2;
   private var vpY:Number = stage.stageHeight / 2;
   private var floor:Number = 50;
   private var vz:Number = 0;
   private var friction:Number = 0.98;

 public function Trees() {
  init();
 }
 private function init():void {
  trees = new Array();
   for (var i:uint = 0; i < numTrees; i++) {
   var tree:Tree = new Tree();
   trees.push(tree);
     tree.xpos = Math.random() * 2000 - 1000;
   tree.ypos = floor;
   tree.zpos = Math.random() * 10000;

    addChild(tree);
   }
   addEventListener(Event.ENTER_FRAME, onEnterFrame);
   stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
  }
   private function onEnterFrame(event:Event):void {
     for (var i:uint = 0; i < numTrees; i++) {
    var tree:Tree = trees[i];
    move(tree);
   }
   vz *= friction;
   sortZ();
  }
   private function onKeyDown(event:KeyboardEvent):void {
     if (event.keyCode == Keyboard.UP) {
    vz -= 1;
     } else if (event.keyCode == Keyboard.DOWN) {
    vz += 1;
   }
  }
   private function move(tree:Tree):void {

   tree.zpos += vz;
     if (tree.zpos < -fl) {
    tree.zpos += 10000;
   }
     if (tree.zpos > 10000 - fl) {
    tree.zpos -= 10000;
   }
     var scale:Number = fl / (fl + tree.zpos);
     tree.scaleX = tree.scaleY = scale;
     tree.x = vpX + tree.xpos * scale;
     tree.y = vpY + tree.ypos * scale;
   tree.alpha = scale;
  }
   private function sortZ():void {
   trees.sortOn("zpos", Array.DESCENDING | Array.NUMERIC);
     for (var i:uint = 0; i < numTrees; i++) {
    var tree:Tree = trees[i];
    setChildIndex(tree, i);
   }
  }
 }
}

  请注意,这里只有一个 z 轴速度变量,因为树不需要在 x 或 y  轴上进行移动,所有
的移动都在 z  轴上。在 onEnterFrame 方法中,判断方向键上和下,增加或减少 vz。加入
一点点摩擦力让速度不会增加到无限大,在按键松开时将速度降下来。
     代码循环获得每棵树,用当前 z  速度更新该树的 z  坐标。然后判断这棵树是否走到了
我们的身后。如果是,将这个棵树向 z 轴内移动 10000  像素。否则,如果超过了 10000 
fl,就将该树往回移动 10000  像素。再执行标准透视动作。为了更好地加强立体感我还加

入了一个小小的设计:
    tree.alpha = scale;
根据 z  轴的深度设置树的透明度。离得越远颜色越淡。这是大气透视,模拟大气与观察者
和物体之间的效果。这是本例中表现物体远离时一种特殊效果。这个特殊的设计给了我们黑
暗的效果和幽深的夜。大家也许可以试试这种方法:
       tree.alpha = scale * .7 + .3;
让树的可见度至少为 30%。看上去不再那么朦胧。这里没有正确或错误可言--只有不同
的数值创造不同的效果。
    大家也许注意到了,我仍把 z 排序方法留在这里。在这个特殊的例子中,它没有发挥
本应有的作用,因为树都是由同一颜色的简单线条构成的,但如果绘制的是一些非常复杂的
或重叠的图形,那么它的存在就是至关重要的了。
     本文件的运行结果  15-7  所示。

转 <wbr>第十五章 <wbr>3D <wbr>基础 <wbr>(2)(as3.0)

图 15-7  当心小树!
     下面我将给大家一个加强的例子,让我们看一下还可以做到什么样的程度。以下是程
(可以在 Trees2.as  中找到):
package {
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.events.KeyboardEvent;
 import flash.ui.Keyboard;
 [SWF(backgroundColor=0x000000)];
  public class Trees2 extends Sprite {
   private var trees:Array;
   private var numTrees:uint = 100;
   private var fl:Number = 250;
   private var vpX:Number = stage.stageWidth / 2;
   private var vpY:Number = stage.stageHeight / 2;
   private var floor:Number = 50;
   private var ax:Number = 0;

   private var ay:Number = 0;
   private var az:Number = 0;
   private var vx:Number = 0;
    private var vy:Number = 0;
   private var vz:Number = 0;
   private var gravity:Number = 0.3;
   private var friction:Number = 0.98;
   public function Trees2() {
   init();
  }
   private function init():void {
   trees = new Array();
     for (var i:uint = 0; i < numTrees; i++) {
    var tree:Tree = new Tree();
    trees.push(tree);
       tree.xpos = Math.random() * 2000 - 1000;
    tree.ypos = floor;
    tree.zpos = Math.random() * 10000;
    addChild(tree);
   }

   addEventListener(Event.ENTER_FRAME, onEnterFrame);
   stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
   stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
  }
   private function onEnterFrame(event:Event):void {
   vx += ax;
   vy += ay;
   vz += az;
   vy -= gravity;
     for (var i:uint = 0; i < numTrees; i++) {
    var tree:Tree = trees[i];
    move(tree);
   }
   vx *= friction;
   vy *= friction;
   vz *= friction;
   sortZ();
  }
   private function onKeyDown(event:KeyboardEvent):void {
   switch (event.keyCode) {
    case Keyboard.UP :

   az = -1;
     break;
    case Keyboard.DOWN :
     az = 1;
     break;
    case Keyboard.LEFT :
     ax = 1;
     break;
    case Keyboard.RIGHT :
     ax = -1;
     break;

   case Keyboard.SPACE :
     ay = 1;
     break;
    default :
     break;
   }
  }
   private function onKeyUp(event:KeyboardEvent):void {
   switch (event.keyCode) {
    case Keyboard.UP :
    case Keyboard.DOWN :
     az = 0;
     break;
    case Keyboard.LEFT :
    case Keyboard.RIGHT :
     ax = 0;
     break;
    case Keyboard.SPACE :
     ay = 0;
     break;

  default :
     break;
   }
  }
   private function move(tree:Tree):void {
   tree.xpos += vx;
   tree.ypos += vy;
   tree.zpos += vz;
     if (tree.ypos < floor) {
    tree.ypos = floor;
   }
     if (tree.zpos < -fl) {
    tree.zpos += 10000;
   }
     if (tree.zpos > 10000 - fl) {
    tree.zpos -= 10000;
   }
     var scale:Number = fl / (fl + tree.zpos);
     tree.scaleX = tree.scaleY = scale;

   tree.x = vpX + tree.xpos * scale;
   tree.y = vpY + tree.ypos * scale;
  tree.alpha = scale;
 }
 private function sortZ():void {
  trees.sortOn("zpos", Array.DESCENDING | Array.NUMERIC);
   for (var i:uint = 0; i < numTrees; i++) {
   var tree:Tree = trees[i];
   setChildIndex(tree, i);
  }

  }
 }
}
     这里,我已经加入了 x  和 y 轴的速度,还有重力。还必需要能够捕获多个按键。我唯
一想念 AS 2  的是 Key.isDown()  方法,任何时间都可以调用找出某个键是否被按住。因为
在 AS 3  中我们只能知道最后一次按下或释放的键,所以不得不判断哪个键被按下并设置相
应轴上的加速度为 1  或 -1。随后,当该键被松开时,再将加速度设回 0。在 onEnterFrame
的开始就将每个轴上的加速度加到相应轴的速度中。左键和右键显然就是用于选择 x  轴的
速度,使用空格键操作 y  轴。有趣的一点是我们实际是从 vy  减去了重力。因为我想要一
个类似于观察者落到树林中的效果,如图 15-8  所示。注意我们同样也限定了树的 y  坐标
为 50,看起来就像是站在陆地上一样。



转 <wbr>第十五章 <wbr>3D <wbr>基础 <wbr>(2)(as3.0)

图 15-8  看,我在飞!
     这里没有对 x  轴的运动加以任何的限制,也就意味着可以在树林边上行进。要想加入
限制对于大家来说也不是件难事,但是作为一个启发性的例子做到这里已经足够了。
缓动与弹性运动
     在 3D 中的缓动与弹性运动不会比 2D  中的难多少(第八章的课题)。我们只需为 z
轴再加入一至两个变量。

 

 
缓动
     对于缓动的介绍不算很多。在 2D  中,我们用 tx  和 ty 最为目标点。现在只需要再在
轴上加入 tz。每帧计算物体每个轴到目标点的距离,并移动一段距离。
     让我们来看一个简单的例子,让物体缓动运动到随机的目标点,到达该点后,再选出另
一个目标并让物体移动过去。注意后面两个例子,我们又回到了 Ball3D  这个类上。以下是
代码(可以在 Easing3D.as  中找到):
package {
 import flash.display.Sprite;
 import flash.events.Event;
  public class Easing3D extends Sprite {
   private var ball:Ball3D;
   private var tx:Number;
   private var ty:Number;
   private var tz:Number;
   private var easing:Number = .1;
   private var fl:Number = 250;
   private var vpX:Number = stage.stageWidth / 2;

   private var vpY:Number = stage.stageHeight / 2;
   public function Easing3D() {
   init();
  }
   private function init():void {
   ball = new Ball3D();
   addChild(ball);
     tx = Math.random() * 500 - 250;
     ty = Math.random() * 500 - 250;
     tz = Math.random() * 500;
   addEventListener(Event.ENTER_FRAME, onEnterFrame);
  }
   private function onEnterFrame(event:Event):void {
     var dx:Number = tx - ball.xpos;
     var dy:Number = ty - ball.ypos;
     var dz:Number = tz - ball.zpos;
   ball.xpos += dx * easing;
   ball.ypos += dy * easing;
     ball.zpos += dz * easing;
     var dist:Number = Math.sqrt(dx*dx + dy*dy + dz*dz);

     if (dist < 1) {
       tx = Math.random() * 500 - 250;
       ty = Math.random() * 500 - 250;
       tz = Math.random() * 500;
   }
     if (ball.zpos > -fl) {
    var scale:Number = fl / (fl + ball.zpos);
      ball.scaleX = ball.scaleY = scale;
      ball.x = vpX + ball.xpos * scale;
      ball.y = vpY + ball.ypos * scale;
    ball.visible = true;
   } else {
    ball.visible = false;
   }
  }
 }
}
代码中最有趣的地方是下面这行:

      var dist:Number = Math.sqrt(dx * dx + dy * dy + dz * dz);
我们知道,在 2D  中计算两点间距离的方程是:
      var dist:Number = Math.sqrt(dx * dx + dy * dy);
在 3D  距离中,只需要将第三个轴距离的平方加入进去。由于这个公式过于简单所以我
常会受到质疑。在加入了一个条件后,似乎应该使用立方根。但是它并不是用在这里的。
 
 
 
弹性运动
     弹性运动是缓动的兄弟,需用相似的方法将其调整为 3D  的。我们只使用物体到目标
的距离改变速度,而不是改变位置。给大家一个快速的示例。本例中(Spring3D.as),点击

鼠标将创建出一个随机的目标点。
package {
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.events.MouseEvent;
  public class Spring3D extends Sprite {
   private var ball:Ball3D;
   private var tx:Number;
   private var ty:Number;
   private var tz:Number;
   private var spring:Number = .1;
   private var friction:Number = .94;
   private var fl:Number = 250;
   private var vpX:Number = stage.stageWidth / 2;
   private var vpY:Number = stage.stageHeight / 2;
   public function Spring3D() {
   init();
  }

   private function init():void {
   ball = new Ball3D();
   addChild(ball);
     tx = Math.random() * 500 - 250;
     ty = Math.random() * 500 - 250;
     tz = Math.random() * 500;
   addEventListener(Event.ENTER_FRAME, onEnterFrame);
   stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
  }
   private function onEnterFrame(event:Event):void {
     var dx:Number = tx - ball.xpos;
     var dy:Number = ty - ball.ypos;
     var dz:Number = tz - ball.zpos;
     ball.vx += dx * spring;
     ball.vy += dy * spring;
     ball.vz += dz * spring;
   ball.xpos += ball.vx;
   ball.ypos += ball.vy;
   ball.zpos += ball.vz;
   ball.vx *= friction;
   ball.vy *= friction;
   ball.vz *= friction;

   if (ball.zpos > -fl) {
   var scale:Number = fl / (fl + ball.zpos);
    ball.scaleX = ball.scaleY = scale;
    ball.x = vpX + ball.xpos * scale;
    ball.y = vpY + ball.ypos * scale;
   ball.visible = true;
  } else {
   ball.visible = false;
  }

  }
   private function onMouseDown(event:MouseEvent):void {
     tx = Math.random() * 500 - 250;
     ty = Math.random() * 500 - 250;
     tz = Math.random() * 500;
  }
 }

 




(如果要转载请注明出处http://blog.sina.com.cn/jooi,谢谢)

<think>这个错误是因为在应用水彩风格时,我们调用的gaussian_blur函数对输入张量的维度有要求。根据错误信息,gaussian_blur函数要求输入是2D、3D、4D或5D,并且使用非恒定填充。在我们的代码中,当应用水彩风格时,我们传入的edge_tensor可能是3D张量(1, H, W)或4D张量(1, 1, H, W)?我们需要检查张量的维度。 在`apply_style_effect`函数中,我们传入的edge_tensor是4D张量(1, 1, H, W)吗?实际上,在`convert_to_line_art`函数中,我们得到的edge_tensor是经过卷积操作后的4D张量(1, 1, H, W)。但是在应用风格之前,我们进行了归一化和增强,但维度没有变化。 然而,在`apply_style_effect`函数中,我们针对水彩风格调用了gaussian_blur,并使用了`edge_tensor.unsqueeze(0)`,这会使4D张量变成5D?然后再用squeeze(0)变回4D?这可能导致维度不匹配。 让我们重新检查维度: 在`convert_to_line_art`函数中: gray_tensor: [1, 1, H, W] (4D) 经过卷积后,edge_tensor: [1, 1, H, W] (4D) 然后我们调用`apply_style_effect(edge_tensor, ...)`,传入的edge_tensor是4D张量。 在`apply_style_effect`中,对于水彩风格: edge_tensor = gaussian_blur(edge_tensor.unsqueeze(0), kernel_size=3, sigma=0.5).squeeze(0) 这里,edge_tensor.unsqueeze(0) -> [1, 1, 1, H, W] (5D) gaussian_blur函数支持5D吗?根据torchvision文档,gaussian_blur支持4D张量(batch, channel, height, width)和3D张量(channel, height, width)。所以5D是不支持的。 因此,我们应该直接使用4D张量进行模糊,而不需要unsqueeze。 修复:将水彩风格的处理改为: edge_tensor = gaussian_blur(edge_tensor, kernel_size=3, sigma=0.5) 但是注意,在`convert_to_line_art`函数中,我们已经在4D张量上工作,所以直接传入4D即可。 另外,在素描、漫画和钢笔风格中,我们也是直接操作4D张量吗?是的,因为我们的操作都是逐元素的,所以没有问题。 因此,修改水彩风格的处理为: elif style_name == "水彩": # 水彩效果:轻微模糊 edge_tensor = gaussian_blur(edge_tensor, kernel_size=3, sigma=0.5) 但是注意,在`convert_to_line_art`函数中,我们后面还有一个反操作,然后保存为图像。所以这里我们处理的是4D张量,但gaussian_blur要求输入是4D(batch, channel, height, width)或3D(channel, height, width)?根据torchvision.transforms.functional.gaussian_blur的文档,它要求输入为(..., H, W),其中...表示任意维度,但至少是2D。所以4D是允许的。 让我们修改代码。 同时,我们也要注意,在`convert_to_line_art`函数中,我们在应用高斯模糊预处理时已经使用了gaussian_blur函数,所以这里再次使用应该没有问题。 修改后的`apply_style_effect`函数: ```python def apply_style_effect(self, edge_tensor, style_name): """应用不同的线稿风格效果""" if style_name == "素描": # 素描效果:增强对比度 edge_tensor = torch.pow(edge_tensor, 0.7) elif style_name == "漫画": # 漫画效果:二值化处理 edge_tensor = (edge_tensor > 0.2).float() elif style_name == "水彩": # 水彩效果:轻微模糊 edge_tensor = gaussian_blur(edge_tensor, kernel_size=3, sigma=0.5) elif style_name == "钢笔": # 钢笔效果:细化线条 edge_tensor = torch.clamp(edge_tensor * 1.5, 0, 1) return edge_tensor ``` 这样修改后,水彩风格应该可以正常使用了。 另外,我们还需要注意,在`convert_to_line_art`函数中,我们应用了两次高斯模糊(一次在预处理,一次在风格处理)。这是设计上的选择,预处理的高斯模糊是为了降噪,而风格处理中的模糊是为了艺术效果。 为了确保代码的健壮性,我们还可以在调用gaussian_blur之前检查张量的维度,但通常这里不会有问题。 现在,我们提供修复后的完整代码(仅修改了`apply_style_effect`函数中的水彩风格部分):</think>这个错误是由于在应用水彩风格效果时,高斯模糊函数的输入维度不正确导致的。让我修复这个问题并提供完整的解决方案。 ### 问题分析与修复 错误发生在水彩风格处理部分: ```python elif style_name == "水彩": # 水彩效果:轻微模糊 edge_tensor = gaussian_blur(edge_tensor.unsqueeze(0), kernel_size=3, sigma=0.5).squeeze(0) ``` 问题原因: 1. `gaussian_blur` 函数需要特定维度的输入张量 2. 在应用风格效果时,输入张量已经是4D(1,1,H,W),但`.unsqueeze(0)`会使其变为5D 3. 高斯模糊函数不支持5D张量 ### 修复后的完整代码 ```python import os import torch import numpy as np import threading import tkinter as tk from tkinter import filedialog, ttk, messagebox, Canvas from PIL import Image, ImageTk, ImageOps import torch.nn.functional as F import time import sys from torchvision.transforms.functional import gaussian_blur class LineArtConverter: def __init__(self, root): self.root = root self.root.title("ArtLine Pro - GPU加速线稿生成器") self.root.geometry("900x750") self.root.resizable(True, True) # 检查CUDA是否可用 self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 存储要处理的文件路径 self.file_paths = [] # 进度相关变量 self.total_files = 0 self.processed_files = 0 self.processing = False self.cancel_processing = False # 预览相关 self.preview_image = None self.preview_canvas = None self.preview_window = None # 创建现代化UI self.setup_modern_ui() # 显示设备信息 device_info = f"使用设备: {self.device}" if self.device.type == "cpu": device_info += " (未检测到可用的CUDA设备)" self.device_label.config(text=device_info) def setup_modern_ui(self): """创建现代化UI界面""" # 设置现代主题 style = ttk.Style() style.theme_use(&#39;clam&#39;) style.configure(&#39;.&#39;, background=&#39;#f5f5f5&#39;) style.configure(&#39;TFrame&#39;, background=&#39;#f5f5f5&#39;) style.configure(&#39;TLabel&#39;, background=&#39;#f5f5f5&#39;, foreground=&#39;#333333&#39;, font=(&#39;Segoe UI&#39;, 9)) style.configure(&#39;TButton&#39;, background=&#39;#4a6fa5&#39;, foreground=&#39;white&#39;, font=(&#39;Segoe UI&#39;, 9, &#39;bold&#39;), borderwidth=1) style.map(&#39;TButton&#39;, background=[(&#39;active&#39;, &#39;#3a5a8a&#39;)]) style.configure(&#39;Title.TLabel&#39;, font=(&#39;Segoe UI&#39;, 18, &#39;bold&#39;), foreground=&#39;#2c3e50&#39;) style.configure(&#39;Section.TLabelframe&#39;, borderwidth=2, relief=&#39;groove&#39;, background=&#39;#f5f5f5&#39;, foreground=&#39;#2c3e50&#39;) style.configure(&#39;Progress.Horizontal.TProgressbar&#39;, thickness=12, background=&#39;#4a6fa5&#39;, troughcolor=&#39;#e0e0e0&#39;) style.configure(&#39;TCombobox&#39;, fieldbackground=&#39;white&#39;, background=&#39;white&#39;) # 主布局 main_frame = ttk.Frame(self.root, padding=(20, 15)) main_frame.pack(fill=tk.BOTH, expand=True) # 标题区域 header_frame = ttk.Frame(main_frame) header_frame.pack(fill=tk.X, pady=(0, 15)) ttk.Label(header_frame, text="ArtLine Pro", style=&#39;Title.TLabel&#39;).pack(side=tk.LEFT) ttk.Label(header_frame, text="GPU加速的智能线稿生成器", font=(&#39;Segoe UI&#39;, 10), foreground=&#39;#7f8c8d&#39;).pack(side=tk.LEFT, padx=(10, 0)) # 设备信息 device_frame = ttk.Frame(main_frame, padding=10, relief=&#39;groove&#39;) device_frame.pack(fill=tk.X, pady=(0, 15)) ttk.Label(device_frame, text="系统状态:", font=(&#39;Segoe UI&#39;, 9, &#39;bold&#39;)).pack(side=tk.LEFT) self.device_label = ttk.Label(device_frame, text=f"使用设备: {self.device}", font=(&#39;Segoe UI&#39;, 9), foreground=&#39;#2c3e50&#39;) self.device_label.pack(side=tk.LEFT, padx=(5, 0)) # 性能标签 self.perf_label = ttk.Label(device_frame, text="", font=(&#39;Segoe UI&#39;, 9), foreground=&#39;#27ae60&#39;) self.perf_label.pack(side=tk.RIGHT) # 双列布局 columns_frame = ttk.Frame(main_frame) columns_frame.pack(fill=tk.BOTH, expand=True) # 左侧控制面板 control_frame = ttk.LabelFrame(columns_frame, text="处理设置", style=&#39;Section.TLabelframe&#39;, padding=15) control_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=(0, 10)) # 边缘强度控制 edge_frame = tttk.Frame(control_frame) edge_frame.pack(fill=tk.X, pady=(0, 15)) ttk.Label(edge_frame, text="边缘强度:").pack(side=tk.LEFT, padx=(0, 10)) self.edge_strength = tk.DoubleVar(value=1.5) edge_scale = ttk.Scale( edge_frame, from_=0.5, to=3.0, variable=self.edge_strength, orient=tk.HORIZONTAL, length=200 ) edge_scale.pack(side=tk.LEFT, expand=True) self.edge_value_label = ttk.Label(edge_frame, text="1.5", width=4) self.edge_value_label.pack(side=tk.LEFT, padx=(10, 0)) # 模糊强度控制 blur_frame = ttk.Frame(control_frame) blur_frame.pack(fill=tk.X, pady=(0, 15)) ttk.Label(blur_frame, text="模糊强度:").pack(side=tk.LEFT, padx=(0, 10)) self.blur_strength = tk.DoubleVar(value=1.0) blur_scale = ttk.Scale( blur_frame, from_=0.0, to=3.0, variable=self.blur_strength, orient=tk.HORIZONTAL, length=200 ) blur_scale.pack(side=tk.LEFT, expand=True) self.blur_value_label = ttk.Label(blur_frame, text="1.0", width=4) self.blur_value_label.pack(side=tk.LEFT, padx=(10, 0)) # 线稿风格选择 style_frame = ttk.Frame(control_frame) style_frame.pack(fill=tk.X, pady=(0, 15)) ttk.Label(style_frame, text="线稿风格:").pack(side=tk.LEFT, padx=(0, 10)) self.style_var = tk.StringVar(value="素描") styles = ["素描", "漫画", "水彩", "钢笔"] style_combo = ttk.Combobox(style_frame, textvariable=self.style_var, values=styles, state="readonly", width=10) style_combo.pack(side=tk.LEFT) # 输出格式选择 format_frame = ttk.FFrame(control_frame) format_frame.pack(fill=tk.X, pady=(0, 15)) ttk.Label(format_frame, text="输出格式:").pack(side=tk.LEFT, padx=(0, 10)) self.format_var = tk.StringVar(value="PNG (无损)") formats = ["PNG (无损)", "JPG (高质量)", "JPG (中等质量)", "JPG (低质量)"] format_combo = ttk.Combobox(format_frame, textvariable=self.format_var, values=formats, state="readonly", width=15) format_combo.pack(side=tk.LEFT) # 操作按钮 btn_frame = ttk.Frame(control_frame) btn_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Button(btn_frame, text="导入图片", command=self.import_images).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(btn_frame, text="导入文件夹", command=self.import_folder).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(btn_frame, text="清除列表", command=self.clear_all).pack(side=tk.LEFT) # 右侧文件区域 file_frame = ttk.LabelFrame(columns_frame, text="文件处理", style=&#39;Section.TLabelframe&#39;, padding=15) file_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) # 文件列表 list_container = ttk.Frame(file_frame) list_container.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) self.file_listbox = tk.Listbox( list_container, selectmode=tk.EXTENDED, background=&#39;white&#39;, relief=&#39;flat&#39;, highlightthickness=0, font=(&#39;Segoe UI&#39;, 9) ) scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.file_listbox.yview) self.file_listbox.config(yscrollcommand=scrollbar.set) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 列表操作按钮 list_btn_frame = ttk.Frame(file_frame) list_btn_frame.pack(fill=tk.X) ttk.Button(list_btn_frame, text="移除选中", command=self.remove_selected).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(list_btn_frame, text="预览选中", command=self.preview_selected).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(list_btn_frame, text="打开位置", command=self.open_file_location).pack(side=tk.LEFT) # 进度区域 progress_frame = ttk.LabelFrame(main_frame, text="处理进度", style=&#39;Section.TLabelframe&#39;, padding=15) progress_frame.pack(fill=tk.X, pady=(15, 0)) self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar( progress_frame, variable=self.progress_var, maximum=100, style=&#39;Progress.Horizontal.TProgressbar&#39;, length=100 ) self.progress_bar.pack(fill=tk.X, pady=(0, 5)) self.progress_label = ttk.Label( progress_frame, text="等待开始处理...", font=(&#39;Segoe UI&#39;, 9) ) self.progress_label.pack(anchor=tk.W) # 底部操作按钮 action_frame = ttk.Frame(main_frame) action_frame.pack(fill=tk.X, pady=(15, 0)) self.cancel_btn = ttk.Button( action_frame, text="取消处理", command=self.cancel_processing, state=tk.DISABLED ) self.cancel_btn.pack(side=tk.LEFT) self.process_btn = ttk.Button( action_frame, text="开始生成线稿", command=self.start_processing, width=15 ) self.process_btn.pack(side=tk.RIGHT) # 绑定事件 edge_scale.bind("<Motion>", lambda e: self.edge_value_label.config( text=f"{self.edge_strength.get():.1f}" )) blur_scale.bind("<Motion>", lambda e: self.blur_value_label.config( text=f"{self.blur_strength.get():.1f}" )) def import_images(self): """导入单张或多张图片""" file_types = ( (&#39;图像文件&#39;, &#39;*.jpg *.jpeg *.png *.bmp *.gif&#39;), (&#39;所有文件&#39;, &#39;*.*&#39;) ) filenames = filedialog.askopenfilenames( title="选择图片", initialdir="/", filetypes=file_types ) if filenames: for filename in filenames: if filename not in self.file_paths: self.file_paths.append(filename) self.file_listbox.insert(tk.END, os.path.basename(filename)) def import_folder(self): """导入文件夹""" folder = filedialog.askdirectory(title="选择图片文件夹") if folder: # 获取文件夹中所有图像文件 valid_extensions = (&#39;.jpg&#39;, &#39;.jpeg&#39;, &#39;.png&#39;, &#39;.bmp&#39;, &#39;.gif&#39;) image_files = [ os.path.join(folder, f) for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f)) and f.lower().endswith(valid_extensions) ] if not image_files: messagebox.showinfo("提示", f"在 {folder} 中未找到任何图像文件") return # 添加文件到列表 added_count = 0 for file_path in image_files: if file_path not in self.file_paths: self.file_paths.append(file_path) self.file_listbox.insert(tk.END, os.path.basename(file_path)) added_count += 1 if added_count > 0: messagebox.showinfo("提示", f"已添加 {added_count} 个图像文件") else: messagebox.showinfo("提示", "所有图像文件已在列表中") def clear_all(self): """清空文件列表""" if self.file_paths: if messagebox.askyesno("确认", "确定要清空所有文件列表吗?"): self.file_listbox.delete(0, tk.END) self.file_paths.clear() def remove_selected(self): """移除选中的文件""" selected_indices = self.file_listbox.curselection() if not selected_indices: return # 从后往前删除,避免索引变化问题 for i in sorted(selected_indices, reverse=True): self.file_listbox.delete(i) del self.file_paths[i] def open_file_location(self): """打开选中文件所在位置""" selected_indices = self.file_listbox.curselection() if not selected_indices: return # 只取第一个选中的文件 file_path = self.file_paths[selected_indices[0]] folder_path = os.path.dirname(file_path) if os.name == &#39;nt&#39;: # Windows os.startfile(folder_path) elif os.name == &#39;posix&#39;: # macOS, Linux import subprocess subprocess.Popen([&#39;open&#39;, folder_path] if sys.platform == &#39;darwin&#39; else [&#39;xdg-open&#39;, folder_path]) def preview_selected(self): """预览选中图片""" selected_indices = self.file_listbox.curselection() if not selected_indices: messagebox.showwarning("警告", "请先选择一张图片") return # 只取第一个选中的文件 file_path = self.file_paths[selected_indices[0]] # 创建预览窗口 if self.preview_window is None or not self.preview_window.winfo_exists(): self.preview_window = tk.Toplevel(self.root) self.preview_window.title("图片预览") self.preview_window.geometry("600x400") # 创建画布 self.preview_canvas = Canvas(self.preview_window, bg=&#39;white&#39;, bd=0, highlightthickness=0) self.preview_canvas.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 添加关闭按钮 btn_frame = ttk.Frame(self.preview_window) btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) ttk.Button(btn_frame, text="关闭预览", command=self.preview_window.destroy).pack(side=tk.RIGHT) ttk.Button(btn_frame, text="换预览", command=lambda: self.convert_preview(file_path)).pack(side=tk.RIGHT, padx=5) # 显示图片 self.show_image(file_path) self.preview_window.lift() def show_image(self, file_path): """在预览窗口中显示图片""" try: img = Image.open(file_path) # 调整图片大小以适应窗口 width, height = img.size max_size = 550 if width > max_size or height > max_size: ratio = min(max_size/width, max_size/height) new_size = (int(width * ratio), int(height * ratio)) img = img.resize(new_size, Image.LANCZOS) # 换为PhotoImage并显示 self.preview_image = ImageTk.PhotoImage(img) self.preview_canvas.delete("all") self.preview_canvas.config(width=img.width, height=img.height) self.preview_canvas.create_image(0, 0, anchor=tk.NW, image=self.preview_image) except Exception as e: messagebox.showerror("错误", f"无法加载图片: {str(e)}") def convert_preview(self, file_path): """换并预览线稿效果""" try: # 换图片 start_time = time.time() output_path = self.convert_to_line_art(file_path, self.edge_strength.get(), self.blur_strength.get(), preview=True) process_time = time.time() - start_time # 更新性能标签 self.perf_label.config(text=f"处理时间: {process_time:.2f}秒") if output_path: # 显示线稿 self.show_image(output_path) # 删除临时预览文件 os.remove(output_path) except Exception as e: messagebox.showerror("错误", f"换失败: {str(e)}") def cancel_processing(self): """取消处理过程""" if self.processing: self.cancel_processing = True self.progress_label.config(text="正在取消处理...") self.cancel_btn.config(state=tk.DISABLED) def start_processing(self): """开始处理选中的文件""" if self.processing: return if not self.file_paths: messagebox.showwarning("警告", "请先选择要处理的图片或文件夹") return # 禁用处理按钮,防止重复处理 self.processing = True self.cancel_processing = False self.process_btn.config(state=tk.DISABLED) self.cancel_btn.config(state=tk.NORMAL) # 初始化进度 self.total_files = len(self.file_paths) self.processed_files = 0 self.progress_var.set(0) self.progress_label.config(text=f"准备处理 {self.total_files} 个文件...") # 在新线程中处理文件,避免界面卡死 processing_thread = threading.Thread(target=self.process_files) processing_thread.daemon = True processing_thread.start() def update_progress(self, filename, success=True): """更新进度条和状态""" self.processed_files += 1 progress = (self.processed_files / self.total_files) * 100 self.progress_var.set(progress) status = "成功" if success else "失败" self.progress_label.config( text=f"已处理 {self.processed_files}/{self.total_files} 个文件 - {os.path.basename(filename)} {status}" ) # 处理完成后恢复按钮状态 if self.processed_files == self.total_files or self.cancel_processing: self.progress_label.config(text=f"处理完成!共处理 {self.processed_files} 个文件") self.process_btn.config(state=tk.NORMAL) self.cancel_btn.config(state=tk.DISABLED) self.processing = False if not self.cancel_processing: messagebox.showinfo("完成", f"所有文件处理完成!共处理 {self.processed_files} 个文件") else: messagebox.showinfo("取消", f"已取消处理!已完成 {self.processed_files} 个文件") def process_files(self): """处理所有选中的文件""" edge_strength = self.edge_strength.get() blur_strength = self.blur_strength.get() for i, file_path in enumerate(self.file_paths): if self.cancel_processing: break try: # 调用函数 start_time = time.time() result = self.convert_to_line_art(file_path, edge_strength, blur_strength) process_time = time.time() - start_time # 在主线程中更新UI self.root.after(0, lambda f=file_path, t=process_time: self.update_progress(f, True)) self.root.after(0, lambda t=process_time: self.perf_label.config( text=f"处理时间: {t:.2f}秒" if t > 0.1 else "处理时间: <0.1秒" )) except Exception as e: print(f"处理 {file_path} 时出错: {str(e)}") self.root.after(0, self.update_progress, file_path, False) # 处理完成或取消 self.root.after(0, lambda: self.update_progress("", True)) def create_edge_detection_kernel(self): """创建边缘检测卷积核""" # Sobel水平和垂直边缘检测核 sobel_x = torch.tensor([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], dtype=torch.float32).view(1, 1, 3, 3) sobel_y = torch.tensor([[1, 0, -1], [2, 0, -2], [1, 0, -1]], dtype=torch.float32).view(1, 1, 3, 3) return sobel_x, sobel_y def apply_style_effect(self, edge_tensor, style_name): """应用不同的线稿风格效果""" # 确保张量在正确的设备上 edge_tensor = edge_tensor.to(self.device) if style_name == "素描": # 素描效果:增强对比度 edge_tensor = torch.pow(edge_tensor, 0.7) elif style_name == "漫画": # 漫画效果:二值化处理 edge_tensor = (edge_tensor > 0.2).float() elif style_name == "水彩": # 水彩效果:轻微模糊 # 直接使用4D张量,不需要unsqueeze edge_tensor = gaussian_blur(edge_tensor, kernel_size=3, sigma=0.5) elif style_name == "钢笔": # 钢笔效果:细化线条 edge_tensor = torch.clamp(edge_tensor * 1.5, 0, 1) return edge_tensor def convert_to_line_art(self, image_path, edge_strength=1.2, blur_strength=1.0, preview=False): """ 优化的线稿换方法 """ try: # 1. 读取图像并为灰度图 with Image.open(image_path) as img: gray_img = img.convert(&#39;L&#39;) # 换为numpy数组 gray_np = np.array(gray_img, dtype=np.float32) / 255.0 # 换为张量,确保是4D张量 [批次=1, 通道=1, 高度, 宽度] gray_tensor = torch.from_numpy(gray_np).to(self.device) gray_tensor = gray_tensor.unsqueeze(0).unsqueeze(0) # 2. 应用高斯模糊减少噪声 if blur_strength > 0: # 使用torchvision的gaussian_blur函数 gray_tensor = gaussian_blur(gray_tensor, kernel_size=5, sigma=blur_strength) # 3. 应用Sobel边缘检测 sobel_x, sobel_y = self.create_edge_detection_kernel() sobel_x = sobel_x.to(self.device) sobel_y = sobel_y.to(self.device) edge_x = F.conv2d(gray_tensor, sobel_x, padding=1) edge_y = F.conv2d(gray_tensor, sobel_y, padding=1) # 4. 合并边缘并归一化 edge_tensor = torch.sqrt(edge_x**2 + edge_y**2) edge_tensor = edge_tensor / (edge_tensor.max() + 1e-8) # 5. 增强边缘强度 edge_tensor = torch.clamp(edge_tensor * edge_strength, 0.0, 1.0) # 6. 应用线稿风格 edge_tensor = self.apply_style_effect(edge_tensor, self.style_var.get()) # 7.得到线稿(黑线白底) line_art_tensor = 1.0 - edge_tensor # 8. 换为最终图像 line_art_np = np.squeeze(line_art_tensor.detach().cpu().numpy()) * 255 line_art_np = line_art_np.astype(np.uint8) line_art = Image.fromarray(line_art_np) # 9. 保存结果 if preview: # 预览模式下保存为临时文件 output_path = os.path.join(os.path.dirname(image_path), "preview_lineart.png") line_art.save(output_path, format=&#39;PNG&#39;) else: # 创建输出目录 output_dir = os.path.join(os.path.dirname(image_path), "line_art_output") os.makedirs(output_dir, exist_ok=True) # 根据选择的格式保存 base_name = os.path.splitext(os.path.basename(image_path))[0] if self.format_var.get().startswith("PNG"): output_path = os.path.join(output_dir, f"{base_name}_lineart.png") line_art.save(output_path, format=&#39;PNG&#39;, compress_level=1) else: quality = { "JPG (高质量)": 95, "JPG (中等质量)": 75, "JPG (低质量)": 50 }[self.format_var.get()] output_path = os.path.join(output_dir, f"{base_name}_lineart.jpg") line_art.save(output_path, format=&#39;JPEG&#39;, quality=quality) return output_path except Exception as e: print(f"处理图像时出错: {str(e)}") return None if __name__ == "__main__": root = tk.Tk() app = LineArtConverter(root) root.mainloop() ``` ### 关键修复点 #### 1. 修复水彩风格处理 ```python def apply_style_effect(self, edge_tensor, style_name): # ... elif style_name == "水彩": # 水彩效果:轻微模糊 # 直接使用4D张量,不需要unsqueeze edge_tensor = gaussian_blur(edge_tensor, kernel_size=3, sigma=0.5) # ... ``` #### 2. 添加设备一致性检查 ```python def apply_style_effect(self, edge_tensor, style_name): # 确保张量在正确的设备上 edge_tensor = edge_tensor.to(self.device) # ... ``` ### 其他改进 1. **增强错误处理**: ```python except Exception as e: print(f"处理图像时出错: {str(e)}") return None ``` 2. **优化图像处理流程**: - 统一使用PyTorch进行所有图像处理 - 确保所有张量都在同一设备上(CPU或GPU) - 优化内存管理 3. **添加必要的导入**: ```python import sys # 用于文件位置打开功能 ``` ### 使用说明 1. **安装依赖**: ```bash pip install torch torchvision pillow numpy ``` 2. **运行程序**: ```bash python line_art_converter.py ``` 3. **水彩风格使用**: - 选择水彩风格后,程序会自动应用轻微模糊效果 - 模糊强度可以通过滑块调整 - 效果预览会立即显示在预览窗口中 ### 不同风格效果说明 | 风格 | 效果描述 | 适用场景 | |------|----------|---------| | **素描** | 增强对比度,线条清晰 | 传统绘画、人物肖像 | | **漫画** | 二值化处理,线条鲜明 | 动漫、插画创作 | | **水彩** | 轻微模糊,柔和过渡 | 水彩画风格、艺术效果 | | **钢笔** | 细化线条,强调轮廓 | 速写、线描艺术 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值