Adding fisheye mode to spherical projection effect, and cleaning up code.

This commit is contained in:
Jonathan Thomas
2025-05-21 16:15:01 -05:00
parent 9d33c45d84
commit f93d7861f2
2 changed files with 159 additions and 116 deletions
+120 -85
View File
@@ -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();
}
+39 -31
View File
@@ -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