3D-s számítógépes geometria és alakzatrekonstrukció

Tesztkörnyezet III

http://cg.iit.bme.hu/portal/node/312
https://portal.vik.bme.hu/kepzes/targyak/VIIIMA25

Dr. Várady Tamás, Dr. Salvi Péter
BME, Villamosmérnöki és Informatikai Kar
Irányítástechnika és Informatika Tanszék

Tartalom

  • Jobb normálisbecslés
  • Átlaggörbület-approximáció
    • Mesh-navigáció [OpenMesh]
    • Dialógusablakok felépítése [Qt]
      • Layoutok
    • Színezés [OpenGL]
  • Fényvonalak
    • Fényvonal-textúra kiszámítása
    • Resource-ok használata [Qt]
    • Textúrák használata [OpenGL]
      • Környezeti textúrák

Harmadik fázis

  • Átlaggörbület
    • Színkódolás [kék→zöld→piros]
    • Szélsőértékek meghatározása
      • Tartomány szélének levágása
      • Explicit megadás
  • Fényvonalak [bővebben később]
    • Dinamikus textúra
    • Kimutat apró egyenetlenségeket
  • Ehhez:
    • Qt [menü, dialóguskészítés]
    • OpenGL [színezés, textúra]
    • OpenMesh [háromszögháló bejárás]

Normálisok és görbületek számítása

class Object {
  // ...
protected:
  virtual Vector normal(BaseMesh::VertexHandle vh) const;
  virtual double meanCurvature(BaseMesh::VertexHandle vh) const;
};

void Object::updateBaseMesh() {
  mesh.request_face_normals();
  mesh.request_vertex_normals();
  mesh.update_face_normals();
  for (auto v : mesh.vertices()) {
    mesh.set_normal(v, normal(v));
    mesh.data(v).mean = meanCurvature(v); // `mean` defined in BaseTraits
  }
}

Jobb normálbecslés

  • Emlékeztető:
    • Lap-normálisok súlyozott átlaga
    • Súlyozás: egyenletes / területarányos
  • Ötlet: működjön jól gömbre!
    • Legyen és
    • Tfh. a pontok egy gömbön vannak
    • Normális irány: gömbközéppontból -be
      • A középpont pozíciója (negálva)
    • Hol a középpont?
      • Az élfelező merőleges síkok metszetében!

Jobb normálbecslés (folytatás)

  • Az élfelező síkok:

    azaz

  • Nézzük három szomszéd pontra (3 egyenlet)
    • Cramer-szabály segítségével a középpont:

      ahol az együttható-mátrix determinánsa
    • Csak az irány kell

Vector Object::normal(BaseMesh::VertexHandle vh) const {
  auto v = OpenMesh::make_smart(vh, &mesh);
  Vector n(0.0, 0.0, 0.0);
  for (auto h : v.incoming_halfedges()) {
    if (h.is_boundary())
      continue;
    auto in_vec  = mesh.calc_edge_vector(h);
    auto out_vec = mesh.calc_edge_vector(h.next());
    double w = in_vec.sqrnorm() * out_vec.sqrnorm();
    n += (in_vec % out_vec) / (w == 0.0 ? 1.0 : w);
  }
  double len = n.length();
  if (len != 0.0)
    n /= len;
  return n;
}

Átlaggörbület-approximáció (ismétlés)

  • Becslés háromszöglegyező alapján

  • Feladatok:
    • kiszámítása (és eltárolása)
    • szögek kiszámítása
    • kiszámítása

double Object::meanCurvature(BaseMesh::VertexHandle vh) const {
  auto v = OpenMesh::make_smart(vh, &mesh);

  // Compute triangle strip area
  double vertex_area = 0;
  for (auto f : v.faces())
    vertex_area += mesh.calc_sector_area(f.halfedge());
  vertex_area /= 3.0;

  // Compute mean values using dihedral angles
  double mean = 0;
  for (auto h : v.incoming_halfedges()) {
    auto vec = mesh.calc_edge_vector(h);
    double angle = mesh.calc_dihedral_angle(h); // signed; returns 0 at the boundary
    mean += angle * vec.norm();
  }
  return mean / (4 * vertex_area);
}

A színfalak mögött

  • A calc_dihedral_angle implementációja
  • Az OpenMesh/Core/Mesh/PolyMeshT.hh alapján
Scalar calc_dihedral_angle(HalfedgeHandle he) const {
  if (is_boundary(edge_handle(he)))
    return 0;
  auto n0 = normal(face_handle(he));
  auto n1 = normal(face_handle(opposite_halfedge_handle(he)));
  auto cos_angle = n0 | n1;
  auto sign = (n0 % n1) | calc_edge_vector(he);
  return std::acos(std::clamp(cos_angle, -1.0, 1.0)) * (sign < 0 ? -1 : 1);
}

Új vizualizációs módok

  • Átlaggörbület színezése min/max alapján
  • Min/max beállítása automatikusan (szélek levágásával)
  • Fényvonalakról (isophotes) bővebben később

visualization.hh

enum class VisType { PLAIN, MEAN, ISOPHOTES };

struct Visualization {
  VisType type;
  // ...
  double mean_min, mean_max, cutoff_ratio;
  static Vector colorMap(double min, double max, double d);
};

visualization.cc

static Vector HSV2RGB(Vector hsv) { // As in Wikipedia
  double c = hsv[2] * hsv[1], h = hsv[0] / 60, m = hsv[2] - c;
  double x = c * (1 - std::abs(std::fmod(h, 2) - 1));
  Vector rgb(m, m, m);
  if (h <= 1) return rgb + Vector(c, x, 0);
  if (h <= 2) return rgb + Vector(x, c, 0);
  if (h <= 3) return rgb + Vector(0, c, x);
  if (h <= 4) return rgb + Vector(0, x, c);
  if (h <= 5) return rgb + Vector(x, 0, c);
  if (h <= 6) return rgb + Vector(c, 0, x);
  return rgb;
}

Vector Visualization::colorMap(double min, double max, double d) {
  double red = 0, green = 120, blue = 240; // Hue
  if (d < 0) {
    double alpha = min ? std::min(d / min, 1.0) : 1.0;
    return HSV2RGB({green * (1 - alpha) + blue * alpha, 1, 1});
  }
  double alpha = max ? std::min(d / max, 1.0) : 1.0;
  return HSV2RGB({green * (1 - alpha) + red * alpha, 1, 1});
}

viewer.hh

class Viewer : public QGLViewer {
  Q_OBJECT
public:
  double getCutoffRatio() const;
  void setCutoffRatio(double ratio);
  double getMeanMin() const;
  void setMeanMin(double min);
  double getMeanMax() const;
  void setMeanMax(double max);
  // ...
private:
  void updateMeanMinMax();
  // ...
};

viewer.cc

double Viewer::getCutoffRatio() const { return vis.cutoff_ratio; }
void Viewer::setCutoffRatio(double ratio) {
  vis.cutoff_ratio = ratio;
  updateMeanMinMax();
}
double Viewer::getMeanMin() const { return vis.mean_min; }
void Viewer::setMeanMin(double min) { vis.mean_min = min; }
double Viewer::getMeanMax() const { return vis.mean_max; }
void Viewer::setMeanMax(double max) { vis.mean_max = max; }

bool Viewer::open(std::string filename) {
  // ...
  updateMeanMinMax();
  setupCamera();
  return true;
}

viewer.cc (folytatás)

void Viewer::updateMeanMinMax() {
  std::vector<double> mean;
  for (auto o : objects) {
    const auto &mesh = o->baseMesh();
    for (auto v : mesh.vertices())
      mean.push_back(mesh.data(v).mean);
  }

  size_t n = mean.size();
  if (n < 3)
    return;

  std::sort(mean.begin(), mean.end());
  size_t k = (double)n * vis.cutoff_ratio;
  vis.mean_min = std::min(mean[k ? k-1 : 0], 0.0);
  vis.mean_max = std::max(mean[k ? n-k : n-1], 0.0);
}

Billentyűkezelés

void Viewer::keyPressEvent(QKeyEvent *e) {
  if (e->modifiers() == Qt::NoModifier)
    switch (e->key()) {
    case Qt::Key_P:
      vis.type = VisType::PLAIN;
      update();
      break;
    case Qt::Key_M:
      vis.type = VisType::MEAN;
      update();
      break;
    case Qt::Key_I:
      vis.type = VisType::ISOPHOTES;
      update();
      break;
    // ...
    default:
      QGLViewer::keyPressEvent(e);
    } else
    QGLViewer::keyPressEvent(e);
}

Színes rajzolás

void Object::draw(const Visualization &vis) const {
  // ...
  if (vis.show_solid || vis.show_wireframe) {
    if (vis.type == VisType::PLAIN)
      glColor3d(1.0, 1.0, 1.0);
    for (auto f : mesh.faces()) {
      glBegin(GL_POLYGON);
      for (auto v : f.vertices()) {
        if (vis.type == VisType::MEAN)
          glColor3dv(vis.colorMap(vis.mean_min, vis.mean_max, mesh.data(v).mean).data());
        glNormal3dv(mesh.normal(v).data());
        glVertex3dv(mesh.point(v).data());
      }
      glEnd();
    }
  }
  // ...
}

Dialógusok paraméterek beállítására

  • Menüpontok készítése
  • Ablakok készítése
  • Régi értékekkel feltöltés
  • Ok nyomására új értékek elmentése
class Window : public QMainWindow {
  // ...
private slots:
  void setCutoff();
  void setRange();
  // ...
};

Visualization menü

Window::Window(QApplication *parent) :
  QMainWindow(), parent(parent), last_directory(".")
{
  // ...
  auto cutoffAction = new QAction(tr("Set &cutoff ratio"), this);
  cutoffAction->setStatusTip(tr("Set mean map cutoff ratio"));
  connect(cutoffAction, &QAction::triggered, this, &Window::setCutoff);

  auto rangeAction = new QAction(tr("Set &range"), this);
  rangeAction->setStatusTip(tr("Set mean map range"));
  connect(rangeAction, &QAction::triggered, this, &Window::setRange);

  auto visMenu = menuBar()->addMenu(tr("&Visualization"));
  visMenu->addAction(cutoffAction);
  visMenu->addAction(rangeAction);
}

Kitérő - layoutok kezelése

  • Widgetek elhelyezése

  • QWidget::setLayout [a legkülső layout]

  • QLayout::addWidget, QLayout::addLayout

  • QBoxLayout, QGridLayout, QFormLayout

    BoxLayout
    GridLayout FormLayout

  • Jó alapméretek, átméretezés kezelése

  • Saját widgetekhez sizeHint()

"Cutoff ratio" dialógus ablak

void Window::setCutoff() {
  auto dlg = std::make_unique<QDialog>(this);
  auto *hb1 = new QHBoxLayout, *hb2 = new QHBoxLayout;
  auto *vb = new QVBoxLayout;
  auto *text = new QLabel(tr("Cutoff ratio:"));
  auto *sb = new QDoubleSpinBox;
  auto *cancel = new QPushButton(tr("Cancel")), *ok = new QPushButton(tr("Ok"));

  sb->setDecimals(3);      sb->setRange(0.001, 0.5);
  sb->setSingleStep(0.01); sb->setValue(viewer->getCutoffRatio());
  connect(cancel, &QPushButton::pressed, dlg.get(), &QDialog::reject);
  connect(ok,     &QPushButton::pressed, dlg.get(), &QDialog::accept);
  ok->setDefault(true);

  hb1->addWidget(text);   hb1->addWidget(sb);
  hb2->addWidget(cancel); hb2->addWidget(ok);
  vb->addLayout(hb1);     vb->addLayout(hb2);

  dlg->setWindowTitle(tr("Set ratio"));
  dlg->setLayout(vb);

  if(dlg->exec() == QDialog::Accepted) {
    viewer->setCutoffRatio(sb->value());
    viewer->update();
  }
}

"Range" dialógus ablak

void Window::setRange() {
  QDialog dlg(this);
  auto *grid = new QGridLayout;
  auto *text1 = new QLabel(tr("Min:")), *text2 = new QLabel(tr("Max:"));
  auto *sb1 = new QDoubleSpinBox, *sb2 = new QDoubleSpinBox;
  auto *cancel = new QPushButton(tr("Cancel")), *ok = new QPushButton(tr("Ok"));

  double max = 1000.0; // controls the number of displayable digits
  sb1->setDecimals(5);                 sb2->setDecimals(5);
  sb1->setRange(-max, 0.0);            sb2->setRange(0.0, max);
  sb1->setSingleStep(0.01);            sb2->setSingleStep(0.01);
  sb1->setValue(viewer->getMeanMin()); sb2->setValue(viewer->getMeanMax());
  connect(cancel, &QPushButton::pressed, &dlg, &QDialog::reject);
  connect(ok,     &QPushButton::pressed, &dlg, &QDialog::accept);
  ok->setDefault(true);

  grid->addWidget( text1, 1, 1, Qt::AlignRight); grid->addWidget(   sb1, 1, 2);
  grid->addWidget( text2, 2, 1, Qt::AlignRight); grid->addWidget(   sb2, 2, 2);
  grid->addWidget(cancel, 3, 1);                 grid->addWidget(    ok, 3, 2);

  dlg.setWindowTitle(tr("Set range"));
  dlg.setLayout(grid);

  if(dlg.exec() == QDialog::Accepted) {
    viewer->setMeanMin(sb1->value()); viewer->setMeanMax(sb2->value());
    viewer->update();
  }
}

Tükröződési és fényvonalak (isophotes)

  • Struktúrált fények visszaverődései
  • Intuitív / hiba
  • Folytonos, sima:
  • Folytonos, tört:
    isophotes

Fényvonalak (isophotes)

  • Egyszerűbben számolható
  • Referencia pontból sugarak
    isophote
  • Beesési szög a normálvektorral:

  • Megjelenítés: piros/fehér 5 fokonként
  • Probléma:
    • Referencia pont változik → textúra változik
    • Új textúrát generálni lassú

OpenGL trükk

  • Környezeti térkép (environment map)

    • Dinamikus textúra
    • A felületi pont textúra pont hozzárendelés
      a nézőponttól és a normálvektortól függ
    • Ötlet: referenciapont = nézőpont

    axes isophote

Gömb leképzés

  • a visszaverődés iránya [szem-koordinátarendszerben]
  • Ehhez rendelt textúra (-ben):

    ahol
  • Lekódolja az összes irányt
    • 3 koordináta, de egységvektor → két szabadságfok
    • Ez a kódolás "látványos" (projekció)
    • Könnyen rátehető panoráma is

Fényvonalak kiszámítása

  • Feladat: textúrakoordináta szög
    axes isophote
  • egységvektor, tehát

    és , mivel épp a referenciairány
  • Ez alapján kiszínezhető a textúra
  • Képfájlként fogjuk beolvasni

Resource fájlok

textures.qrc

<!DOCTYPE RCC><RCC version="1.0">
<qresource>
  <file>isophotes.png</file>
</qresource>
</RCC>
  • Belefordulnak a futtatható állományba
  • Tipikusan képek, szövegek
  • Adat-forrásfájl generálódik belőlük
  • Qt-s fájlműveleteknél :/ elérési út
    (persze előbb hozzá kell adni a projekthez)

OpenGL textúra áttekintés

  • Textúra ID
    • Generálás (n db.): glGenTextures(n, addr)
    • Hozzárendelés: glBindTexture(type, id)
    • Törlés (n db.): glDeleteTextures(n, addr)
  • Paraméterek (glTexParameter[if]v?)
    • Külső pontra mit csináljon (pl. csempézés)
    • Minőségjavító min/mag filterek stb.
  • Környezeti beállítások (glTexEnv[if]v?)
    • Textúra, árnyalás és anyag (szín) kapcsolata
  • Textúra készítese
    • glTexImage2D(...sok paraméter...)

Textúra ID tárolása

visualization.hh

struct Visualization {
  static GLuint isophote_texture;
  ...
};

visualization.cc

GLuint Visualization::isophote_texture;

Textúra törlése

class Viewer : public QGLViewer {
  Q_OBJECT
public:
  ~Viewer();
  // ...
};
Viewer::~Viewer() {
  glDeleteTextures(1, &vis.isophote_texture);
}

Textúra készítése

void Viewer::init() {
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);

  QImage img(":/isophotes.png");
  glGenTextures(1, &vis.isophote_texture);
  glBindTexture(GL_TEXTURE_2D, vis.isophote_texture);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.width(), img.height(), 0, GL_BGRA,
               GL_UNSIGNED_BYTE, img.convertToFormat(QImage::Format_ARGB32).bits());
}
  • GL_LINEAR (lineáris interpoláció):
    • MIN: távoli felületek
    • MAG: közeli felületek
  • GL_CLAMP_TO_EDGE:
    • Kívül eső pontokat a szélére húzza

OpenGL textúra koordináták

  • glEnable(GL_TEXTURE_2D)
  • Megadható pontonként
    • glTexCoord[1234][dfis]v?
    • Az aktuális (bind-olt) textúrára vonatkozik
  • Generált koordináták
    • glTexGen[dfi]v? : generálás paraméterei
      • GL_TEXTURE_GEN_MODE : mód beállítása
        • GL_OBJECT_LINEAR
        • GL_EYE_LINEAR
        • GL_SPHERE_MAP
      • GL_OBJECT_PLANE / GL_EYE_PLANE : sík megadása
    • glEnable(GL_TEXTURE_GEN_S), glEnable(GL_TEXTURE_GEN_T)

Rajzolás textúrával

void Object::draw(const Visualization &vis) const {
  // ...
  if (vis.show_solid || vis.show_wireframe) {
    if (vis.type == VisType::PLAIN)
      glColor3d(1.0, 1.0, 1.0);
    else if (vis.type == VisType::ISOPHOTES) {
      glBindTexture(GL_TEXTURE_2D, vis.isophote_texture);
      glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
      glEnable(GL_TEXTURE_2D);
      glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
      glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
      glEnable(GL_TEXTURE_GEN_S);
      glEnable(GL_TEXTURE_GEN_T);
    }
    // ... (actual drawing) ...
    if (vis.type == VisType::ISOPHOTES) {
      glDisable(GL_TEXTURE_GEN_S);
      glDisable(GL_TEXTURE_GEN_T);
      glDisable(GL_TEXTURE_2D);
      glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    }
  }
  // ...
}

Kész a harmadik fázis

Phase 3 complete