You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-03-02 08:53:52 -08:00
Adding fisheye mode to spherical projection effect, and cleaning up code.
This commit is contained in:
@@ -15,16 +15,18 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#endif
|
||||
|
||||
using namespace openshot;
|
||||
|
||||
SphericalProjection::SphericalProjection()
|
||||
: yaw(0.0), pitch(0.0), roll(0.0), fov(90.0),
|
||||
projection_mode(0), interpolation(0)
|
||||
: yaw(0.0)
|
||||
, pitch(0.0)
|
||||
, roll(0.0)
|
||||
, fov(90.0)
|
||||
, projection_mode(0)
|
||||
, invert(0)
|
||||
, interpolation(0)
|
||||
{
|
||||
init_effect_details();
|
||||
}
|
||||
@@ -33,8 +35,8 @@ SphericalProjection::SphericalProjection(Keyframe new_yaw,
|
||||
Keyframe new_pitch,
|
||||
Keyframe new_roll,
|
||||
Keyframe new_fov)
|
||||
: yaw(new_yaw), pitch(new_pitch), roll(new_roll), fov(new_fov),
|
||||
projection_mode(0), interpolation(0)
|
||||
: yaw(new_yaw), pitch(new_pitch), roll(new_roll)
|
||||
, fov(new_fov), projection_mode(0), invert(0), interpolation(0)
|
||||
{
|
||||
init_effect_details();
|
||||
}
|
||||
@@ -44,11 +46,9 @@ void SphericalProjection::init_effect_details()
|
||||
InitEffectInfo();
|
||||
info.class_name = "SphericalProjection";
|
||||
info.name = "Spherical Projection";
|
||||
info.description = "Flatten 360° video with yaw/pitch/roll, sphere or hemisphere mode, nearest or bilinear";
|
||||
info.description = "Flatten and reproject 360° video with yaw, pitch, roll, and fov (sphere, hemisphere, fisheye modes)";
|
||||
info.has_audio = false;
|
||||
info.has_video = true;
|
||||
|
||||
// Keyframes auto-registered
|
||||
}
|
||||
|
||||
std::shared_ptr<openshot::Frame> SphericalProjection::GetFrame(
|
||||
@@ -61,85 +61,102 @@ std::shared_ptr<openshot::Frame> SphericalProjection::GetFrame(
|
||||
|
||||
int W = img->width(), H = img->height();
|
||||
int bpl = img->bytesPerLine();
|
||||
uchar *src = img->bits();
|
||||
uchar* src = img->bits();
|
||||
|
||||
QImage output(W, H, QImage::Format_ARGB32);
|
||||
output.fill(Qt::black);
|
||||
uchar *dst = output.bits();
|
||||
uchar* dst = output.bits();
|
||||
int dst_bpl = output.bytesPerLine();
|
||||
|
||||
// read keyframes & convert
|
||||
// Evaluate keyframes (note roll is inverted + offset 180°)
|
||||
double yaw_r = yaw.GetValue(frame_number) * M_PI/180.0;
|
||||
double pitch_r = pitch.GetValue(frame_number) * M_PI/180.0;
|
||||
double roll_r = roll.GetValue(frame_number) * M_PI/180.0;
|
||||
double roll_r = -roll.GetValue(frame_number) * M_PI/180.0 + M_PI;
|
||||
double fov_r = fov.GetValue(frame_number) * M_PI/180.0;
|
||||
|
||||
// rotation matrix R = Ry * Rx * Rz
|
||||
double sy=sin(yaw_r), cy=cos(yaw_r),
|
||||
sp=sin(pitch_r),cp=cos(pitch_r),
|
||||
sr=sin(roll_r), cr=cos(roll_r);
|
||||
double r00=cy*cr+sy*sp*sr, r01=-cy*sr+sy*sp*cr, r02=sy*cp;
|
||||
double r10=cp*sr, r11=cp*cr, r12=-sp;
|
||||
double r20=-sy*cr+cy*sp*sr, r21=sy*sr+cy*sp*cr, r22=cy*cp;
|
||||
// Build composite rotation matrix R = Ry * Rx * Rz
|
||||
double sy = sin(yaw_r), cy = cos(yaw_r);
|
||||
double sp = sin(pitch_r), cp = cos(pitch_r);
|
||||
double sr = sin(roll_r), cr = cos(roll_r);
|
||||
|
||||
// fov scalars
|
||||
double hx = tan(fov_r/2.0);
|
||||
double vy = hx * (double(H)/W);
|
||||
double r00 = cy*cr + sy*sp*sr, r01 = -cy*sr + sy*sp*cr, r02 = sy*cp;
|
||||
double r10 = cp*sr, r11 = cp*cr, r12 = -sp;
|
||||
double r20 = -sy*cr + cy*sp*sr, r21 = sy*sr + cy*sp*cr, r22 = cy*cp;
|
||||
|
||||
// Precompute perspective scalars
|
||||
double hx = tan(fov_r*0.5);
|
||||
double vy = hx * double(H)/W;
|
||||
|
||||
#ifdef _OPENMP
|
||||
#pragma omp parallel for schedule(static)
|
||||
#endif
|
||||
for(int yy=0; yy<H; yy++) {
|
||||
uchar *dst_row = dst + yy*dst_bpl;
|
||||
double ndc_y = (2.0*(yy+0.5)/H - 1.0) * vy;
|
||||
for(int xx=0; xx<W; xx++) {
|
||||
double ndc_x = (2.0*(xx+0.5)/W - 1.0) * hx;
|
||||
// camera ray
|
||||
double vx=ndc_x, vy2=-ndc_y, vz=-1.0;
|
||||
double inv = 1.0/sqrt(vx*vx+vy2*vy2+vz*vz);
|
||||
vx*=inv; vy2*=inv; vz*=inv;
|
||||
// rotate
|
||||
for (int yy = 0; yy < H; yy++) {
|
||||
uchar* dst_row = dst + yy * dst_bpl;
|
||||
double ndc_y = (2.0*(yy + 0.5)/H - 1.0) * vy;
|
||||
|
||||
for (int xx = 0; xx < W; xx++) {
|
||||
// Generate ray in camera space
|
||||
double ndc_x = (2.0*(xx + 0.5)/W - 1.0) * hx;
|
||||
double vx = ndc_x, vy2 = -ndc_y, vz = -1.0;
|
||||
double inv = 1.0/sqrt(vx*vx + vy2*vy2 + vz*vz);
|
||||
vx *= inv; vy2 *= inv; vz *= inv;
|
||||
|
||||
// Rotate ray into world coordinates
|
||||
double dx = r00*vx + r01*vy2 + r02*vz;
|
||||
double dy = r10*vx + r11*vy2 + r12*vz;
|
||||
double dz = r20*vx + r21*vy2 + r22*vz;
|
||||
// spherical coords
|
||||
double lon = atan2(dx, dz);
|
||||
double lat = asin(dy);
|
||||
|
||||
// adjust for hemisphere
|
||||
if (projection_mode == 1) {
|
||||
lon = std::clamp(lon, -M_PI/2.0, M_PI/2.0);
|
||||
// For sphere/hemisphere, optionally invert view by 180°
|
||||
if (projection_mode < 2 && invert) {
|
||||
dx = -dx;
|
||||
dz = -dz;
|
||||
}
|
||||
|
||||
// UV mapping
|
||||
double uf = ((lon + (projection_mode?M_PI/2.0:M_PI))
|
||||
/ (projection_mode?M_PI:2.0*M_PI)) * W;
|
||||
double vf = (lat + M_PI/2.0) / M_PI * H;
|
||||
double uf, vf;
|
||||
|
||||
if (projection_mode == 2) {
|
||||
// Fisheye mode: invert circular fisheye
|
||||
double ax = 0.0, ay = 0.0, az = invert ? -1.0 : 1.0;
|
||||
double cos_t = dx*ax + dy*ay + dz*az;
|
||||
double theta = acos(cos_t);
|
||||
double rpx = (theta / fov_r) * (W/2.0);
|
||||
double phi = atan2(dy, dx);
|
||||
uf = W*0.5 + rpx*cos(phi);
|
||||
vf = H*0.5 + rpx*sin(phi);
|
||||
}
|
||||
else {
|
||||
// Sphere or hemisphere: equirectangular sampling
|
||||
double lon = atan2(dx, dz);
|
||||
double lat = asin(dy);
|
||||
if (projection_mode == 1) // hemisphere
|
||||
lon = std::clamp(lon, -M_PI/2.0, M_PI/2.0);
|
||||
uf = ((lon + (projection_mode? M_PI/2.0 : M_PI))
|
||||
/ (projection_mode? M_PI : 2.0*M_PI)) * W;
|
||||
vf = (lat + M_PI/2.0)/M_PI * H;
|
||||
}
|
||||
|
||||
uchar* d = dst_row + xx*4;
|
||||
|
||||
// sampling
|
||||
if (interpolation == 0) {
|
||||
int sx = std::clamp(int(std::floor(uf)),0,W-1);
|
||||
int sy2= std::clamp(int(std::floor(vf)),0,H-1);
|
||||
uchar *s = src + sy2*bpl + sx*4;
|
||||
uchar *d = dst_row + xx*4;
|
||||
d[0]=s[0]; d[1]=s[1]; d[2]=s[2]; d[3]=s[3];
|
||||
} else {
|
||||
// bilinear
|
||||
double x0 = std::floor(uf), y0 = std::floor(vf);
|
||||
// Nearest-neighbor sampling
|
||||
int x0 = std::clamp(int(std::floor(uf)), 0, W-1);
|
||||
int y0 = std::clamp(int(std::floor(vf)), 0, H-1);
|
||||
uchar* s = src + y0*bpl + x0*4;
|
||||
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
|
||||
}
|
||||
else {
|
||||
// Bilinear sampling
|
||||
int x0 = std::clamp(int(std::floor(uf)), 0, W-1);
|
||||
int y0 = std::clamp(int(std::floor(vf)), 0, H-1);
|
||||
int x1 = std::clamp(x0 + 1, 0, W-1);
|
||||
int y1 = std::clamp(y0 + 1, 0, H-1);
|
||||
double dxr = uf - x0, dyr = vf - y0;
|
||||
int x1 = std::clamp(int(x0)+1, 0, W-1);
|
||||
int y1 = std::clamp(int(y0)+1, 0, H-1);
|
||||
int x0i = std::clamp(int(x0),0,W-1), y0i = std::clamp(int(y0),0,H-1);
|
||||
uchar *p00 = src + y0i*bpl + x0i*4;
|
||||
uchar *p10 = src + y0i*bpl + x1*4;
|
||||
uchar *p01 = src + y1*bpl + x0i*4;
|
||||
uchar *p11 = src + y1*bpl + x1*4;
|
||||
uchar *d = dst_row + xx*4;
|
||||
for(int c=0;c<4;c++) {
|
||||
double v0 = p00[c] * (1-dxr) + p10[c] * dxr;
|
||||
double v1 = p01[c] * (1-dxr) + p11[c] * dxr;
|
||||
double v2 = v0 * (1-dyr) + v1 * dyr;
|
||||
d[c] = uchar(v2 + 0.5);
|
||||
uchar* p00 = src + y0*bpl + x0*4;
|
||||
uchar* p10 = src + y0*bpl + x1*4;
|
||||
uchar* p01 = src + y1*bpl + x0*4;
|
||||
uchar* p11 = src + y1*bpl + x1*4;
|
||||
for (int c = 0; c < 4; c++) {
|
||||
double v0 = p00[c]*(1-dxr) + p10[c]*dxr;
|
||||
double v1 = p01[c]*(1-dxr) + p11[c]*dxr;
|
||||
d[c] = uchar(v0*(1-dyr) + v1*dyr + 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +180,7 @@ Json::Value SphericalProjection::JsonValue() const
|
||||
root["roll"] = roll.JsonValue();
|
||||
root["fov"] = fov.JsonValue();
|
||||
root["projection_mode"] = projection_mode;
|
||||
root["invert"] = invert;
|
||||
root["interpolation"] = interpolation;
|
||||
return root;
|
||||
}
|
||||
@@ -172,7 +190,8 @@ void SphericalProjection::SetJson(const std::string value)
|
||||
try {
|
||||
Json::Value root = openshot::stringToJson(value);
|
||||
SetJsonValue(root);
|
||||
} catch (...) {
|
||||
}
|
||||
catch (...) {
|
||||
throw InvalidJSON("Invalid JSON for SphericalProjection");
|
||||
}
|
||||
}
|
||||
@@ -181,10 +200,11 @@ void SphericalProjection::SetJsonValue(const Json::Value root)
|
||||
{
|
||||
EffectBase::SetJsonValue(root);
|
||||
if (!root["yaw"].isNull()) yaw.SetJsonValue(root["yaw"]);
|
||||
if (!root["pitch"].isNull()) pitch.SetJsonValue(root["pitch"]);
|
||||
if (!root["roll"].isNull()) roll.SetJsonValue(root["roll"]);
|
||||
if (!root["pitch"].isNull()) pitch.SetJsonValue(root["pitch"]);
|
||||
if (!root["roll"].isNull()) roll.SetJsonValue(root["roll"]);
|
||||
if (!root["fov"].isNull()) fov.SetJsonValue(root["fov"]);
|
||||
if (!root["projection_mode"].isNull()) projection_mode = root["projection_mode"].asInt();
|
||||
if (!root["invert"].isNull()) invert = root["invert"].asInt();
|
||||
if (!root["interpolation"].isNull()) interpolation = root["interpolation"].asInt();
|
||||
}
|
||||
|
||||
@@ -194,34 +214,49 @@ std::string SphericalProjection::PropertiesJSON(int64_t requested_frame) const
|
||||
|
||||
root["yaw"] = add_property_json("Yaw",
|
||||
yaw.GetValue(requested_frame),
|
||||
"float","degrees",
|
||||
&yaw,-180,180,false,requested_frame);
|
||||
"float", "degrees",
|
||||
&yaw, -180, 180,
|
||||
false, requested_frame);
|
||||
root["pitch"] = add_property_json("Pitch",
|
||||
pitch.GetValue(requested_frame),
|
||||
"float","degrees",
|
||||
&pitch,-90,90,false,requested_frame);
|
||||
"float", "degrees",
|
||||
&pitch, -90, 90,
|
||||
false, requested_frame);
|
||||
root["roll"] = add_property_json("Roll",
|
||||
roll.GetValue(requested_frame),
|
||||
"float","degrees",
|
||||
&roll,-180,180,false,requested_frame);
|
||||
"float", "degrees",
|
||||
&roll, -180, 180,
|
||||
false, requested_frame);
|
||||
root["fov"] = add_property_json("FOV",
|
||||
fov.GetValue(requested_frame),
|
||||
"float","degrees",
|
||||
&fov,1,179,false,requested_frame);
|
||||
"float", "degrees",
|
||||
&fov, 1, 179,
|
||||
false, requested_frame);
|
||||
|
||||
root["projection_mode"] = add_property_json("Projection Mode",
|
||||
projection_mode,
|
||||
"int","",
|
||||
NULL,0,1,false,requested_frame);
|
||||
"int", "",
|
||||
nullptr, 0, 2,
|
||||
false, requested_frame);
|
||||
root["projection_mode"]["choices"].append(add_property_choice_json("Sphere", 0, projection_mode));
|
||||
root["projection_mode"]["choices"].append(add_property_choice_json("Hemisphere", 1, projection_mode));
|
||||
root["projection_mode"]["choices"].append(add_property_choice_json("Fisheye", 2, projection_mode));
|
||||
|
||||
root["invert"] = add_property_json("Invert View",
|
||||
invert,
|
||||
"int", "",
|
||||
nullptr, 0, 1,
|
||||
false, requested_frame);
|
||||
root["invert"]["choices"].append(add_property_choice_json("Normal", 0, invert));
|
||||
root["invert"]["choices"].append(add_property_choice_json("Invert", 1, invert));
|
||||
|
||||
root["interpolation"] = add_property_json("Interpolation",
|
||||
interpolation,
|
||||
"int","",
|
||||
NULL,0,1,false,requested_frame);
|
||||
root["interpolation"]["choices"].append(add_property_choice_json("Nearest", 0, interpolation));
|
||||
root["interpolation"]["choices"].append(add_property_choice_json("Bilinear", 1, interpolation));
|
||||
"int", "",
|
||||
nullptr, 0, 1,
|
||||
false, requested_frame);
|
||||
root["interpolation"]["choices"].append(add_property_choice_json("Nearest", 0, interpolation));
|
||||
root["interpolation"]["choices"].append(add_property_choice_json("Bilinear", 1, interpolation));
|
||||
|
||||
return root.toStyledString();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @file
|
||||
* @file
|
||||
* @brief Header file for SphericalProjection effect class
|
||||
* @author Jonathan Thomas <jonathan@openshot.org>
|
||||
*
|
||||
@@ -24,42 +24,50 @@
|
||||
namespace openshot
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Projects a 360° frame through a pinhole camera.
|
||||
* You can choose full sphere or hemisphere, and nearest-neighbor or bilinear sampling.
|
||||
*/
|
||||
class SphericalProjection : public EffectBase
|
||||
{
|
||||
private:
|
||||
void init_effect_details();
|
||||
/**
|
||||
* @brief Projects 360° or fisheye video through a virtual camera.
|
||||
* Supports yaw, pitch, roll, FOV, sphere/hemisphere/fisheye modes,
|
||||
* optional inversion, and nearest/bilinear sampling.
|
||||
*/
|
||||
class SphericalProjection : public EffectBase
|
||||
{
|
||||
private:
|
||||
void init_effect_details();
|
||||
|
||||
public:
|
||||
Keyframe yaw; ///< Yaw around up-axis (degrees)
|
||||
Keyframe pitch; ///< Pitch around right-axis (degrees)
|
||||
Keyframe roll; ///< Roll around forward-axis (degrees)
|
||||
Keyframe fov; ///< Field-of-view (horizontal degrees)
|
||||
public:
|
||||
Keyframe yaw; ///< Yaw around up-axis (degrees)
|
||||
Keyframe pitch; ///< Pitch around right-axis (degrees)
|
||||
Keyframe roll; ///< Roll around forward-axis (degrees)
|
||||
Keyframe fov; ///< Field-of-view (horizontal, degrees)
|
||||
|
||||
int projection_mode; ///< 0 = full sphere, 1 = hemisphere
|
||||
int interpolation; ///< 0 = nearest, 1 = bilinear
|
||||
int projection_mode; ///< 0=Sphere, 1=Hemisphere, 2=Fisheye
|
||||
int invert; ///< 0=Normal, 1=Invert (back lens / +180°)
|
||||
int interpolation; ///< 0=Nearest, 1=Bilinear
|
||||
|
||||
SphericalProjection();
|
||||
SphericalProjection(Keyframe new_yaw,
|
||||
Keyframe new_pitch,
|
||||
Keyframe new_roll,
|
||||
Keyframe new_fov);
|
||||
/// Blank ctor (for JSON deserialization)
|
||||
SphericalProjection();
|
||||
|
||||
std::shared_ptr<Frame> GetFrame(int64_t frame_number) override
|
||||
{ return GetFrame(std::make_shared<Frame>(), frame_number); }
|
||||
/// Ctor with custom curves
|
||||
SphericalProjection(Keyframe new_yaw,
|
||||
Keyframe new_pitch,
|
||||
Keyframe new_roll,
|
||||
Keyframe new_fov);
|
||||
|
||||
std::shared_ptr<Frame> GetFrame(std::shared_ptr<Frame> frame,
|
||||
int64_t frame_number) override;
|
||||
/// ClipBase override: create a fresh Frame then call the main GetFrame
|
||||
std::shared_ptr<Frame> GetFrame(int64_t frame_number) override
|
||||
{ return GetFrame(std::make_shared<Frame>(), frame_number); }
|
||||
|
||||
std::string Json() const override;
|
||||
void SetJson(const std::string value) override;
|
||||
Json::Value JsonValue() const override;
|
||||
void SetJsonValue(const Json::Value root) override;
|
||||
std::string PropertiesJSON(int64_t requested_frame) const override;
|
||||
};
|
||||
/// EffectBase override: reproject the QImage
|
||||
std::shared_ptr<Frame> GetFrame(std::shared_ptr<Frame> frame,
|
||||
int64_t frame_number) override;
|
||||
|
||||
// JSON serialization
|
||||
std::string Json() const override;
|
||||
void SetJson(std::string value) override;
|
||||
Json::Value JsonValue() const override;
|
||||
void SetJsonValue(Json::Value root) override;
|
||||
std::string PropertiesJSON(int64_t requested_frame) const override;
|
||||
};
|
||||
|
||||
} // namespace openshot
|
||||
|
||||
|
||||
Reference in New Issue
Block a user