├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── jarRepositories.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── runConfigurations.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── debug │ ├── ic_launcher-playstore.png │ └── res │ │ ├── drawable │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── ic_launcher_background.xml │ └── main │ ├── AndroidManifest.xml │ ├── cpp │ ├── CMakeLists.txt │ └── timelapse.cpp │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── dan │ │ └── timelapse │ │ ├── MainActivity.kt │ │ ├── filters │ │ ├── AddFramesFilter.kt │ │ ├── AlignFramesFilter.kt │ │ ├── AverageFramesFilter.kt │ │ ├── AverageWeightedForLastFramesFilter.kt │ │ ├── AverageWeightedForLightFramesFilter.kt │ │ ├── DarkestPixelsFramesFilter.kt │ │ ├── EndlessAddFramesFilter.kt │ │ ├── EndlessAverageFramesFilter.kt │ │ ├── EndlessAverageWeightedForLastFramesFilter.kt │ │ ├── EndlessAverageWeightedForLightFramesFilter.kt │ │ ├── EndlessDarkestPixelsFramesFilter.kt │ │ ├── EndlessLightestPixelsFramesFilter.kt │ │ ├── FramesConsumer.kt │ │ ├── FramesFilter.kt │ │ ├── HDRFramesFilter.kt │ │ ├── LightestPixelsFramesFilter.kt │ │ ├── MultiFramesFilter.kt │ │ ├── SampleFramesFilter.kt │ │ ├── ScaleFramesFilter.kt │ │ ├── SumFramesFilter.kt │ │ └── TransitionFramesFilter.kt │ │ ├── framesinput │ │ ├── FramesInput.kt │ │ ├── ImagesFramesInput.kt │ │ └── VideoFramesInput.kt │ │ ├── images │ │ ├── ImageTools.kt │ │ └── ImageWriter.kt │ │ ├── screens │ │ ├── AppFragment.kt │ │ ├── BusyDialog.kt │ │ ├── MainFragment.kt │ │ ├── MaskEditFragment.kt │ │ └── SettingsFragment.kt │ │ ├── utils │ │ ├── OutputParams.kt │ │ ├── Settings.kt │ │ └── UriFile.kt │ │ ├── video │ │ ├── VideoEncoder.kt │ │ ├── VideoTools.kt │ │ └── VideoWriter.kt │ │ └── widgets │ │ └── TouchImageView.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ └── ic_launcher_foreground.xml │ ├── layout │ ├── activity_main.xml │ ├── busy_dialog.xml │ ├── main_fragment.xml │ ├── mask_edit_fragment.xml │ └── settings_fragment.xml │ ├── menu │ └── app_menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── examples ├── other │ ├── endless-lightest-pixels.gif │ └── simple.gif ├── smooth │ └── original_vs_smooth_10x.gif └── transition │ ├── crop.gif │ ├── input_1.jpg │ ├── input_2.jpg │ ├── input_3.jpg │ ├── input_4.jpg │ └── no-crop.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── opencv ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── org │ │ └── opencv │ │ └── engine │ │ └── OpenCVEngineInterface.aidl │ ├── cpp │ └── include │ │ └── opencv2 │ │ ├── calib3d.hpp │ │ ├── calib3d │ │ ├── calib3d.hpp │ │ └── calib3d_c.h │ │ ├── core.hpp │ │ ├── core │ │ ├── affine.hpp │ │ ├── async.hpp │ │ ├── base.hpp │ │ ├── bindings_utils.hpp │ │ ├── bufferpool.hpp │ │ ├── check.hpp │ │ ├── core.hpp │ │ ├── core_c.h │ │ ├── cuda.hpp │ │ ├── cuda.inl.hpp │ │ ├── cuda │ │ │ ├── block.hpp │ │ │ ├── border_interpolate.hpp │ │ │ ├── color.hpp │ │ │ ├── common.hpp │ │ │ ├── datamov_utils.hpp │ │ │ ├── detail │ │ │ │ ├── color_detail.hpp │ │ │ │ ├── reduce.hpp │ │ │ │ ├── reduce_key_val.hpp │ │ │ │ ├── transform_detail.hpp │ │ │ │ ├── type_traits_detail.hpp │ │ │ │ └── vec_distance_detail.hpp │ │ │ ├── dynamic_smem.hpp │ │ │ ├── emulation.hpp │ │ │ ├── filters.hpp │ │ │ ├── funcattrib.hpp │ │ │ ├── functional.hpp │ │ │ ├── limits.hpp │ │ │ ├── reduce.hpp │ │ │ ├── saturate_cast.hpp │ │ │ ├── scan.hpp │ │ │ ├── simd_functions.hpp │ │ │ ├── transform.hpp │ │ │ ├── type_traits.hpp │ │ │ ├── utility.hpp │ │ │ ├── vec_distance.hpp │ │ │ ├── vec_math.hpp │ │ │ ├── vec_traits.hpp │ │ │ ├── warp.hpp │ │ │ ├── warp_reduce.hpp │ │ │ └── warp_shuffle.hpp │ │ ├── cuda_stream_accessor.hpp │ │ ├── cuda_types.hpp │ │ ├── cv_cpu_dispatch.h │ │ ├── cv_cpu_helper.h │ │ ├── cvdef.h │ │ ├── cvstd.hpp │ │ ├── cvstd.inl.hpp │ │ ├── cvstd_wrapper.hpp │ │ ├── detail │ │ │ ├── async_promise.hpp │ │ │ ├── dispatch_helper.impl.hpp │ │ │ └── exception_ptr.hpp │ │ ├── directx.hpp │ │ ├── dualquaternion.hpp │ │ ├── dualquaternion.inl.hpp │ │ ├── eigen.hpp │ │ ├── fast_math.hpp │ │ ├── hal │ │ │ ├── hal.hpp │ │ │ ├── interface.h │ │ │ ├── intrin.hpp │ │ │ ├── intrin_avx.hpp │ │ │ ├── intrin_avx512.hpp │ │ │ ├── intrin_cpp.hpp │ │ │ ├── intrin_forward.hpp │ │ │ ├── intrin_msa.hpp │ │ │ ├── intrin_neon.hpp │ │ │ ├── intrin_rvv.hpp │ │ │ ├── intrin_rvv071.hpp │ │ │ ├── intrin_sse.hpp │ │ │ ├── intrin_sse_em.hpp │ │ │ ├── intrin_vsx.hpp │ │ │ ├── intrin_wasm.hpp │ │ │ ├── msa_macros.h │ │ │ └── simd_utils.impl.hpp │ │ ├── mat.hpp │ │ ├── mat.inl.hpp │ │ ├── matx.hpp │ │ ├── neon_utils.hpp │ │ ├── ocl.hpp │ │ ├── ocl_genbase.hpp │ │ ├── opencl │ │ │ ├── ocl_defs.hpp │ │ │ ├── opencl_info.hpp │ │ │ ├── opencl_svm.hpp │ │ │ └── runtime │ │ │ │ ├── autogenerated │ │ │ │ ├── opencl_clblas.hpp │ │ │ │ ├── opencl_clfft.hpp │ │ │ │ ├── opencl_core.hpp │ │ │ │ ├── opencl_core_wrappers.hpp │ │ │ │ ├── opencl_gl.hpp │ │ │ │ └── opencl_gl_wrappers.hpp │ │ │ │ ├── opencl_clblas.hpp │ │ │ │ ├── opencl_clfft.hpp │ │ │ │ ├── opencl_core.hpp │ │ │ │ ├── opencl_core_wrappers.hpp │ │ │ │ ├── opencl_gl.hpp │ │ │ │ ├── opencl_gl_wrappers.hpp │ │ │ │ ├── opencl_svm_20.hpp │ │ │ │ ├── opencl_svm_definitions.hpp │ │ │ │ └── opencl_svm_hsa_extension.hpp │ │ ├── opengl.hpp │ │ ├── operations.hpp │ │ ├── optim.hpp │ │ ├── ovx.hpp │ │ ├── parallel │ │ │ ├── backend │ │ │ │ ├── parallel_for.openmp.hpp │ │ │ │ └── parallel_for.tbb.hpp │ │ │ └── parallel_backend.hpp │ │ ├── persistence.hpp │ │ ├── quaternion.hpp │ │ ├── quaternion.inl.hpp │ │ ├── saturate.hpp │ │ ├── simd_intrinsics.hpp │ │ ├── softfloat.hpp │ │ ├── sse_utils.hpp │ │ ├── traits.hpp │ │ ├── types.hpp │ │ ├── types_c.h │ │ ├── utility.hpp │ │ ├── utils │ │ │ ├── allocator_stats.hpp │ │ │ ├── allocator_stats.impl.hpp │ │ │ ├── filesystem.hpp │ │ │ ├── fp_control_utils.hpp │ │ │ ├── instrumentation.hpp │ │ │ ├── logger.defines.hpp │ │ │ ├── logger.hpp │ │ │ ├── logtag.hpp │ │ │ ├── tls.hpp │ │ │ └── trace.hpp │ │ ├── va_intel.hpp │ │ ├── version.hpp │ │ └── vsx_utils.hpp │ │ ├── cvconfig.h │ │ ├── features2d.hpp │ │ ├── features2d │ │ ├── features2d.hpp │ │ └── hal │ │ │ └── interface.h │ │ ├── flann.hpp │ │ ├── flann │ │ ├── all_indices.h │ │ ├── allocator.h │ │ ├── any.h │ │ ├── autotuned_index.h │ │ ├── composite_index.h │ │ ├── config.h │ │ ├── defines.h │ │ ├── dist.h │ │ ├── dummy.h │ │ ├── dynamic_bitset.h │ │ ├── flann.hpp │ │ ├── flann_base.hpp │ │ ├── general.h │ │ ├── ground_truth.h │ │ ├── hdf5.h │ │ ├── heap.h │ │ ├── hierarchical_clustering_index.h │ │ ├── index_testing.h │ │ ├── kdtree_index.h │ │ ├── kdtree_single_index.h │ │ ├── kmeans_index.h │ │ ├── linear_index.h │ │ ├── logger.h │ │ ├── lsh_index.h │ │ ├── lsh_table.h │ │ ├── matrix.h │ │ ├── miniflann.hpp │ │ ├── nn_index.h │ │ ├── object_factory.h │ │ ├── params.h │ │ ├── random.h │ │ ├── result_set.h │ │ ├── sampling.h │ │ ├── saving.h │ │ ├── simplex_downhill.h │ │ └── timer.h │ │ ├── imgcodecs.hpp │ │ ├── imgcodecs │ │ ├── imgcodecs.hpp │ │ ├── imgcodecs_c.h │ │ ├── ios.h │ │ ├── legacy │ │ │ └── constants_c.h │ │ └── macosx.h │ │ ├── imgproc.hpp │ │ ├── imgproc │ │ ├── bindings.hpp │ │ ├── detail │ │ │ └── gcgraph.hpp │ │ ├── hal │ │ │ ├── hal.hpp │ │ │ └── interface.h │ │ ├── imgproc.hpp │ │ ├── imgproc_c.h │ │ ├── segmentation.hpp │ │ └── types_c.h │ │ ├── opencv.hpp │ │ ├── opencv_modules.hpp │ │ ├── photo.hpp │ │ └── photo │ │ ├── cuda.hpp │ │ ├── legacy │ │ └── constants_c.h │ │ └── photo.hpp │ ├── java │ └── org │ │ └── opencv │ │ ├── android │ │ ├── AsyncServiceHelper.java │ │ ├── BaseLoaderCallback.java │ │ ├── Camera2Renderer.java │ │ ├── CameraActivity.java │ │ ├── CameraBridgeViewBase.java │ │ ├── CameraGLRendererBase.java │ │ ├── CameraGLSurfaceView.java │ │ ├── CameraRenderer.java │ │ ├── FpsMeter.java │ │ ├── InstallCallbackInterface.java │ │ ├── JavaCamera2View.java │ │ ├── JavaCameraView.java │ │ ├── LoaderCallbackInterface.java │ │ ├── OpenCVLoader.java │ │ ├── StaticHelper.java │ │ └── Utils.java │ │ ├── calib3d │ │ ├── Calib3d.java │ │ ├── StereoBM.java │ │ ├── StereoMatcher.java │ │ ├── StereoSGBM.java │ │ └── UsacParams.java │ │ ├── core │ │ ├── Algorithm.java │ │ ├── Core.java │ │ ├── CvException.java │ │ ├── CvType.java │ │ ├── DMatch.java │ │ ├── KeyPoint.java │ │ ├── Mat.java │ │ ├── MatAt.kt │ │ ├── MatMatMul.kt │ │ ├── MatOfByte.java │ │ ├── MatOfDMatch.java │ │ ├── MatOfDouble.java │ │ ├── MatOfFloat.java │ │ ├── MatOfFloat4.java │ │ ├── MatOfFloat6.java │ │ ├── MatOfInt.java │ │ ├── MatOfInt4.java │ │ ├── MatOfKeyPoint.java │ │ ├── MatOfPoint.java │ │ ├── MatOfPoint2f.java │ │ ├── MatOfPoint3.java │ │ ├── MatOfPoint3f.java │ │ ├── MatOfRect.java │ │ ├── MatOfRect2d.java │ │ ├── MatOfRotatedRect.java │ │ ├── Point.java │ │ ├── Point3.java │ │ ├── Range.java │ │ ├── Rect.java │ │ ├── Rect2d.java │ │ ├── RotatedRect.java │ │ ├── Scalar.java │ │ ├── Size.java │ │ ├── TermCriteria.java │ │ └── TickMeter.java │ │ ├── engine │ │ └── OpenCVEngineInterface.aidl │ │ ├── features2d │ │ ├── AKAZE.java │ │ ├── AffineFeature.java │ │ ├── AgastFeatureDetector.java │ │ ├── BFMatcher.java │ │ ├── BOWImgDescriptorExtractor.java │ │ ├── BOWKMeansTrainer.java │ │ ├── BOWTrainer.java │ │ ├── BRISK.java │ │ ├── DescriptorMatcher.java │ │ ├── FastFeatureDetector.java │ │ ├── Feature2D.java │ │ ├── Features2d.java │ │ ├── FlannBasedMatcher.java │ │ ├── GFTTDetector.java │ │ ├── KAZE.java │ │ ├── MSER.java │ │ ├── ORB.java │ │ ├── SIFT.java │ │ ├── SimpleBlobDetector.java │ │ └── SimpleBlobDetector_Params.java │ │ ├── imgcodecs │ │ └── Imgcodecs.java │ │ ├── imgproc │ │ ├── CLAHE.java │ │ ├── GeneralizedHough.java │ │ ├── GeneralizedHoughBallard.java │ │ ├── GeneralizedHoughGuil.java │ │ ├── Imgproc.java │ │ ├── IntelligentScissorsMB.java │ │ ├── LineSegmentDetector.java │ │ ├── Moments.java │ │ └── Subdiv2D.java │ │ ├── photo │ │ ├── AlignExposures.java │ │ ├── AlignMTB.java │ │ ├── CalibrateCRF.java │ │ ├── CalibrateDebevec.java │ │ ├── CalibrateRobertson.java │ │ ├── MergeDebevec.java │ │ ├── MergeExposures.java │ │ ├── MergeMertens.java │ │ ├── MergeMertensForPipeline.java │ │ ├── MergeRobertson.java │ │ ├── Photo.java │ │ ├── Tonemap.java │ │ ├── TonemapDrago.java │ │ ├── TonemapMantiuk.java │ │ └── TonemapReinhard.java │ │ ├── utils │ │ └── Converters.java │ │ ├── video │ │ ├── BackgroundSubtractor.java │ │ ├── BackgroundSubtractorKNN.java │ │ ├── BackgroundSubtractorMOG2.java │ │ ├── DISOpticalFlow.java │ │ ├── DenseOpticalFlow.java │ │ ├── FarnebackOpticalFlow.java │ │ ├── KalmanFilter.java │ │ ├── SparseOpticalFlow.java │ │ ├── SparsePyrLKOpticalFlow.java │ │ ├── Tracker.java │ │ ├── TrackerDaSiamRPN.java │ │ ├── TrackerDaSiamRPN_Params.java │ │ ├── TrackerGOTURN.java │ │ ├── TrackerGOTURN_Params.java │ │ ├── TrackerMIL.java │ │ ├── TrackerMIL_Params.java │ │ ├── VariationalRefinement.java │ │ └── Video.java │ │ └── videoio │ │ ├── VideoCapture.java │ │ ├── VideoWriter.java │ │ └── Videoio.java │ ├── jniLibs │ └── arm64-v8a │ │ ├── libc++_shared.so │ │ └── libopencv_java4.so │ └── res │ └── values │ └── attrs.xml ├── screenshots ├── input-info.jpg ├── main-screen.jpg ├── media.jpg ├── parameters.jpg └── toolbar.jpg └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | /.idea/vcs.xml 11 | /.idea/codeStyles/* 12 | .DS_Store 13 | /build 14 | /captures 15 | .externalNativeBuild 16 | .cxx 17 | local.properties 18 | /app/keystore.config 19 | /app/release/output-metadata.json 20 | /app/release/app-release.apk 21 | /opencv/build 22 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimeLapse 2 | 3 | Create timelapse videos from another video or a series of photos (the photos will be sorted alphabetically). 4 | 5 | * [Main screen](#main-screen) 6 | * [Toolbar](#toolbar) 7 | * [Parameters](#parameters) 8 | * [Media](#media) 9 | * [Examples](#examples) 10 | * [Smooth](#smooth) 11 | * [Transition](#transition) 12 | * [Other](#other) 13 | 14 | ## Main screen 15 | ![](screenshots/main-screen.jpg) 16 | 17 | 18 | ### Toolbar 19 | ![](screenshots/toolbar.jpg) 20 | 21 | In order: 22 | * Open a video file 23 | * Save the generated video 24 | * ... contains 25 | * Open a series of images 26 | * Open an images folder 27 | * Save the last frame a image (full size) 28 | * Settings 29 | 30 | ### Parameters 31 | ![](screenshots/parameters.jpg) 32 | 33 | * Speed: allow to sub-sample input frames (keep only one of "speed" frames). Example: 3x means use only one of 3 frames. 1x means use all frames. 34 | * Align: allow to align frame (for still). If needed you can create a mask. 35 | * Effect: You can select one from the list. Some effect have an extra parameter. For example for Average, 2x means it will output an average of 2 consecutive frames in a sliding window. Ex: 0 and 1, then 1 and 2 ... . It's better to experiment. 36 | * Output FPS: by default it will match the input but can be changed. 37 | * Duration: you can generate a smaller video to test it before creating the full one. 38 | * Orientation: auto (landscape or portrait, depends on the input), landscape or portrait 39 | 40 | ### Media 41 | ![](screenshots/media.jpg) 42 | 43 | In order: 44 | * Play the original video (don't works if the input is a series of images) 45 | * Play the generated video (it will generate it if needed) 46 | * Stop the player 47 | 48 | ## Examples 49 | 50 | ### Smooth 51 | 52 | Original on left vs smooth 10x on right. 53 | 54 | ![](examples/smooth/original_vs_smooth_10x.gif) 55 | 56 | ### Transition 57 | 58 | Input images 59 | 1 | 2 | 3 | 4 60 | -- | -- | -- | -- 61 | ![](examples/transition/input_1.jpg) | ![](examples/transition/input_2.jpg) | ![](examples/transition/input_3.jpg) | ![](examples/transition/input_4.jpg) 62 | 63 | Crop | No Crop 64 | -- | -- 65 | ![](examples/transition/crop.gif) | ![](examples/transition/no-crop.gif) 66 | 67 | ### Other 68 | 69 | Normal | Endless Lightest Pixels 70 | -- | -- 71 | ![](examples/other/simple.gif) | ![](examples/other/endless-lightest-pixels.gif) 72 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keepclassmembers class com.dan.timelapse.utils.Settings { 24 | public *; 25 | } 26 | -------------------------------------------------------------------------------- /app/src/debug/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/debug/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3700B3 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # For more information about using CMake with Android Studio, read the 3 | # documentation: https://d.android.com/studio/projects/add-native-code.html 4 | 5 | # Sets the minimum version of CMake required to build the native library. 6 | 7 | cmake_minimum_required(VERSION 3.18.1) 8 | 9 | # Declares and names the project. 10 | 11 | project("timelapse") 12 | 13 | # Creates and names a library, sets it as either STATIC 14 | # or SHARED, and provides the relative paths to its source code. 15 | # You can define multiple libraries, and CMake builds them for you. 16 | # Gradle automatically packages shared libraries with your APK. 17 | 18 | add_library( # Sets the name of the library. 19 | timelapse 20 | 21 | # Sets the library as a shared library. 22 | SHARED 23 | 24 | # Provides a relative path to your source file(s). 25 | timelapse.cpp ) 26 | 27 | # Searches for a specified prebuilt library and stores the path as a 28 | # variable. Because CMake includes system libraries in the search path by 29 | # default, you only need to specify the name of the public NDK library 30 | # you want to add. CMake verifies that the library exists before 31 | # completing its build. 32 | 33 | find_library( # Sets the name of the path variable. 34 | log-lib 35 | 36 | # Specifies the name of the NDK library that 37 | # you want CMake to locate. 38 | log ) 39 | 40 | # Specifies libraries CMake should link to your target library. You 41 | # can link multiple libraries, such as libraries you define in this 42 | # build script, prebuilt third-party libraries, or system libraries. 43 | 44 | target_link_libraries( # Specifies the target library. 45 | timelapse 46 | 47 | # Links the target library to the log library 48 | # included in the NDK. 49 | -L../../../../../opencv/src/main/jniLibs/${ANDROID_ABI} 50 | opencv_java4 51 | ${log-lib} ) 52 | 53 | include_directories(../../../../opencv/src/main/cpp/include) -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danopdev/TimeLapse/660f23911c644bdcb278e3929dcff3569f52a884/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/AddFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core 4 | import org.opencv.core.Mat 5 | 6 | class AddFramesFilter(size: Int, nextConsumer: FramesConsumer) 7 | : MultiFramesFilter(size, true, nextConsumer) { 8 | 9 | private val sum = Mat() 10 | 11 | override fun stopFilter() { 12 | sum.release() 13 | super.stopFilter() 14 | } 15 | 16 | override fun consume(index: Int, removedFrame: Mat, frames: List) { 17 | val firstFrame = frames.first() 18 | 19 | if (sum.empty()) sum.create(firstFrame.size(), firstFrame.type()) 20 | 21 | firstFrame.copyTo(sum) 22 | for( frameIndex in 1 until frames.size) { 23 | Core.add(sum, frames[frameIndex], sum) 24 | } 25 | 26 | next(index, sum) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/AverageFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.CvType.CV_8UC3 4 | import org.opencv.core.Mat 5 | 6 | class AverageFramesFilter(size: Int, nextConsumer: FramesConsumer) 7 | : SumFramesFilter(size, nextConsumer) { 8 | private val outputFrame = Mat() 9 | 10 | override fun stopFilter() { 11 | outputFrame.release() 12 | super.stopFilter() 13 | } 14 | 15 | override fun consumeSum(index: Int, sum: Mat, frames: List) { 16 | sum.convertTo(outputFrame, CV_8UC3, 1.0 / frames.size) 17 | next(index, outputFrame) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/AverageWeightedForLastFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core 4 | import org.opencv.core.CvType 5 | import org.opencv.core.Mat 6 | 7 | 8 | class AverageWeightedForLastFramesFilter(size: Int, nextConsumer: FramesConsumer) 9 | : SumFramesFilter(size, nextConsumer) { 10 | private val extraSum = Mat() 11 | private val outputFrame = Mat() 12 | 13 | override fun stopFilter() { 14 | outputFrame.release() 15 | extraSum.release() 16 | super.stopFilter() 17 | } 18 | 19 | override fun consumeSum(index: Int, sum: Mat, frames: List) { 20 | val extra = frames.size / 2 21 | Core.addWeighted( sum, 1.0, frames.last(), extra.toDouble(), 0.0, extraSum, sum.type() ) 22 | extraSum.convertTo(outputFrame, CvType.CV_8UC3, 1.0 / (frames.size + extra)) 23 | next(index, outputFrame) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/AverageWeightedForLightFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core 4 | import org.opencv.core.CvType 5 | import org.opencv.core.Mat 6 | 7 | 8 | class AverageWeightedForLightFramesFilter(private val size: Int, nextConsumer: FramesConsumer) 9 | : MultiFramesFilter(size, true, nextConsumer) { 10 | 11 | companion object { 12 | private const val POWER = 6.0 13 | } 14 | 15 | private val power = Mat() 16 | private val sumWeighted = Mat() 17 | private val outputFrame = Mat() 18 | 19 | override fun stopFilter() { 20 | sumWeighted.release() 21 | power.release() 22 | outputFrame.release() 23 | super.stopFilter() 24 | } 25 | 26 | override fun consume(index: Int, removedFrame: Mat, frames: List) { 27 | frames.last().convertTo(power, CvType.CV_64FC3) 28 | Core.pow(power, POWER, power) 29 | 30 | if (sumWeighted.empty()) { 31 | sumWeighted.create(power.size(), power.type()) 32 | power.copyTo(sumWeighted) 33 | } else { 34 | Core.add( sumWeighted, power, sumWeighted) 35 | } 36 | 37 | if (!removedFrame.empty()) { 38 | removedFrame.convertTo(power, CvType.CV_64FC3) 39 | Core.pow(power, POWER, power) 40 | Core.subtract( sumWeighted, power, sumWeighted) 41 | } 42 | 43 | if (frames.size >= size) { 44 | sumWeighted.convertTo(power, sumWeighted.type(), 1.0 / frames.size) 45 | Core.pow(power, 1.0 / POWER, power) 46 | power.convertTo(outputFrame, CvType.CV_8UC3) 47 | next(index, outputFrame) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/DarkestPixelsFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import com.dan.timelapse.images.ImageTools 4 | import org.opencv.core.Mat 5 | 6 | class DarkestPixelsFramesFilter(val size: Int, nextConsumer: FramesConsumer) 7 | : MultiFramesFilter(size, false, nextConsumer) { 8 | 9 | private val outputFrame = Mat() 10 | 11 | override fun stopFilter() { 12 | outputFrame.release() 13 | super.stopFilter() 14 | } 15 | 16 | override fun consume(index: Int, removedFrame: Mat, frames: List) { 17 | ImageTools.mergeDarkestPixels(frames, outputFrame) 18 | next(index, outputFrame) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/EndlessAddFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core 4 | import org.opencv.core.Mat 5 | 6 | class EndlessAddFramesFilter(nextConsumer: FramesConsumer) : FramesFilter(nextConsumer) { 7 | private val sum = Mat() 8 | 9 | override fun consume(index: Int, frame: Mat) { 10 | if (sum.empty()) { 11 | sum.create(frame.size(), frame.type()) 12 | frame.copyTo(sum) 13 | } else { 14 | Core.add(sum, frame, sum) 15 | } 16 | 17 | next(index, sum) 18 | } 19 | 20 | override fun stopFilter() { 21 | sum.release() 22 | super.stopFilter() 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/EndlessAverageFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core 4 | import org.opencv.core.CvType 5 | import org.opencv.core.Mat 6 | 7 | class EndlessAverageFramesFilter(nextConsumer: FramesConsumer) : FramesFilter(nextConsumer) { 8 | private val sum = Mat() 9 | private val outputFrame = Mat() 10 | private var frameCounter = 0 11 | 12 | override fun consume(index: Int, frame: Mat) { 13 | frameCounter++ 14 | 15 | if (sum.empty()) { 16 | frame.convertTo(sum, CvType.CV_32SC3) 17 | next(index, frame) 18 | return 19 | } 20 | 21 | Core.add( sum, frame, sum, Mat(), CvType.CV_32SC3) 22 | sum.convertTo(outputFrame, CvType.CV_8UC3, 1.0 / frameCounter) 23 | next(index, outputFrame) 24 | } 25 | 26 | override fun stopFilter() { 27 | frameCounter = 0 28 | sum.release() 29 | outputFrame.release() 30 | super.stopFilter() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/EndlessAverageWeightedForLastFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core 4 | import org.opencv.core.CvType 5 | import org.opencv.core.Mat 6 | 7 | 8 | class EndlessAverageWeightedForLastFramesFilter(nextConsumer: FramesConsumer) : FramesFilter(nextConsumer) { 9 | private val sum = Mat() 10 | private val sumWeighted = Mat() 11 | private val outputFrame = Mat() 12 | private var size = 0 13 | 14 | override fun consume(index: Int, frame: Mat) { 15 | size++ 16 | 17 | if (sum.empty()) { 18 | frame.convertTo(sum, CvType.CV_32SC3) 19 | next(index, frame) 20 | return 21 | } 22 | 23 | Core.add( sum, frame, sum, Mat(), CvType.CV_32SC3) 24 | 25 | val extra = size / 2 26 | Core.addWeighted( sum, 1.0, frame, extra.toDouble(), 0.0, sumWeighted, sum.type()) 27 | 28 | sumWeighted.convertTo(outputFrame, CvType.CV_8UC3, 1.0 / (size + extra)) 29 | next(index, outputFrame) 30 | } 31 | 32 | override fun stopFilter() { 33 | sum.release() 34 | sumWeighted.release() 35 | outputFrame.release() 36 | size = 0 37 | super.stopFilter() 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/EndlessAverageWeightedForLightFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core 4 | import org.opencv.core.CvType 5 | import org.opencv.core.Mat 6 | 7 | class EndlessAverageWeightedForLightFramesFilter(nextConsumer: FramesConsumer): FramesFilter(nextConsumer) { 8 | 9 | companion object { 10 | private const val POWER = 6.0 11 | } 12 | 13 | private val power = Mat() 14 | private val sumWeighted = Mat() 15 | private val outputFrame = Mat() 16 | private var size = 0 17 | 18 | override fun stopFilter() { 19 | sumWeighted.release() 20 | power.release() 21 | outputFrame.release() 22 | size = 0 23 | super.stopFilter() 24 | } 25 | 26 | override fun consume(index: Int, frame: Mat) { 27 | size++ 28 | 29 | frame.convertTo(power, CvType.CV_64FC3) 30 | Core.pow(power, POWER, power) 31 | 32 | if (sumWeighted.empty()) { 33 | sumWeighted.create(power.size(), power.type()) 34 | power.copyTo(sumWeighted) 35 | next(index, frame) 36 | return 37 | } 38 | 39 | Core.add( sumWeighted, power, sumWeighted) 40 | sumWeighted.convertTo(power, sumWeighted.type(), 1.0 / size) 41 | Core.pow(power, 1.0 / POWER, power) 42 | power.convertTo(outputFrame, CvType.CV_8UC3) 43 | next(index, outputFrame) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/EndlessDarkestPixelsFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import com.dan.timelapse.images.ImageTools 4 | import org.opencv.core.Mat 5 | 6 | class EndlessDarkestPixelsFramesFilter(nextConsumer: FramesConsumer): FramesFilter(nextConsumer) { 7 | private val outputFrame = Mat() 8 | 9 | override fun stopFilter() { 10 | outputFrame.release() 11 | super.stopFilter() 12 | } 13 | 14 | override fun consume(index: Int, frame: Mat) { 15 | if (outputFrame.empty()) { 16 | outputFrame.create(frame.size(), frame.type()) 17 | frame.copyTo(outputFrame) 18 | next(index, frame) 19 | return 20 | } 21 | 22 | ImageTools.mergeDarkestPixels(listOf(outputFrame, frame), outputFrame) 23 | next(index, outputFrame) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/EndlessLightestPixelsFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import com.dan.timelapse.images.ImageTools 4 | import org.opencv.core.Mat 5 | 6 | class EndlessLightestPixelsFramesFilter(nextConsumer: FramesConsumer): FramesFilter(nextConsumer) { 7 | private val outputFrame = Mat() 8 | 9 | override fun stopFilter() { 10 | outputFrame.release() 11 | super.stopFilter() 12 | } 13 | 14 | override fun consume(index: Int, frame: Mat) { 15 | if (outputFrame.empty()) { 16 | outputFrame.create(frame.size(), frame.type()) 17 | frame.copyTo(outputFrame) 18 | next(index, frame) 19 | return 20 | } 21 | 22 | ImageTools.mergeLightestPixels(listOf(outputFrame, frame), outputFrame) 23 | next(index, outputFrame) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/FramesConsumer.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Mat 4 | 5 | interface FramesConsumer { 6 | fun start() 7 | fun stop(canceled: Boolean) 8 | fun consume(index: Int, frame: Mat) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/FramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Mat 4 | 5 | abstract class FramesFilter(private val nextConsumer: FramesConsumer) : FramesConsumer { 6 | open fun startFilter() {} 7 | open fun stopFilter() {} 8 | 9 | fun next(index: Int, frame: Mat) { 10 | nextConsumer.consume(index, frame) 11 | } 12 | 13 | override fun start() { 14 | startFilter() 15 | nextConsumer.start() 16 | } 17 | 18 | override fun stop(canceled: Boolean) { 19 | nextConsumer.stop(canceled) 20 | stopFilter() 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/HDRFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Mat 4 | import org.opencv.photo.Photo 5 | 6 | class HDRFramesFilter(private val size: Int, nextConsumer: FramesConsumer) 7 | : MultiFramesFilter(size, true, nextConsumer) { 8 | private val hdrFrame = Mat() 9 | private val outputFrame = Mat() 10 | private val mergeMertens = Photo.createMergeMertensForPipeline() 11 | 12 | override fun stopFilter() { 13 | hdrFrame.release() 14 | outputFrame.release() 15 | mergeMertens.release() 16 | super.stopFilter() 17 | } 18 | 19 | override fun consume(index: Int, removedFrame: Mat, frames: List) { 20 | mergeMertens.push(frames.last()) 21 | if (!removedFrame.empty()) mergeMertens.pop() 22 | 23 | if (frames.size < size) return 24 | 25 | mergeMertens.process(hdrFrame) 26 | 27 | if (!hdrFrame.empty()) { 28 | hdrFrame.convertTo(outputFrame, frames[0].type(), 255.0) 29 | next(index, outputFrame) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/LightestPixelsFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import com.dan.timelapse.images.ImageTools 4 | import org.opencv.core.Mat 5 | 6 | class LightestPixelsFramesFilter(val size: Int, nextConsumer: FramesConsumer) 7 | : MultiFramesFilter(size, false, nextConsumer) { 8 | 9 | private val outputFrame = Mat() 10 | 11 | override fun stopFilter() { 12 | outputFrame.release() 13 | super.stopFilter() 14 | } 15 | 16 | override fun consume(index: Int, removedFrame: Mat, frames: List) { 17 | ImageTools.mergeLightestPixels(frames, outputFrame) 18 | next(index, outputFrame) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/MultiFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Mat 4 | 5 | abstract class MultiFramesFilter(private val size: Int, private val notifyOnPartial: Boolean, nextConsumer: FramesConsumer) 6 | : FramesFilter(nextConsumer) { 7 | 8 | private val frames = mutableListOf() 9 | 10 | abstract fun consume(index: Int, removedFrame: Mat, frames: List) 11 | 12 | override fun consume(index: Int, frame: Mat) { 13 | frames.add(frame.clone()) 14 | 15 | val removedFrame = if (frames.size > size) frames.removeFirst() else Mat() 16 | 17 | if (frames.size >= size || notifyOnPartial) { 18 | consume(index, removedFrame, frames) 19 | } 20 | 21 | removedFrame.release() //force to free memory immediately 22 | } 23 | 24 | override fun stopFilter() { 25 | frames.forEach{ frame -> frame.release() } //force to free memory immediately 26 | frames.clear() 27 | 28 | super.stopFilter() 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/SampleFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Mat 4 | 5 | class SampleFramesFilter(private val sample: Int, nextConsumer: FramesConsumer): FramesFilter(nextConsumer) { 6 | private var counter = 0 7 | 8 | override fun startFilter() { 9 | counter = 0 10 | super.startFilter() 11 | } 12 | 13 | override fun consume(index: Int, frame: Mat) { 14 | if (0 == counter) next(index, frame) 15 | counter++ 16 | if (counter >= sample) counter = 0 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/SumFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core 4 | import org.opencv.core.CvType 5 | import org.opencv.core.Mat 6 | 7 | 8 | abstract class SumFramesFilter(private val size: Int, nextConsumer: FramesConsumer) 9 | : MultiFramesFilter(size, true, nextConsumer) { 10 | private val sum = Mat() 11 | 12 | override fun stopFilter() { 13 | sum.release() 14 | super.stopFilter() 15 | } 16 | 17 | abstract fun consumeSum(index: Int, sum: Mat, frames: List) 18 | 19 | override fun consume(index: Int, removedFrame: Mat, frames: List) { 20 | val newFrame = frames.last() 21 | 22 | if (sum.empty()) { 23 | newFrame.convertTo(sum, CvType.CV_16UC3) 24 | } else { 25 | Core.add( sum, newFrame, sum, Mat(), CvType.CV_16UC3) 26 | } 27 | 28 | if (!removedFrame.empty()) { 29 | Core.subtract( sum, removedFrame, sum, Mat(), CvType.CV_16UC3) 30 | } 31 | 32 | if (frames.size >= size) { 33 | consumeSum(index, sum, frames) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/filters/TransitionFramesFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.filters 2 | 3 | import org.opencv.core.Core.addWeighted 4 | import org.opencv.core.Mat 5 | 6 | class TransitionFramesFilter(private val size: Int, nextConsumer: FramesConsumer) 7 | : FramesFilter(nextConsumer) { 8 | private val outputFrame = Mat() 9 | private var prevFrame = Mat() 10 | 11 | override fun stopFilter() { 12 | outputFrame.release() 13 | prevFrame.release() 14 | super.stopFilter() 15 | } 16 | 17 | override fun consume(index: Int, frame: Mat) { 18 | if (!prevFrame.empty()) { 19 | for(step in 1 until size) { 20 | val lastFrameWeight = (size - step).toDouble() / size 21 | addWeighted(prevFrame, lastFrameWeight, frame, 1.0 - lastFrameWeight, 0.0, outputFrame) 22 | next(index, outputFrame) 23 | } 24 | prevFrame.release() 25 | } 26 | 27 | next(index, frame) 28 | prevFrame = frame.clone() 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/framesinput/FramesInput.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.framesinput 2 | 3 | import android.net.Uri 4 | import org.opencv.core.Mat 5 | 6 | abstract class FramesInput { 7 | companion object { 8 | fun fixName(original: String?): String { 9 | if (null == original) return "unknown" 10 | return original.split('.')[0] 11 | } 12 | } 13 | 14 | abstract val fps: Int 15 | abstract val name: String 16 | abstract val width: Int 17 | abstract val height: Int 18 | abstract val size: Int 19 | abstract val videoUri: Uri? 20 | 21 | abstract fun forEachFrame(callback: (Int, Int, Mat)->Boolean) 22 | 23 | fun firstFrame(): Mat { 24 | var firstFrame = Mat() 25 | forEachFrame { _, _, frame -> 26 | firstFrame = frame.clone() 27 | false 28 | } 29 | return firstFrame 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/images/ImageTools.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.images 2 | 3 | import org.opencv.core.Mat 4 | import org.opencv.utils.Converters 5 | 6 | class ImageTools { 7 | companion object { 8 | fun mergeLightestPixels(images: List, output: Mat) { 9 | val imagesMat = Converters.vector_Mat_to_Mat(images) 10 | mergeLightestPixelsNative(imagesMat.nativeObj, output.nativeObj) 11 | } 12 | 13 | fun mergeDarkestPixels(images: List, output: Mat) { 14 | val imagesMat = Converters.vector_Mat_to_Mat(images) 15 | mergeDarkestPixelsNative(imagesMat.nativeObj, output.nativeObj) 16 | } 17 | 18 | private external fun mergeLightestPixelsNative(images: Long, output: Long) 19 | private external fun mergeDarkestPixelsNative(images: Long, output: Long) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/images/ImageWriter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.images 2 | 3 | import android.graphics.Bitmap 4 | import com.dan.timelapse.filters.FramesConsumer 5 | import org.opencv.android.Utils 6 | import org.opencv.core.Mat 7 | import java.io.File 8 | 9 | class ImageWriter(private val file: File, private val jpegQuality: Int) 10 | : FramesConsumer { 11 | 12 | private var lastFrame = Mat() 13 | private var _success = false 14 | 15 | val success: Boolean 16 | get() = _success 17 | 18 | override fun start() { 19 | } 20 | 21 | override fun stop(canceled: Boolean) { 22 | if (!canceled && !lastFrame.empty()) { 23 | val bitmap = Bitmap.createBitmap( 24 | lastFrame.width(), 25 | lastFrame.height(), 26 | Bitmap.Config.ARGB_8888 27 | ) 28 | 29 | Utils.matToBitmap( lastFrame, bitmap) 30 | 31 | try { 32 | val outputStream = file.outputStream() 33 | bitmap.compress(Bitmap.CompressFormat.JPEG, jpegQuality, outputStream) 34 | outputStream.close() 35 | _success = true 36 | } catch (e: Exception) { 37 | e.printStackTrace() 38 | } 39 | } 40 | 41 | lastFrame.release() 42 | } 43 | 44 | override fun consume(index: Int, frame: Mat) { 45 | lastFrame.create(frame.size(), frame.type()) 46 | frame.copyTo(lastFrame) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/screens/AppFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.screens 2 | 3 | import androidx.fragment.app.Fragment 4 | import com.dan.timelapse.MainActivity 5 | 6 | open class AppFragment(val activity: MainActivity) : Fragment() { 7 | 8 | val settings = activity.settings 9 | 10 | fun runOnUiThread(action: ()->Unit) { 11 | activity.runOnUiThread(action) 12 | } 13 | 14 | open fun onBack(homeButton: Boolean) { 15 | } 16 | 17 | fun showToast(message: String) { 18 | runOnUiThread { 19 | activity.showToast(message) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/utils/OutputParams.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.utils 2 | 3 | class OutputParams { 4 | companion object { 5 | const val VALUE_UNKNOWN = Int.MIN_VALUE 6 | 7 | const val COMPARE_NOT_CHANGED = 0 8 | const val COMPARE_CHANGED_ONLY_FPS = 1 9 | const val COMPARE_CHANGED = 2 10 | 11 | const val KEY_H265 = "H265" 12 | const val KEY_CROP = "CROP" 13 | const val KEY_FPS = "FPS" 14 | const val KEY_SPEED = "SPEED" 15 | const val KEY_ALIGN = "ALIGN" 16 | const val KEY_EFFECT = "EFFECT" 17 | const val KEY_EFFECT_SIZE = "EFFECT-SIZE" 18 | const val KEY_DURATION = "DURATION" 19 | const val KEY_WIDTH = "WIDTH" 20 | const val KEY_HEIGHT = "HEIGHT" 21 | 22 | private fun compare(a: Map, b: Map): Int { 23 | var fpsChanged = false 24 | 25 | a.forEach { (key, value) -> 26 | if (value != b.getOrDefault(key, VALUE_UNKNOWN)) { 27 | if (KEY_FPS == key) { 28 | fpsChanged = true 29 | } else { 30 | return COMPARE_CHANGED 31 | } 32 | } 33 | } 34 | 35 | return if (fpsChanged) COMPARE_CHANGED_ONLY_FPS else COMPARE_NOT_CHANGED 36 | } 37 | } 38 | 39 | private val _params = mutableMapOf() 40 | 41 | fun set(key: String, value: Int) { 42 | _params[key] = value 43 | } 44 | 45 | fun get(key: String): Int { 46 | return _params.getOrDefault(key, VALUE_UNKNOWN) 47 | } 48 | 49 | fun compareWith(other: OutputParams?): Int { 50 | if (null == other) return COMPARE_CHANGED 51 | 52 | val compare1 = compare(_params, other._params) 53 | if (COMPARE_CHANGED == compare1) return COMPARE_CHANGED 54 | 55 | val compare2 = compare(other._params, _params) 56 | if (COMPARE_CHANGED == compare2) return COMPARE_CHANGED 57 | 58 | if (COMPARE_CHANGED_ONLY_FPS == compare1 || COMPARE_CHANGED_ONLY_FPS == compare2) return COMPARE_CHANGED_ONLY_FPS 59 | return COMPARE_NOT_CHANGED 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dan/timelapse/video/VideoWriter.kt: -------------------------------------------------------------------------------- 1 | package com.dan.timelapse.video 2 | 3 | import com.dan.timelapse.filters.FramesConsumer 4 | import org.opencv.core.Mat 5 | 6 | class VideoWriter( 7 | private val path: String, 8 | private val fps: Int, 9 | private val h265: Boolean) 10 | : FramesConsumer { 11 | 12 | private var encoder: VideoEncoder? = null 13 | 14 | override fun start() { 15 | 16 | } 17 | 18 | override fun stop(canceled: Boolean) { 19 | encoder?.let { 20 | it.release() 21 | encoder = null 22 | } 23 | } 24 | 25 | override fun consume(index: Int, frame: Mat) { 26 | if (null == encoder) { 27 | encoder = VideoEncoder.create(path, fps, frame.width(), frame.height(), 0, h265) 28 | } 29 | 30 | encoder?.write(frame) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/busy_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 22 | 23 | 33 | 34 |