Как сопоставить трехмерную перспективу реальной фотографии и объекта в CSS3 3D-преобразованиях
Ситуация:
В моем конкретном случае у меня есть 3D фоторамка и 2D фото. И я хочу, чтобы 2D-фотография соответствовала 3D-кадру, используя функции преобразования CSS3 (Rotate, Scale, Skew).
Эта проблема:
Я не смог точно сопоставить их, используя ручной метод, то есть набрав значение вращения и наблюдая за тем, что он делает.
Идеальное решение № 1
Существует онлайн визуальный инструмент, который позволяет перетаскивать углы фотографии (как это делает фотошоп), и он дает вам правильные значения CSS3-преобразования.
Идеальное решение № 2
Существует невизуальный инструмент - такой же, как и раньше, но вы вручную вводите 4 точечные координаты (углы изображения), и он дает вам правильные значения CSS3-преобразования.
Реальное решение этого вопроса
Если таких инструментов нет (мой поиск не нашел ни одного), я бы хотел, чтобы кто-нибудь попытался объяснить математику, стоящую за этим, чтобы я мог сам рассчитать их - если это вообще возможно?
Я подготовил демоверсию JSFiddle для вас:
/* Main issue here */
.transform {
transform: rotateX(34deg) rotateZ(13deg) rotateY(-10deg) scaleY(1) scaleX(1) skewY(0deg) skewX(0deg) translateY(0px) translateX(20px);
transform-origin: 50% 0% 0;
}
/* Supporting styles */
.container {
position: relative;
width: 500px;
height: 500px;
}
.frame,
.photo {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.photo {
top: 50px;
left: 95px;
right: 65px;
bottom: 270px;
}
.frame img,
.photo img {
width: 100%
}
.frame {
z-index: 2;
}
<div class="container">
<div class="frame">
<img src="http://cdn.idesigned.cz/img/cc08acc7b9b08ab53bf935d720210f13.png" />
</div>
<div class="photo">
<div class="transform">
<img src="https://static.pexels.com/photos/7976/pexels-photo.jpg" />
</div>
</div>
</div>
4 ответа
Если вы можете использовать трехмерные преобразования (такие как rotateZ
), то вы также можете предоставить matrix3d
который вы можете вычислить из желаемых точечных соответствий.
Вот скрипка: https://jsfiddle.net/szym/03s5mwjv/
Я использую numeric.js, чтобы решить набор из 4 линейных уравнений, чтобы найти матрицу преобразования перспективы, которая преобразует src
на dst
, Это по сути та же математика, что и в getPerspectiveTransform
в OpenCV.
Вычисленное двумерное перспективное преобразование представляет собой матрицу 3x3 с использованием однородных координат. CSS matrix3d
является матрицей 4x4, использующей однородные координаты, поэтому нам нужно добавить единичную строку / столбец для z
ось. Более того, matrix3d
указывается в главном порядке столбцов.
Как только вы получите matrix3d
Вы можете просто вставить его в таблицу стилей. Но имейте в виду, что матрица вычисляется в предположении (0, 0)
как происхождение, поэтому вам также нужно установить transformOrigin: 0 0
,
// Computes the matrix3d that maps src points to dst.
function computeTransform(src, dst) {
// src and dst should have length 4 each
var count = 4;
var a = []; // (2*count) x 8 matrix
var b = []; // (2*count) vector
for (var i = 0; i < 2 * count; ++i) {
a.push([0, 0, 0, 0, 0, 0, 0, 0]);
b.push(0);
}
for (var i = 0; i < count; ++i) {
var j = i + count;
a[i][0] = a[j][3] = src[i][0];
a[i][1] = a[j][4] = src[i][1];
a[i][2] = a[j][5] = 1;
a[i][3] = a[i][4] = a[i][5] =
a[j][0] = a[j][1] = a[j][2] = 0;
a[i][6] = -src[i][0] * dst[i][0];
a[i][7] = -src[i][1] * dst[i][0];
a[j][6] = -src[i][0] * dst[i][1];
a[j][7] = -src[i][1] * dst[i][1];
b[i] = dst[i][0];
b[j] = dst[i][1];
}
var x = numeric.solve(a, b);
// matrix3d is homogeneous coords in column major!
// the z coordinate is unused
var m = [
x[0], x[3], 0, x[6],
x[1], x[4], 0, x[7],
0, 0, 1, 0,
x[2], x[5], 0, 1
];
var transform = "matrix3d(";
for (var i = 0; i < m.length - 1; ++i) {
transform += m[i] + ", ";
}
transform += m[15] + ")";
return transform;
}
// Collect the four corners by user clicking in the corners
var dst = [];
document.getElementById('frame').addEventListener('mousedown', function(evt) {
// Make sure the coordinates are within the target element.
var box = evt.target.getBoundingClientRect();
var point = [evt.clientX - box.left, evt.clientY - box.top];
dst.push(point);
if (dst.length == 4) {
// Once we have all corners, compute the transform.
var img = document.getElementById('img');
var w = img.width,
h = img.height;
var transform = computeTransform(
[
[0, 0],
[w, 0],
[w, h],
[0, h]
],
dst
);
document.getElementById('photo').style.visibility = 'visible';
document.getElementById('transform').style.transformOrigin = '0 0';
document.getElementById('transform').style.transform = transform;
document.getElementById('result').innerHTML = transform;
}
});
.container {
position: relative;
width: 50%;
}
#frame,
#photo {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#photo {
visibility: hidden;
}
#frame img,
#photo img {
width: 100%
}
#photo {
opacity: 0.7;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/numeric/1.2.6/numeric.min.js"></script>
<p id="result">Click the desired top-left, top-right, bottom-right, bottom-left corners
<div class="container">
<div id="frame">
<img src="http://cdn.idesigned.cz/img/cc08acc7b9b08ab53bf935d720210f13.png" />
</div>
<div id="photo">
<div id="transform">
<img id="img" src="http://placehold.it/350x150" />
</div>
</div>
</div>
Я написал ответ на Math SE о том, как вычислить матрицу преобразования, чтобы сопоставить углы одного изображения с четырьмя заданными координатами, используя проективное преобразование. В нем есть детали того, что на самом деле происходит в этих вычислениях. В нем также есть немного CSS, и я адаптировал оригинальную демонстрацию к вашему примеру сценария:
function adj(m) { // Compute the adjugate of m
return [
m[4]*m[8]-m[5]*m[7], m[2]*m[7]-m[1]*m[8], m[1]*m[5]-m[2]*m[4],
m[5]*m[6]-m[3]*m[8], m[0]*m[8]-m[2]*m[6], m[2]*m[3]-m[0]*m[5],
m[3]*m[7]-m[4]*m[6], m[1]*m[6]-m[0]*m[7], m[0]*m[4]-m[1]*m[3]
];
}
function multmm(a, b) { // multiply two matrices
var c = Array(9);
for (var i = 0; i != 3; ++i) {
for (var j = 0; j != 3; ++j) {
var cij = 0;
for (var k = 0; k != 3; ++k) {
cij += a[3*i + k]*b[3*k + j];
}
c[3*i + j] = cij;
}
}
return c;
}
function multmv(m, v) { // multiply matrix and vector
return [
m[0]*v[0] + m[1]*v[1] + m[2]*v[2],
m[3]*v[0] + m[4]*v[1] + m[5]*v[2],
m[6]*v[0] + m[7]*v[1] + m[8]*v[2]
];
}
function basisToPoints(x1, y1, x2, y2, x3, y3, x4, y4) { // map basis to these points
var m = [
x1, x2, x3,
y1, y2, y3,
1, 1, 1
];
var v = multmv(adj(m), [x4, y4, 1]);
return multmm(m, [
v[0], 0, 0,
0, v[1], 0,
0, 0, v[2]
]);
}
function general2DProjection(
x1s, y1s, x1d, y1d,
x2s, y2s, x2d, y2d,
x3s, y3s, x3d, y3d,
x4s, y4s, x4d, y4d
) {
console.log(Array.prototype.join.call(arguments, ", "));
var s = basisToPoints(x1s, y1s, x2s, y2s, x3s, y3s, x4s, y4s);
var d = basisToPoints(x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d);
return multmm(d, adj(s));
}
function transform2d(elt, x1, y1, x2, y2, x3, y3, x4, y4) {
var w = elt.offsetWidth, h = elt.offsetHeight;
var t = general2DProjection
(0, 0, x1, y1, w, 0, x2, y2, 0, h, x3, y3, w, h, x4, y4);
for(i = 0; i != 9; ++i) t[i] = t[i]/t[8];
t = [t[0], t[3], 0, t[6],
t[1], t[4], 0, t[7],
0 , 0 , 1, 0 ,
t[2], t[5], 0, t[8]];
t = "matrix3d(" + t.join(", ") + ")";
elt.style["-webkit-transform"] = t;
elt.style["-moz-transform"] = t;
elt.style["-o-transform"] = t;
elt.style.transform = t;
}
corners = [100, 50, 300, 50, 100, 150, 300, 150];
function update() {
var box = document.getElementById("photo");
transform2d(box, corners[0], corners[1], corners[2], corners[3],
corners[4], corners[5], corners[6], corners[7]);
for (var i = 0; i != 8; i += 2) {
var elt = document.getElementById("marker" + i);
elt.style.left = corners[i] + "px";
elt.style.top = corners[i + 1] + "px";
}
document.getElementById("matrix").textContent = box.style.transform;
}
function move(evnt) {
if (currentcorner < 0) return;
corners[currentcorner] = evnt.pageX;
corners[currentcorner + 1] = evnt.pageY;
console.log(corners);
update();
}
currentcorner = -1;
window.addEventListener('load', function() {
document.documentElement.style.margin="0px";
document.documentElement.style.padding="0px";
document.body.style.margin="0px";
document.body.style.padding="0px";
update();
});
window.addEventListener('mousedown', function(evnt) {
var x = evnt.pageX, y = evnt.pageY, dx, dy;
var best = 400; // 20px grab radius
currentcorner = -1;
for (var i = 0; i != 8; i += 2) {
dx = x - corners[i];
dy = y - corners[i + 1];
if (best > dx*dx + dy*dy) {
best = dx*dx + dy*dy;
currentcorner = i;
}
}
move(evnt);
evnt.preventDefault();
}, true);
window.addEventListener('mouseup', function(evnt) {
currentcorner = -1;
}, true)
window.addEventListener('mousemove', move, true);
/* Supporting styles */
#photo {
position: absolute;
z-index: 1;
transform-origin: 0% 0% 0;
}
.dot {
position: absolute;
z-index: 2;
margin: -0.5ex;
padding: 0ex;
width: 1ex;
height: 1ex;
border-radius: 0.5ex;
background-color: #ff0000;
}
<img id="photo" src="https://static.pexels.com/photos/7976/pexels-photo.jpg" />
<img class="frame" src="http://cdn.idesigned.cz/img/cc08acc7b9b08ab53bf935d720210f13.png" />
<div class="dot" id="marker0"></div>
<div class="dot" id="marker2"></div>
<div class="dot" id="marker4"></div>
<div class="dot" id="marker6"></div>
<div id="matrix"></div>
Формулировка такова, что вы можете заставить ее работать всего с тремя арифметическими операциями: +
, -
а также *
, Тебе даже не нужно /
(если вы используете вспомогательные вместо обратных матриц), гораздо меньше различий в регистрах, квадратных корней или любых других подобных вещей.
Если вы предпочитаете переполнение стека (и для получения перекрестной ссылки), смотрите Перерисовать изображение с 3D-перспективы на 2d.
На основе кода @szym я создал демонстрацию для своих нужд, которую можно улучшить. В коде используются перетаскиваемые точки, поэтому их легче находить и вычислять матрицу. Также слева напечатана матрица.
// Computes the matrix3d that maps src points to dst.
function compute_transform(src, dst) {
// src and dst should have length 4 each
var count = 4;
var a = []; // (2*count) x 8 matrix
var b = []; // (2*count) vector
for (var i = 0; i < 2 * count; ++i) {
a.push([0, 0, 0, 0, 0, 0, 0, 0]);
b.push(0);
}
for (var i = 0; i < count; ++i) {
var j = i + count;
a[i][0] = a[j][3] = src[i][0];
a[i][1] = a[j][4] = src[i][1];
a[i][2] = a[j][5] = 1;
a[i][3] = a[i][4] = a[i][5] =
a[j][0] = a[j][1] = a[j][2] = 0;
a[i][6] = -src[i][0] * dst[i][0];
a[i][7] = -src[i][1] * dst[i][0];
a[j][6] = -src[i][0] * dst[i][1];
a[j][7] = -src[i][1] * dst[i][1];
b[i] = dst[i][0];
b[j] = dst[i][1];
}
var x = numeric.solve(a, b);
// matrix3d is homogenous coords in column major!
// the z coordinate is unused
var m = [
x[0], x[3], 0, x[6],
x[1], x[4], 0, x[7],
0, 0, 1, 0,
x[2], x[5], 0, 1
];
return "matrix3d(" + m.join(',') + ')';
}
// Collect the four corners by user clicking in the corners:
var points = [];
// map flatten the array
$('.point').each(function() {
var {left, top} = $(this).position();
points.push([left, top]);
});
transform_terminal();
$('.point').each(function(i) {
var drag = false;
var [container] = $('.laptop');
var $point = $(this).mousedown(function() {
drag = true;
});
$(document).on('mouseup', function() {
drag = false;
}).on('mousemove', function(event) {
if (drag) {
var box = container.getBoundingClientRect();
var x = event.clientX - box.left;
var y = event.clientY - box.top;
points[i] = [x, y];
$point.css({
left: x,
top: y
});
transform_terminal();
}
});
});
function transform_terminal() {
var w = gemetry.width + 20,
h = gemetry.height + 20;
var transform = compute_transform(
[
[0, 0],
[w, 0],
[w, h],
[0, h]
],
points
);
$('.output pre').html(`
.terminal {
transform: ${transform};
}
`.trim())
term.css({
'--transform': transform
});
}
См. Демонстрацию в разделе «Действие». Я использую его для сопоставления терминала jQuery с изображением ноутбука.
Я могу себе представить, что трудно найти инструмент или формулу для этого. Если вы знаете углы и меры книги, я думаю, что это легко вычислить. W3C Working Draft здесь вы можете найти более подробную информацию о преобразованиях, но, как я уже сказал, без подробностей вашего изображения (книги) я понятия не имею, как вы можете рассчитать точные координаты.
Единственное, что может вам помочь - это перспектива css. Я сделал Plunk, чтобы вы могли видеть, как это выглядит с ним.
Единственное, что я изменил:
.container {
position: relative;
width: 500px;
height: 500px;
perspective: 500px;
}
а также
.transform{
-webkit-transform: rotateX(19deg) rotateZ(6deg) rotateY(0deg) scaleY(0.85) scaleX(0.85) skewY(0deg) skewX(-8deg) translateY(-10px) translateX(33px);
-webkit-transform-origin: 50% 0% 0;
transform: rotateX(19deg) rotateZ(6deg) rotateY(0deg) scaleY(0.85) scaleX(0.85) skewY(0deg) skewX(-8deg) translateY(-10px) translateX(33px);
transform-origin: 50% 0% 0;
}
Фотография не помещается точно в рамку, потому что это квадрат, но я надеюсь, что это поможет вам немного дальше.