OpenShot Library | libopenshot  0.4.0
SphericalProjection.cpp
Go to the documentation of this file.
1 
9 // Copyright (c) 2008-2025 OpenShot Studios, LLC
10 //
11 // SPDX-License-Identifier: LGPL-3.0-or-later
12 
13 #include "SphericalProjection.h"
14 #include "Exceptions.h"
15 
16 #include <cmath>
17 #include <algorithm>
18 #include <omp.h>
19 
20 using namespace openshot;
21 
23  : yaw(0.0)
24  , pitch(0.0)
25  , roll(0.0)
26  , fov(90.0)
27  , projection_mode(0)
28  , invert(0)
29  , interpolation(0)
30 {
31  init_effect_details();
32 }
33 
35  Keyframe new_pitch,
36  Keyframe new_roll,
37  Keyframe new_fov)
38  : yaw(new_yaw), pitch(new_pitch), roll(new_roll)
39  , fov(new_fov), projection_mode(0), invert(0), interpolation(0)
40 {
41  init_effect_details();
42 }
43 
44 void SphericalProjection::init_effect_details()
45 {
47  info.class_name = "SphericalProjection";
48  info.name = "Spherical Projection";
49  info.description = "Flatten and reproject 360° video with yaw, pitch, roll, and fov (sphere, hemisphere, fisheye modes)";
50  info.has_audio = false;
51  info.has_video = true;
52 }
53 
54 std::shared_ptr<openshot::Frame> SphericalProjection::GetFrame(
55  std::shared_ptr<openshot::Frame> frame,
56  int64_t frame_number)
57 {
58  auto img = frame->GetImage();
59  if (img->format() != QImage::Format_ARGB32)
60  *img = img->convertToFormat(QImage::Format_ARGB32);
61 
62  int W = img->width(), H = img->height();
63  int bpl = img->bytesPerLine();
64  uchar* src = img->bits();
65 
66  QImage output(W, H, QImage::Format_ARGB32);
67  output.fill(Qt::black);
68  uchar* dst = output.bits();
69  int dst_bpl = output.bytesPerLine();
70 
71  // Evaluate keyframes (note roll is inverted + offset 180°)
72  double yaw_r = yaw.GetValue(frame_number) * M_PI/180.0;
73  double pitch_r = pitch.GetValue(frame_number) * M_PI/180.0;
74  double roll_r = -roll.GetValue(frame_number) * M_PI/180.0 + M_PI;
75  double fov_r = fov.GetValue(frame_number) * M_PI/180.0;
76 
77  // Build composite rotation matrix R = Ry * Rx * Rz
78  double sy = sin(yaw_r), cy = cos(yaw_r);
79  double sp = sin(pitch_r), cp = cos(pitch_r);
80  double sr = sin(roll_r), cr = cos(roll_r);
81 
82  double r00 = cy*cr + sy*sp*sr, r01 = -cy*sr + sy*sp*cr, r02 = sy*cp;
83  double r10 = cp*sr, r11 = cp*cr, r12 = -sp;
84  double r20 = -sy*cr + cy*sp*sr, r21 = sy*sr + cy*sp*cr, r22 = cy*cp;
85 
86  // Precompute perspective scalars
87  double hx = tan(fov_r*0.5);
88  double vy = hx * double(H)/W;
89 
90 #pragma omp parallel for schedule(static)
91  for (int yy = 0; yy < H; yy++) {
92  uchar* dst_row = dst + yy * dst_bpl;
93  double ndc_y = (2.0*(yy + 0.5)/H - 1.0) * vy;
94 
95  for (int xx = 0; xx < W; xx++) {
96  // Generate ray in camera space
97  double ndc_x = (2.0*(xx + 0.5)/W - 1.0) * hx;
98  double vx = ndc_x, vy2 = -ndc_y, vz = -1.0;
99  double inv = 1.0/sqrt(vx*vx + vy2*vy2 + vz*vz);
100  vx *= inv; vy2 *= inv; vz *= inv;
101 
102  // Rotate ray into world coordinates
103  double dx = r00*vx + r01*vy2 + r02*vz;
104  double dy = r10*vx + r11*vy2 + r12*vz;
105  double dz = r20*vx + r21*vy2 + r22*vz;
106 
107  // For sphere/hemisphere, optionally invert view by 180°
108  if (projection_mode < 2 && invert) {
109  dx = -dx;
110  dz = -dz;
111  }
112 
113  double uf, vf;
114 
115  if (projection_mode == 2) {
116  // Fisheye mode: invert circular fisheye
117  double ax = 0.0, ay = 0.0, az = invert ? -1.0 : 1.0;
118  double cos_t = dx*ax + dy*ay + dz*az;
119  double theta = acos(cos_t);
120  double rpx = (theta / fov_r) * (W/2.0);
121  double phi = atan2(dy, dx);
122  uf = W*0.5 + rpx*cos(phi);
123  vf = H*0.5 + rpx*sin(phi);
124  }
125  else {
126  // Sphere or hemisphere: equirectangular sampling
127  double lon = atan2(dx, dz);
128  double lat = asin(dy);
129  if (projection_mode == 1) // hemisphere
130  lon = std::clamp(lon, -M_PI/2.0, M_PI/2.0);
131  uf = ((lon + (projection_mode? M_PI/2.0 : M_PI))
132  / (projection_mode? M_PI : 2.0*M_PI)) * W;
133  vf = (lat + M_PI/2.0)/M_PI * H;
134  }
135 
136  uchar* d = dst_row + xx*4;
137 
138  if (interpolation == 0) {
139  // Nearest-neighbor sampling
140  int x0 = std::clamp(int(std::floor(uf)), 0, W-1);
141  int y0 = std::clamp(int(std::floor(vf)), 0, H-1);
142  uchar* s = src + y0*bpl + x0*4;
143  d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
144  }
145  else {
146  // Bilinear sampling
147  int x0 = std::clamp(int(std::floor(uf)), 0, W-1);
148  int y0 = std::clamp(int(std::floor(vf)), 0, H-1);
149  int x1 = std::clamp(x0 + 1, 0, W-1);
150  int y1 = std::clamp(y0 + 1, 0, H-1);
151  double dxr = uf - x0, dyr = vf - y0;
152  uchar* p00 = src + y0*bpl + x0*4;
153  uchar* p10 = src + y0*bpl + x1*4;
154  uchar* p01 = src + y1*bpl + x0*4;
155  uchar* p11 = src + y1*bpl + x1*4;
156  for (int c = 0; c < 4; c++) {
157  double v0 = p00[c]*(1-dxr) + p10[c]*dxr;
158  double v1 = p01[c]*(1-dxr) + p11[c]*dxr;
159  d[c] = uchar(v0*(1-dyr) + v1*dyr + 0.5);
160  }
161  }
162  }
163  }
164 
165  *img = output;
166  return frame;
167 }
168 
169 std::string SphericalProjection::Json() const
170 {
171  return JsonValue().toStyledString();
172 }
173 
175 {
176  Json::Value root = EffectBase::JsonValue();
177  root["type"] = info.class_name;
178  root["yaw"] = yaw.JsonValue();
179  root["pitch"] = pitch.JsonValue();
180  root["roll"] = roll.JsonValue();
181  root["fov"] = fov.JsonValue();
182  root["projection_mode"] = projection_mode;
183  root["invert"] = invert;
184  root["interpolation"] = interpolation;
185  return root;
186 }
187 
188 void SphericalProjection::SetJson(const std::string value)
189 {
190  try {
191  Json::Value root = openshot::stringToJson(value);
192  SetJsonValue(root);
193  }
194  catch (...) {
195  throw InvalidJSON("Invalid JSON for SphericalProjection");
196  }
197 }
198 
199 void SphericalProjection::SetJsonValue(const Json::Value root)
200 {
202  if (!root["yaw"].isNull()) yaw.SetJsonValue(root["yaw"]);
203  if (!root["pitch"].isNull()) pitch.SetJsonValue(root["pitch"]);
204  if (!root["roll"].isNull()) roll.SetJsonValue(root["roll"]);
205  if (!root["fov"].isNull()) fov.SetJsonValue(root["fov"]);
206  if (!root["projection_mode"].isNull()) projection_mode = root["projection_mode"].asInt();
207  if (!root["invert"].isNull()) invert = root["invert"].asInt();
208  if (!root["interpolation"].isNull()) interpolation = root["interpolation"].asInt();
209 }
210 
211 std::string SphericalProjection::PropertiesJSON(int64_t requested_frame) const
212 {
213  Json::Value root = BasePropertiesJSON(requested_frame);
214 
215  root["yaw"] = add_property_json("Yaw",
216  yaw.GetValue(requested_frame),
217  "float", "degrees",
218  &yaw, -180, 180,
219  false, requested_frame);
220  root["pitch"] = add_property_json("Pitch",
221  pitch.GetValue(requested_frame),
222  "float", "degrees",
223  &pitch, -90, 90,
224  false, requested_frame);
225  root["roll"] = add_property_json("Roll",
226  roll.GetValue(requested_frame),
227  "float", "degrees",
228  &roll, -180, 180,
229  false, requested_frame);
230  root["fov"] = add_property_json("FOV",
231  fov.GetValue(requested_frame),
232  "float", "degrees",
233  &fov, 1, 179,
234  false, requested_frame);
235 
236  root["projection_mode"] = add_property_json("Projection Mode",
238  "int", "",
239  nullptr, 0, 2,
240  false, requested_frame);
241  root["projection_mode"]["choices"].append(add_property_choice_json("Sphere", 0, projection_mode));
242  root["projection_mode"]["choices"].append(add_property_choice_json("Hemisphere", 1, projection_mode));
243  root["projection_mode"]["choices"].append(add_property_choice_json("Fisheye", 2, projection_mode));
244 
245  root["invert"] = add_property_json("Invert View",
246  invert,
247  "int", "",
248  nullptr, 0, 1,
249  false, requested_frame);
250  root["invert"]["choices"].append(add_property_choice_json("Normal", 0, invert));
251  root["invert"]["choices"].append(add_property_choice_json("Invert", 1, invert));
252 
253  root["interpolation"] = add_property_json("Interpolation",
255  "int", "",
256  nullptr, 0, 1,
257  false, requested_frame);
258  root["interpolation"]["choices"].append(add_property_choice_json("Nearest", 0, interpolation));
259  root["interpolation"]["choices"].append(add_property_choice_json("Bilinear", 1, interpolation));
260 
261  return root.toStyledString();
262 }
openshot::SphericalProjection::invert
int invert
0=Normal, 1=Invert (back lens / +180°)
Definition: SphericalProjection.h:44
openshot::ClipBase::add_property_json
Json::Value add_property_json(std::string name, float value, std::string type, std::string memo, const Keyframe *keyframe, float min_value, float max_value, bool readonly, int64_t requested_frame) const
Generate JSON for a property.
Definition: ClipBase.cpp:96
openshot::stringToJson
const Json::Value stringToJson(const std::string value)
Definition: Json.cpp:16
openshot::EffectBase::info
EffectInfoStruct info
Information about the current effect.
Definition: EffectBase.h:69
openshot::SphericalProjection::interpolation
int interpolation
0=Nearest, 1=Bilinear
Definition: SphericalProjection.h:45
openshot::SphericalProjection::pitch
Keyframe pitch
Pitch around right-axis (degrees)
Definition: SphericalProjection.h:39
openshot
This namespace is the default namespace for all code in the openshot library.
Definition: Compressor.h:28
openshot::SphericalProjection::SetJsonValue
void SetJsonValue(Json::Value root) override
Load Json::Value into this object.
Definition: SphericalProjection.cpp:199
openshot::ClipBase::add_property_choice_json
Json::Value add_property_choice_json(std::string name, int value, int selected_value) const
Generate JSON choice for a property (dropdown properties)
Definition: ClipBase.cpp:132
openshot::EffectBase::JsonValue
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
Definition: EffectBase.cpp:79
openshot::SphericalProjection::yaw
Keyframe yaw
Yaw around up-axis (degrees)
Definition: SphericalProjection.h:38
openshot::Keyframe::SetJsonValue
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Definition: KeyFrame.cpp:372
openshot::Keyframe::JsonValue
Json::Value JsonValue() const
Generate Json::Value for this object.
Definition: KeyFrame.cpp:339
openshot::EffectBase::BasePropertiesJSON
Json::Value BasePropertiesJSON(int64_t requested_frame) const
Generate JSON object of base properties (recommended to be used by all effects)
Definition: EffectBase.cpp:179
openshot::SphericalProjection::PropertiesJSON
std::string PropertiesJSON(int64_t requested_frame) const override
Definition: SphericalProjection.cpp:211
openshot::Keyframe
A Keyframe is a collection of Point instances, which is used to vary a number or property over time.
Definition: KeyFrame.h:53
openshot::InvalidJSON
Exception for invalid JSON.
Definition: Exceptions.h:217
openshot::SphericalProjection::GetFrame
std::shared_ptr< Frame > GetFrame(int64_t frame_number) override
ClipBase override: create a fresh Frame then call the main GetFrame.
Definition: SphericalProjection.h:57
openshot::EffectBase::InitEffectInfo
void InitEffectInfo()
Definition: EffectBase.cpp:24
openshot::EffectInfoStruct::has_audio
bool has_audio
Determines if this effect manipulates the audio of a frame.
Definition: EffectBase.h:41
openshot::SphericalProjection::projection_mode
int projection_mode
0=Sphere, 1=Hemisphere, 2=Fisheye
Definition: SphericalProjection.h:43
SphericalProjection.h
Header file for SphericalProjection effect class.
openshot::SphericalProjection::SphericalProjection
SphericalProjection()
Blank ctor (for JSON deserialization)
Definition: SphericalProjection.cpp:22
openshot::SphericalProjection::Json
std::string Json() const override
Generate JSON string of this object.
Definition: SphericalProjection.cpp:169
openshot::EffectInfoStruct::class_name
std::string class_name
The class name of the effect.
Definition: EffectBase.h:36
openshot::EffectInfoStruct::description
std::string description
The description of this effect and what it does.
Definition: EffectBase.h:38
openshot::EffectInfoStruct::has_video
bool has_video
Determines if this effect manipulates the image of a frame.
Definition: EffectBase.h:40
openshot::EffectInfoStruct::name
std::string name
The name of the effect.
Definition: EffectBase.h:37
openshot::SphericalProjection::SetJson
void SetJson(std::string value) override
Load JSON string into this object.
Definition: SphericalProjection.cpp:188
openshot::SphericalProjection::JsonValue
Json::Value JsonValue() const override
Generate Json::Value for this object.
Definition: SphericalProjection.cpp:174
openshot::SphericalProjection::fov
Keyframe fov
Field-of-view (horizontal, degrees)
Definition: SphericalProjection.h:41
Exceptions.h
Header file for all Exception classes.
openshot::SphericalProjection::roll
Keyframe roll
Roll around forward-axis (degrees)
Definition: SphericalProjection.h:40
openshot::EffectBase::SetJsonValue
virtual void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Definition: EffectBase.cpp:115
openshot::Keyframe::GetValue
double GetValue(int64_t index) const
Get the value at a specific index.
Definition: KeyFrame.cpp:258