You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-03-02 08:53:52 -08:00
Adding new spherical projection effect, to flatten out 360/180 videos into the viewport.
This commit is contained in:
@@ -120,6 +120,7 @@ set(EFFECTS_SOURCES
|
||||
effects/Pixelate.cpp
|
||||
effects/Saturation.cpp
|
||||
effects/Shift.cpp
|
||||
effects/SphericalProjection.cpp
|
||||
effects/Wave.cpp
|
||||
audio_effects/STFT.cpp
|
||||
audio_effects/Noise.cpp
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "EffectInfo.h"
|
||||
#include "Effects.h"
|
||||
#include "effects/SphericalProjection.h"
|
||||
|
||||
using namespace openshot;
|
||||
|
||||
@@ -67,6 +68,9 @@ EffectBase* EffectInfo::CreateEffect(std::string effect_type) {
|
||||
else if (effect_type == "Shift")
|
||||
return new Shift();
|
||||
|
||||
else if (effect_type == "SphericalProjection")
|
||||
return new SphericalProjection();
|
||||
|
||||
else if (effect_type == "Wave")
|
||||
return new Wave();
|
||||
|
||||
@@ -135,6 +139,7 @@ Json::Value EffectInfo::JsonValue() {
|
||||
root.append(Pixelate().JsonInfo());
|
||||
root.append(Saturation().JsonInfo());
|
||||
root.append(Shift().JsonInfo());
|
||||
root.append(SphericalProjection().JsonInfo());
|
||||
root.append(Wave().JsonInfo());
|
||||
/* Audio */
|
||||
root.append(Noise().JsonInfo());
|
||||
|
||||
227
src/effects/SphericalProjection.cpp
Normal file
227
src/effects/SphericalProjection.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief Source file for SphericalProjection effect class
|
||||
* @author Jonathan Thomas <jonathan@openshot.org>
|
||||
*
|
||||
* @ref License
|
||||
*/
|
||||
|
||||
// Copyright (c) 2008-2025 OpenShot Studios, LLC
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
#include "SphericalProjection.h"
|
||||
#include "Exceptions.h"
|
||||
|
||||
#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)
|
||||
{
|
||||
init_effect_details();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
init_effect_details();
|
||||
}
|
||||
|
||||
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.has_audio = false;
|
||||
info.has_video = true;
|
||||
|
||||
// Keyframes auto-registered
|
||||
}
|
||||
|
||||
std::shared_ptr<openshot::Frame> SphericalProjection::GetFrame(
|
||||
std::shared_ptr<openshot::Frame> frame,
|
||||
int64_t frame_number)
|
||||
{
|
||||
auto img = frame->GetImage();
|
||||
if (img->format() != QImage::Format_ARGB32)
|
||||
*img = img->convertToFormat(QImage::Format_ARGB32);
|
||||
|
||||
int W = img->width(), H = img->height();
|
||||
int bpl = img->bytesPerLine();
|
||||
uchar *src = img->bits();
|
||||
|
||||
QImage output(W, H, QImage::Format_ARGB32);
|
||||
output.fill(Qt::black);
|
||||
uchar *dst = output.bits();
|
||||
int dst_bpl = output.bytesPerLine();
|
||||
|
||||
// read keyframes & convert
|
||||
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 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;
|
||||
|
||||
// fov scalars
|
||||
double hx = tan(fov_r/2.0);
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*img = output;
|
||||
return frame;
|
||||
}
|
||||
|
||||
std::string SphericalProjection::Json() const
|
||||
{
|
||||
return JsonValue().toStyledString();
|
||||
}
|
||||
|
||||
Json::Value SphericalProjection::JsonValue() const
|
||||
{
|
||||
Json::Value root = EffectBase::JsonValue();
|
||||
root["type"] = info.class_name;
|
||||
root["yaw"] = yaw.JsonValue();
|
||||
root["pitch"] = pitch.JsonValue();
|
||||
root["roll"] = roll.JsonValue();
|
||||
root["fov"] = fov.JsonValue();
|
||||
root["projection_mode"] = projection_mode;
|
||||
root["interpolation"] = interpolation;
|
||||
return root;
|
||||
}
|
||||
|
||||
void SphericalProjection::SetJson(const std::string value)
|
||||
{
|
||||
try {
|
||||
Json::Value root = openshot::stringToJson(value);
|
||||
SetJsonValue(root);
|
||||
} catch (...) {
|
||||
throw InvalidJSON("Invalid JSON for SphericalProjection");
|
||||
}
|
||||
}
|
||||
|
||||
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["fov"].isNull()) fov.SetJsonValue(root["fov"]);
|
||||
if (!root["projection_mode"].isNull()) projection_mode = root["projection_mode"].asInt();
|
||||
if (!root["interpolation"].isNull()) interpolation = root["interpolation"].asInt();
|
||||
}
|
||||
|
||||
std::string SphericalProjection::PropertiesJSON(int64_t requested_frame) const
|
||||
{
|
||||
Json::Value root = BasePropertiesJSON(requested_frame);
|
||||
|
||||
root["yaw"] = add_property_json("Yaw",
|
||||
yaw.GetValue(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);
|
||||
root["roll"] = add_property_json("Roll",
|
||||
roll.GetValue(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);
|
||||
|
||||
root["projection_mode"] = add_property_json("Projection Mode",
|
||||
projection_mode,
|
||||
"int","",
|
||||
NULL,0,1,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["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));
|
||||
|
||||
return root.toStyledString();
|
||||
}
|
||||
66
src/effects/SphericalProjection.h
Normal file
66
src/effects/SphericalProjection.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief Header file for SphericalProjection effect class
|
||||
* @author Jonathan Thomas <jonathan@openshot.org>
|
||||
*
|
||||
* @ref License
|
||||
*/
|
||||
|
||||
// Copyright (c) 2008-2025 OpenShot Studios, LLC
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
#ifndef OPENSHOT_SPHERICAL_PROJECTION_EFFECT_H
|
||||
#define OPENSHOT_SPHERICAL_PROJECTION_EFFECT_H
|
||||
|
||||
#include "../EffectBase.h"
|
||||
#include "../Frame.h"
|
||||
#include "../Json.h"
|
||||
#include "../KeyFrame.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
|
||||
SphericalProjection();
|
||||
SphericalProjection(Keyframe new_yaw,
|
||||
Keyframe new_pitch,
|
||||
Keyframe new_roll,
|
||||
Keyframe new_fov);
|
||||
|
||||
std::shared_ptr<Frame> GetFrame(int64_t frame_number) override
|
||||
{ return GetFrame(std::make_shared<Frame>(), frame_number); }
|
||||
|
||||
std::shared_ptr<Frame> GetFrame(std::shared_ptr<Frame> frame,
|
||||
int64_t frame_number) override;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace openshot
|
||||
|
||||
#endif // OPENSHOT_SPHERICAL_PROJECTION_EFFECT_H
|
||||
Reference in New Issue
Block a user