{"name": "SkyStitch - Drone Orthomosaic", "package_name": "skystitch", "version": "2.3.2", "experimental": false, "qgis_min": "3.16.0", "qgis_max": "3.99.0", "downloads": 208, "uploaded_by": "kholifatkhur", "upload_datetime": "2026-08-03T03:23:58.279874", "changelog": "2.3.0\r\n- NEW: default 3D engine \"SkyStitch - 3D Photogrammetry (local node)\". It\r\ntalks directly to a NodeODM node running on the user's own machine (e.g. the\r\nfree WebODM native installer's node, or a standalone NodeODM): uploads the\r\nphotos (resumable), processes locally, downloads all.zip, extracts it, and\r\nloads ortho/DSM/DTM/point cloud into QGIS. Each user installs the engine\r\nonce; the plugin then runs everything from QGIS -- no Docker to launch, no\r\nWebODM login, no cloud. Has a Test Connection button and optional node token.\r\nNew files: nodeodm_dialog.py, nodeodm_worker.py, pipeline/nodeodm_client.py.\r\n- CHANGED: menu now shows just the 2D engine and the local-node 3D engine. The\r\nWebODM (login) and local-Docker engines still ship but are hidden by default\r\n(SHOW_WEBODM_REMOTE_ENGINE / SHOW_LOCAL_DOCKER_ENGINE in skystitch_plugin.py).\r\n\r\n2.2.2\r\n- CHANGED: the local Docker OpenDroneMap tool is now hidden from the menu to\r\navoid Docker confusion. The menu shows just two tools: the 2D stitch and\r\n\"3D Photogrammetry (WebODM, no Docker)\". Nothing was deleted -- the Docker\r\nengine's files still ship and can be brought back by setting\r\nSHOW_LOCAL_DOCKER_ENGINE = True in skystitch_plugin.py.\r\n\r\n2.2.1\r\n- CHANGED: WebODM is now the recommended path for 3D. Menu order is\r\n2D -> \"3D Photogrammetry (WebODM, no Docker)\" -> \"3D via local Docker\r\n(advanced)\", and the tools were renamed to make clear that WebODM needs no\r\nDocker (its server can be your own PC at http://localhost:8000 via the free\r\nnative installer), while the local OpenDroneMap engine is the Docker-based\r\nadvanced option. No functionality removed.\r\n\r\n2.2.0\r\n- NEW: third engine \"SkyStitch - WebODM (remote)\". It connects to a WebODM\r\nserver (the free native Windows installer running locally, or a remote\r\nserver) over the WebODM REST API: it uploads the photos, processes on the\r\nserver, and downloads the orthophoto, DSM, DTM, point cloud and 3D mesh\r\nback into QGIS -- with NO Docker needed on the user's machine. Has a\r\n\"Test Connection\" button, resumable per-image upload, live progress/log,\r\nand cancel. New files: webodm_dialog.py, webodm_worker.py,\r\npipeline/webodm_client.py. Requires the Python package \"requests\" (a small,\r\ncommon dependency; the dialog shows a pip hint if missing).\r\nTip: WebODM's Windows installer (webodm.org/download) runs natively without\r\nDocker, so pairing it with this engine avoids Docker/WSL entirely.\r\n\r\n2.1.0\r\n- NEW: \"Test Docker\" and \"Install Docker\" buttons in the OpenDroneMap (3D)\r\ndialog (Advanced section). Test checks that the docker CLI and engine\r\nrespond; Install runs \"winget install Docker.DockerDesktop\" on Windows\r\n(a UAC prompt appears; a restart is needed afterwards) or opens the\r\nofficial download page on macOS/Linux. Helper logic lives in\r\npipeline/odm_engine.py (docker_status, docker_install_hint,\r\nstart_docker_install). Note: Docker still cannot be bundled inside the\r\nplugin zip -- it is system software needing admin rights and a reboot -- so\r\nthese buttons make setup easier but can't make it fully silent.\r\n\r\n2.0.0\r\n- NEW: optional \"OpenDroneMap (3D)\" engine, added as a second tool/dialog\r\nalongside the original 2D stitcher (the 2D engine is left unchanged). It\r\ndrives OpenDroneMap through its official Docker image to run real SfM/MVS\r\nphotogrammetry and produce a true (relief-corrected) orthophoto plus DSM,\r\nDTM/DEM, point cloud (LAZ) and textured 3D mesh (OBJ). It can optionally\r\ngenerate contour, slope and hillshade from the DSM using QGIS Processing,\r\nand accepts an ODM gcp_list.txt for survey-grade accuracy. This mode needs\r\nDocker (see README.md -> \"OpenDroneMap (3D) mode\"); the 2D engine's Python\r\ndependencies are not required for it.\r\nNew files: odm_dialog.py, odm_worker.py, pipeline/odm_engine.py,\r\npipeline/derivatives.py.\r\n\r\n1.9.5\r\n- FIX: a GCP/ICP file with more than one row pointing at the same photo\r\npreviously had each later row silently overwrite the earlier one, so\r\nonly the last-listed point for that photo was ever used. Multiple GCP\r\nrows matched to the same photo are now averaged into a single anchor\r\nposition for that photo (this plugin anchors whole photos, not\r\nindividual pixels within one, so averaging is the correct way to fold\r\nseveral points into that model). If the matched rows disagree by more\r\nthan 2m after conversion, a [WARNING] is now logged so a mistyped\r\ncoordinate, wrong photo name, or wrong CRS gets noticed instead of\r\nsilently producing an averaged-but-wrong anchor.\r\n- Cleaned up three bare `except: pass`/`except: continue` blocks\r\n(flagged by static analysis) in geo_utils.py, proj_fix.py, and\r\nskystitch_dialog.py. Behavior is unchanged (all three are genuinely\r\noptional/best-effort paths), but failures are now logged at debug\r\nlevel via the standard `logging` module instead of being silently\r\ndiscarded, so a real underlying problem is still discoverable.\r\n\r\n1.9.4\r\n- NEW: output GeoTIFF compression is now selectable (Advanced options ->\r\n\"Output compression\"): DEFLATE (previous default, lossless), ZSTD\r\n(lossless, usually smaller/faster than DEFLATE), LZW (lossless), JPEG\r\n(lossy, smallest files, adjustable quality 1-100%), or None\r\n(uncompressed). JPEG mode stores the transparency mask as an internal\r\nGDAL mask band instead of a literal 4th band, since JPEG doesn't\r\nsupport an alpha channel directly; QGIS and other GDAL-aware viewers\r\nstill render it transparently the same way. The chosen setting is\r\nremembered between sessions, same as the other advanced options.\r\n\r\n1.9.3\r\n- CHANGED: photos that don't overlap with the main feature-matched group\r\n(or, in the extreme case, when NO photo overlaps any other at all) are\r\nno longer dropped from the output. They're still placed on the canvas\r\nat their own GPS coordinate (using the same scale/rotation as the\r\nfitted mosaic, since there's no matching info to refine their own\r\norientation), and a [NOTICE] is logged listing which photos were\r\nplaced this way -- the build no longer stops or silently skips them.\r\nOverlap between a GPS-only-placed photo and any other photo (matched\r\nor GPS-only) is handled the same as any other overlap (seam/blend),\r\nnot treated as an error. Previously, a fully non-overlapping photo set\r\nraised \"No photos could be matched to each other\" and stopped the\r\nwhole build; individual non-overlapping photos within an otherwise\r\nfine set were silently left out of the mosaic.\r\n\r\n1.9.2\r\n- CRITICAL FIX: the output .tif's 4th band was written as the alpha\r\nchannel, but only got a text label (set_band_description(4, \"alpha\"))\r\n-- it was never actually tagged with GDAL's alpha color\r\ninterpretation. QGIS (and other GDAL-aware viewers) only render\r\nnodata as transparent when a band is tagged that way; without it,\r\nband 4 was treated as an ordinary band and the literal black (0,0,0)\r\npixels filling the canvas outside each photo's rotated footprint\r\nshowed up as a solid black border around the mosaic, even inside\r\nQGIS with a basemap underneath. Band 4 is now explicitly set to\r\nGDAL's alpha color interpretation (rasterio.enums.ColorInterp.alpha)\r\non save, so that area renders transparent as intended. Mosaics built\r\nwith 1.9.1 or earlier will need to be rebuilt to get the fix (existing\r\n.tif files aren't modified retroactively).\r\n\r\n1.9.1\r\n- FIX: the output .tif already carries a proper alpha band, so the area\r\noutside the mosaic's tilted footprint shows up as transparent (not\r\nblack) in GIS software like QGIS. But the separate \"_preview\" image\r\nthe pipeline also saves alongside it was a plain RGB .jpg, which\r\ndropped that alpha band entirely (and JPEG can't represent\r\ntransparency even if it hadn't) -- so that same area showed as a\r\nsolid black border whenever the preview was opened outside a GIS\r\nviewer. The preview is now saved as \"_preview.png\" with the same\r\nalpha channel as the .tif, so the border is transparent there too.\r\n\r\n1.9.0\r\n- NEW: optional GCP/ICP correction. A new \"GCP / ICP file (optional)\"\r\nfield in Advanced options accepts a CSV or Excel (.xlsx) file with\r\nsurveyed, high-accuracy coordinates for specific photos (matched by\r\nfilename). Matched photos are used as trusted anchors, refined with\r\nan iterative (ICP-style) reweighting pass that also down-weights\r\nGPS-derived positions that disagree strongly with the GCP-anchored\r\nfit, correcting the whole mosaic's position/scale/rotation beyond\r\nplain consumer-GPS accuracy. Also available on the CLI via --gcp.\r\nReading .xlsx files requires the optional 'openpyxl' package; .csv\r\nneeds no extra dependency. Leaving the field empty uses GPS only,\r\nidentical to previous behavior.\r\n- The small result-preview thumbnail previously shown in the dialog\r\nafter a build finished has been removed from the UI. The preview\r\n.jpg is still saved next to the output .tif by the pipeline; open\r\nthe layer in the QGIS canvas (or that file) to inspect the result.\r\n\r\n1.8.0\r\n- NEW: heads-up warning when photo GPS altitudes vary by more than 15m\r\nacross the flight, which can indicate hilly/non-flat terrain or a\r\nmid-flight altitude change -- this plugin does not perform DEM-based\r\nelevation correction, so the result may be misaligned in those areas.\r\nThis is a lightweight heuristic based on EXIF GPS altitude only (no\r\nDEM/elevation data is used or required).\r\n- All [WARNING] messages (unrecognized camera model, GPS baseline too\r\ntight, terrain relief, ...) now also appear in the QGIS message bar\r\nas they happen, not just in the scrolling log.\r\n\r\n1.7.0\r\n- NEW: optional exposure/brightness compensation step (on by default,\r\ncan be turned off via a new checkbox in Advanced options, or\r\n--no-exposure-compensation on the CLI). Equalizes each photo's\r\noverall brightness to the group's median before blending, so a\r\nframe taken under a passing cloud (or with slightly different\r\nauto-exposure) doesn't leave a visible brightness seam. Runs as a\r\nnew \"[STEP 5/7]\" stage; total step count increased from 6 to 7.\r\n- The dialog now shows a rough pre-run estimate of build time and\r\npeak RAM usage (based on photo count/resolution) next to the photo\r\ncount, before \"Build Mosaic\" is even clicked.\r\n- The output CRS (previously only visible buried in the log text) is\r\nnow shown as a selectable/copyable label in the dialog once a\r\nbuild finishes, alongside the final resolution.\r\n\r\n1.6.2\r\n- GeoTIFF output now gets internal overviews/pyramids (average\r\nresampling) built automatically, plus horizontal-differencing\r\n(predictor=2) and RGB-tagged compression, so large mosaics pan/zoom\r\nsmoothly in QGIS instead of being resampled on the fly every time.\r\n- The dialog now shows which of the 6 pipeline steps is currently\r\nrunning next to the progress bar, instead of only in the scrolling\r\nlog.\r\n- The dialog now displays the small preview .jpg the pipeline already\r\ngenerates, right below the progress bar, once the run finishes --\r\nno need to open the output folder or the QGIS canvas to sanity-check\r\nthe result.\r\n- The success message now reports the final ground sample distance\r\n(m/px) actually used, since it can differ from the analytic GSD\r\nestimate shown earlier in the log (e.g. after the memory-based\r\nresolution guard in mosaic_builder.py lowers it).\r\n\r\n1.6.1\r\n- CRITICAL FIX: the parallel feature detection added in 1.6 used\r\nProcessPoolExecutor, which spawns brand-new OS processes. Inside a\r\nplugin running in QGIS's own embedded Python, a new process's entry\r\npoint is the QGIS executable itself, not this plugin -- so every\r\nworker \"process\" actually relaunched a whole new QGIS window (several\r\nof which then crashed with \"Unexpected error: A process in the\r\nprocess pool was terminated abruptly...\" once the pool tried to use\r\nthem as plain workers). Switched to ThreadPoolExecutor, which never\r\nspawns a process and so cannot trigger this. Detection is still run\r\nin parallel and still benefits from multiple CPU cores, since cv2's\r\nheavy C++ calls (including SIFT's detectAndCompute) release Python's\r\nGIL while running -- same mechanism numpy already relies on for\r\nthread-based parallelism. If 1.6 caused extra QGIS windows to open on\r\nyour machine, close them and update to this version.\r\n\r\n1.6\r\n- Feature detection (SIFT) in build_match_graph now runs across multiple\r\nprocesses (concurrent.futures.ProcessPoolExecutor) instead of one\r\nphoto at a time, since detecting each photo's features is completely\r\nindependent of the others. On a multi-core machine this cuts Step 2's\r\ndetection time roughly in proportion to available CPU cores for\r\nflights with dozens-to-hundreds of photos. Cancellation remains\r\nresponsive: an in-progress batch stops promptly instead of waiting for\r\nevery worker to finish. No effect on results -- same keypoints/\r\ndescriptors are produced either way, just computed in parallel.\r\n\r\n- render_mosaic gained a new default blending mode, blend_mode=\"seam\"\r\n(previous behavior kept as blend_mode=\"feather\"). The old feather mode\r\naveraged every overlapping photo together weighted by distance to each\r\nphoto's edge; this is seamless on flat ground but any object taller\r\nthan ground level (rooftops, walls, trees) is seen in a slightly\r\ndifferent pixel position by each photo, so it got blended into a\r\nsemi-transparent double image (\"ghosting\") instead of a clean line.\r\nThe new seam mode instead finds a cut line between each pair of\r\noverlapping photos with graph-cut seam finding\r\n(cv2.detail.GraphCutSeamFinder, run only on the overlap's own bounding\r\nbox for speed, with an automatic downscale-then-upscale fallback for\r\nunusually large overlaps) and hard-assigns each pixel to one photo or\r\nthe other, with only a narrow (~12px) feathered band right at the cut\r\nline to hide pixel-level aliasing. This does not fix the underlying\r\nparallax error on tall objects (that needs true DSM-based\r\northorectification, which is out of scope for this planar-homography\r\npipeline) but replaces the ghosted double-exposure look with a\r\nsharper, cleaner cut through it. Takes noticeably longer to render\r\nthan feather mode (graph-cut has a real per-overlap cost); \"feather\"\r\nis still available for flat/open terrain (farmland, open fields) where\r\nthere's nothing tall to ghost and a fully seamless blend is\r\npreferable.\r\n\r\n1.5\r\n- The tight-GPS-baseline fallback in fit_world_similarity (triggered\r\nwhen photos are too close together to fit rotation/scale from GPS\r\nalone) previously always assumed zero rotation ('photo up = north'),\r\nwhich was only right by coincidence. It now uses the root photo's\r\ngimbal yaw (read from DJI-style XMP metadata, previously parsed by\r\nread_xmp_gimbal_yaw() but never actually used anywhere) as a real\r\norientation estimate when available, falling back to the previous\r\nzero-rotation behavior when it isn't (e.g. camera doesn't expose\r\ngimbal yaw). No effect on the normal (wide GPS baseline) case.\r\n\r\n1.4\r\n- Unrecognized camera models used to silently fall back to a guessed\r\nsensor width (DEFAULT_SENSOR_WIDTH_MM) when estimating GSD from camera\r\nparameters, with no indication to the user. Now flagged explicitly:\r\nestimate_analytic_gsd() reports whether the sensor width was actually\r\nknown, and the pipeline logs a warning (once per distinct unrecognized\r\nmake/model) when this estimate is computed, since it's silently relied\r\non as the scale fallback whenever the GPS baseline between photos is\r\ntoo tight to calibrate scale on its own.\r\n\r\n1.3\r\n- render_mosaic now warps each photo only into its own footprint's\r\nbounding box on the canvas, instead of warping every photo onto the\r\nFULL canvas every time. This was previously the biggest memory/CPU\r\ncost in the whole pipeline for large surveys -- cost scaled with\r\n(number of photos x full canvas size) even though each photo only\r\never covers a small fraction of the final mosaic.\r\n- Replaced the old fixed max_canvas_dim=18000px cap with a memory-budget\r\nbased guard (default 2 GiB for the accumulator buffers): if the\r\ncanvas at the estimated GSD would need more memory than that, GSD is\r\nautomatically coarsened (same coverage area, lower resolution) instead\r\nof risking a MemoryError/crash. A clear warning is logged when this\r\nhappens. A secondary max_canvas_dim=30000px hard cap still protects\r\nagainst degenerate/very elongated canvases.\r\n\r\n1.2\r\n- Feature matching (SIFT) now uses each photo's GPS position to only try\r\nmatching it against its nearest neighbors (default: 12), instead of\r\nevery other photo in the flight. This turns matching from O(n^2) into\r\nroughly O(n x k), which matters a lot for flights with hundreds of\r\nphotos -- most distant pairs could never overlap anyway. Falls back to\r\nmatching every pair when GPS positions aren't available or the photo\r\ncount is small, so behavior/accuracy is unchanged for small flights.", "external_deps": null, "download_url": "https://plugins.qgis.org/plugins/skystitch/version/2.3.2/download/"}